org.jetbrains.idea.svn.treeConflict.MergeFromTheirsResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.jetbrains.idea.svn.treeConflict.MergeFromTheirsResolver.java

Source

/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.idea.svn.treeConflict;

import com.intellij.CommonBundle;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diff.impl.patch.*;
import com.intellij.openapi.diff.impl.patch.formove.PatchApplier;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.MessageDialogBuilder;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser;
import com.intellij.openapi.vcs.changes.patch.ApplyPatchDifferentiatedDialog;
import com.intellij.openapi.vcs.changes.patch.ApplyPatchExecutor;
import com.intellij.openapi.vcs.changes.patch.ApplyPatchMode;
import com.intellij.openapi.vcs.changes.patch.FilePatchInProgress;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.SmartList;
import com.intellij.util.containers.Convertor;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.continuation.Continuation;
import com.intellij.util.continuation.ContinuationContext;
import com.intellij.util.continuation.TaskDescriptor;
import com.intellij.util.continuation.Where;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.history.SvnChangeList;
import org.jetbrains.idea.svn.history.SvnRepositoryLocation;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNTreeConflictDescription;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * User: Irina.Chernushina
 * Date: 5/18/12
 * Time: 2:44 PM
 */
public class MergeFromTheirsResolver {
    private final SvnVcs myVcs;
    private final SVNTreeConflictDescription myDescription;
    private final Change myChange;
    private final FilePath myOldFilePath;
    private final FilePath myNewFilePath;
    private final String myOldPresentation;
    private final String myNewPresentation;
    private final SvnRevisionNumber myCommittedRevision;
    private Boolean myAdd;

    private final List<Change> myTheirsChanges;
    private final List<Change> myTheirsBinaryChanges;
    private final List<VcsException> myWarnings;
    private List<TextFilePatch> myTextPatches;
    private VirtualFile myBaseForPatch;

    public MergeFromTheirsResolver(SvnVcs vcs, SVNTreeConflictDescription description, Change change,
            SvnRevisionNumber revision) {
        myVcs = vcs;
        myDescription = description;
        myChange = change;
        myCommittedRevision = revision;
        myOldFilePath = myChange.getBeforeRevision().getFile();
        myNewFilePath = myChange.getAfterRevision().getFile();
        myBaseForPatch = ChangesUtil.findValidParentAccurately(myNewFilePath);
        myOldPresentation = TreeConflictRefreshablePanel.filePath(myOldFilePath);
        myNewPresentation = TreeConflictRefreshablePanel.filePath(myNewFilePath);

        myTheirsChanges = new ArrayList<Change>();
        myTheirsBinaryChanges = new ArrayList<Change>();
        myWarnings = new ArrayList<VcsException>();
        myTextPatches = Collections.emptyList();
    }

    public void execute() {
        int ok = Messages.showOkCancelDialog(myVcs.getProject(),
                (myChange.isMoved()
                        ? SvnBundle.message("confirmation.resolve.tree.conflict.merge.moved", myOldPresentation,
                                myNewPresentation)
                        : SvnBundle.message("confirmation.resolve.tree.conflict.merge.renamed", myOldPresentation,
                                myNewPresentation)),
                TreeConflictRefreshablePanel.TITLE, Messages.getQuestionIcon());
        if (Messages.OK != ok)
            return;

        FileDocumentManager.getInstance().saveAllDocuments();
        //final String name = "Merge changes from theirs for: " + myOldPresentation;

        final Continuation fragmented = Continuation.createFragmented(myVcs.getProject(), false);
        fragmented.addExceptionHandler(VcsException.class, new Consumer<VcsException>() {
            @Override
            public void consume(VcsException e) {
                myWarnings.add(e);
                if (e.isWarning()) {
                    return;
                }
                AbstractVcsHelper.getInstance(myVcs.getProject()).showErrors(myWarnings,
                        TreeConflictRefreshablePanel.TITLE);
            }
        });

        final List<TaskDescriptor> tasks = new SmartList<TaskDescriptor>();
        if (SVNNodeKind.DIR.equals(myDescription.getNodeKind())) {
            tasks.add(new PreloadChangesContentsForDir());
        } else {
            tasks.add(new PreloadChangesContentsForFile());
        }
        tasks.add(new ConvertTextPaths());
        tasks.add(new PatchCreator());
        tasks.add(new SelectPatchesInApplyPatchDialog());
        tasks.add(new SelectBinaryFiles());

        fragmented.run(tasks);
    }

    private void appendResolveConflictToContext(final ContinuationContext context) {
        context.next(new ResolveConflictInSvn());
    }

    private void appendTailToContextLast(final ContinuationContext context) {
        context.last(new ApplyBinaryChanges(), new FinalNotification());
    }

    private List<Change> filterOutBinary(List<Change> paths) {
        List<Change> result = null;
        for (Iterator<Change> iterator = paths.iterator(); iterator.hasNext();) {
            final Change change = iterator.next();
            if (ChangesUtil.isBinaryChange(change)) {
                result = (result == null ? new SmartList<Change>() : result);
                result.add(change);
                iterator.remove();
            }
        }
        return result;
    }

    private class FinalNotification extends TaskDescriptor {
        private FinalNotification() {
            super("", Where.AWT);
        }

        @Override
        public void run(ContinuationContext context) {
            final StringBuilder message = new StringBuilder().append("Theirs changes merged for ")
                    .append(myOldPresentation);
            VcsBalloonProblemNotifier.showOverChangesView(myVcs.getProject(), message.toString(), MessageType.INFO);
            if (!myWarnings.isEmpty()) {
                AbstractVcsHelper.getInstance(myVcs.getProject()).showErrors(myWarnings,
                        TreeConflictRefreshablePanel.TITLE);
            }
        }
    }

    private class ResolveConflictInSvn extends TaskDescriptor {
        private ResolveConflictInSvn() {
            super("Accepting working state", Where.POOLED);
        }

        @Override
        public void run(ContinuationContext context) {
            try {
                new SvnTreeConflictResolver(myVcs, myOldFilePath, myCommittedRevision, null)
                        .resolveSelectMineFull(myDescription);
            } catch (VcsException e1) {
                context.handleException(e1, false);
            }
        }
    }

    private class ConvertTextPaths extends TaskDescriptor {
        private ConvertTextPaths() {
            super("", Where.AWT);
        }

        @Override
        public void run(ContinuationContext context) {
            initAddOption();
            List<Change> convertedChanges = new SmartList<Change>();
            try {
                // revision contents is preloaded, so ok to call in awt
                convertedChanges = convertPaths(myTheirsChanges);
            } catch (VcsException e) {
                context.handleException(e, true);
            }
            myTheirsChanges.clear();
            myTheirsChanges.addAll(convertedChanges);
        }
    }

    private class SelectPatchesInApplyPatchDialog extends TaskDescriptor {
        private SelectPatchesInApplyPatchDialog() {
            super("", Where.AWT);
        }

        @Override
        public void run(ContinuationContext context) {
            final ChangeListManager clManager = ChangeListManager.getInstance(myVcs.getProject());
            final LocalChangeList changeList = clManager.getChangeList(myChange);
            final ApplyPatchDifferentiatedDialog dialog = new ApplyPatchDifferentiatedDialog(myVcs.getProject(),
                    new TreeConflictApplyTheirsPatchExecutor(myVcs, context, myBaseForPatch),
                    Collections.<ApplyPatchExecutor>singletonList(
                            new ApplyPatchSaveToFileExecutor(myVcs.getProject(), myBaseForPatch)),
                    ApplyPatchMode.APPLY_PATCH_IN_MEMORY, myTextPatches, changeList);
            context.suspend();
            dialog.show();
        }
    }

    private class TreeConflictApplyTheirsPatchExecutor implements ApplyPatchExecutor {
        private final SvnVcs myVcs;
        private final ContinuationContext myInner;
        private final VirtualFile myBaseDir;

        public TreeConflictApplyTheirsPatchExecutor(SvnVcs vcs, ContinuationContext inner,
                final VirtualFile baseDir) {
            myVcs = vcs;
            myInner = inner;
            myBaseDir = baseDir;
        }

        @Override
        public String getName() {
            return "Apply patch";
        }

        @Override
        public void apply(MultiMap<VirtualFile, FilePatchInProgress> patchGroups, LocalChangeList localList,
                String fileName,
                TransparentlyFailedValueI<Map<String, Map<String, CharSequence>>, PatchSyntaxException> additionalInfo) {
            final List<FilePatch> patches;
            try {
                patches = ApplyPatchSaveToFileExecutor.patchGroupsToOneGroup(patchGroups, myBaseDir);
            } catch (IOException e) {
                myInner.handleException(e, true);
                return;
            }

            final PatchApplier<BinaryFilePatch> patchApplier = new PatchApplier<BinaryFilePatch>(myVcs.getProject(),
                    myBaseDir, patches, localList, null, null);
            patchApplier.scheduleSelf(false, myInner, true); // 3
            boolean thereAreCreations = false;
            for (FilePatch patch : patches) {
                if (patch.isNewFile() || !Comparing.equal(patch.getAfterName(), patch.getBeforeName())) {
                    thereAreCreations = true;
                    break;
                }
            }
            if (thereAreCreations) {
                // restore deletion of old directory:
                myInner.next(new DirectoryAddition()); // 2
            }
            appendResolveConflictToContext(myInner); // 1
            appendTailToContextLast(myInner); // 4
            myInner.ping();
        }
    }

    private class DirectoryAddition extends TaskDescriptor {
        private DirectoryAddition() {
            super("Adding " + myOldPresentation + " to Subversion", Where.POOLED);
        }

        @Override
        public void run(ContinuationContext context) {
            try {
                myVcs.createWCClient().doAdd(myOldFilePath.getIOFile(), true, true, true, SVNDepth.EMPTY, false,
                        true);
            } catch (SVNException e) {
                context.handleException(e, true);
            }
        }
    }

    private class PatchCreator extends TaskDescriptor {
        private PatchCreator() {
            super("Creating patch for theirs changes", Where.POOLED);
        }

        @Override
        public void run(ContinuationContext context) {
            final Project project = myVcs.getProject();
            final List<FilePatch> patches;
            try {
                patches = IdeaTextPatchBuilder.buildPatch(project, myTheirsChanges, myBaseForPatch.getPath(),
                        false);
                myTextPatches = ObjectsConvertor.convert(patches, new Convertor<FilePatch, TextFilePatch>() {
                    @Override
                    public TextFilePatch convert(FilePatch o) {
                        return (TextFilePatch) o;
                    }
                });
            } catch (VcsException e) {
                context.handleException(e, true);
            }
        }
    }

    private class SelectBinaryFiles extends TaskDescriptor {
        private SelectBinaryFiles() {
            super("", Where.AWT);
        }

        @Override
        public void run(ContinuationContext context) {
            if (myTheirsBinaryChanges.isEmpty())
                return;
            final List<Change> converted;
            try {
                converted = convertPaths(myTheirsBinaryChanges);
            } catch (VcsException e) {
                context.handleException(e, true);
                return;
            }
            if (converted.isEmpty())
                return;
            final Map<FilePath, Change> map = new HashMap<FilePath, Change>();
            for (Change change : converted) {
                map.put(ChangesUtil.getFilePath(change), change);
            }
            final Collection<FilePath> selected = chooseBinaryFiles(converted, map.keySet());
            myTheirsBinaryChanges.clear();
            for (FilePath filePath : selected) {
                myTheirsBinaryChanges.add(map.get(filePath));
            }
        }
    }

    private class ApplyBinaryChanges extends TaskDescriptor {
        private ApplyBinaryChanges() {
            super("", Where.AWT);
        }

        @Override
        public void run(final ContinuationContext context) {
            if (myTheirsBinaryChanges.isEmpty())
                return;
            final Application application = ApplicationManager.getApplication();
            final List<FilePath> dirtyPaths = new ArrayList<FilePath>();
            for (final Change change : myTheirsBinaryChanges) {
                try {
                    application.runWriteAction(new ThrowableComputable<Void, VcsException>() {
                        @Override
                        public Void compute() throws VcsException {
                            try {
                                if (change.getAfterRevision() == null) {
                                    final FilePath path = change.getBeforeRevision().getFile();
                                    dirtyPaths.add(path);
                                    final VirtualFile file = LocalFileSystem.getInstance()
                                            .refreshAndFindFileByIoFile(path.getIOFile());
                                    if (file == null) {
                                        context.handleException(
                                                new VcsException("Can not delete file: " + file.getPath(), true),
                                                false);
                                        return null;
                                    }
                                    file.delete(TreeConflictRefreshablePanel.class);
                                } else {
                                    final FilePath file = change.getAfterRevision().getFile();
                                    dirtyPaths.add(file);
                                    final String parentPath = file.getParentPath().getPath();
                                    final VirtualFile parentFile = VfsUtil.createDirectoryIfMissing(parentPath);
                                    if (parentFile == null) {
                                        context.handleException(
                                                new VcsException("Can not create directory: " + parentPath, true),
                                                false);
                                        return null;
                                    }
                                    final VirtualFile child = parentFile
                                            .createChildData(TreeConflictRefreshablePanel.class, file.getName());
                                    if (child == null) {
                                        context.handleException(
                                                new VcsException("Can not create file: " + file.getPath(), true),
                                                false);
                                        return null;
                                    }
                                    final BinaryContentRevision revision = (BinaryContentRevision) change
                                            .getAfterRevision();
                                    final byte[] content = revision.getBinaryContent();
                                    // actually it was the fix for IDEA-91572 Error saving merged data: Argument 0 for @NotNull parameter of > com/intellij/
                                    if (content == null) {
                                        context.handleException(
                                                new VcsException(
                                                        "Can not load Theirs content for file " + file.getPath()),
                                                false);
                                        return null;
                                    }
                                    child.setBinaryContent(content);
                                }
                            } catch (IOException e) {
                                throw new VcsException(e);
                            }
                            return null;
                        }
                    });
                } catch (VcsException e) {
                    context.handleException(e, true);
                    return;
                }
            }
            VcsDirtyScopeManager.getInstance(myVcs.getProject()).filePathsDirty(dirtyPaths, null);
        }
    }

    private Collection<FilePath> chooseBinaryFiles(List<Change> converted, Set<FilePath> paths) {
        String singleMessage = "";
        if (paths.size() == 1) {
            final Change change = converted.get(0);
            final FileStatus status = change.getFileStatus();
            final FilePath path = ChangesUtil.getFilePath(change);
            final String stringPath = TreeConflictRefreshablePanel.filePath(path);
            if (FileStatus.DELETED.equals(status)) {
                singleMessage = "Delete binary file " + stringPath + " (according to theirs changes)?";
            } else if (FileStatus.ADDED.equals(status)) {
                singleMessage = "Create binary file " + stringPath + " (according to theirs changes)?";
            } else {
                singleMessage = "Apply changes to binary file " + stringPath + " (according to theirs changes)?";
            }
        }
        return AbstractVcsHelper.getInstance(myVcs.getProject()).selectFilePathsToProcess(
                new ArrayList<FilePath>(paths), TreeConflictRefreshablePanel.TITLE, "Select binary files to patch",
                TreeConflictRefreshablePanel.TITLE, singleMessage, new VcsShowConfirmationOption() {

                    @Override
                    public Value getValue() {
                        return null;
                    }

                    @Override
                    public void setValue(Value value) {
                    }

                    @Override
                    public boolean isPersistent() {
                        return false;
                    }
                });
    }

    private List<Change> convertPaths(List<Change> changesForPatch) throws VcsException {
        initAddOption();
        final List<Change> changes = new ArrayList<Change>();
        for (Change change : changesForPatch) {
            if (!isUnderOldDir(change, myOldFilePath))
                continue;
            ContentRevision before = null;
            ContentRevision after = null;
            if (change.getBeforeRevision() != null) {
                before = new SimpleContentRevision(change.getBeforeRevision().getContent(),
                        rebasePath(myOldFilePath, myNewFilePath, change.getBeforeRevision().getFile()),
                        change.getBeforeRevision().getRevisionNumber().asString());
            }
            if (change.getAfterRevision() != null) {
                // if addition or move - do not move to the new path
                if (myAdd && (change.getBeforeRevision() == null || change.isMoved() || change.isRenamed())) {
                    after = change.getAfterRevision();
                } else {
                    after = new SimpleContentRevision(change.getAfterRevision().getContent(),
                            rebasePath(myOldFilePath, myNewFilePath, change.getAfterRevision().getFile()),
                            change.getAfterRevision().getRevisionNumber().asString());
                }
            }
            changes.add(new Change(before, after));
        }
        return changes;
    }

    private boolean isUnderOldDir(Change change, FilePath path) {
        if (change.getBeforeRevision() != null) {
            final boolean isUnder = FileUtil.isAncestor(path.getIOFile(),
                    change.getBeforeRevision().getFile().getIOFile(), true);
            if (isUnder) {
                return true;
            }
        }
        if (change.getAfterRevision() != null) {
            final boolean isUnder = FileUtil.isAncestor(path.getIOFile(),
                    change.getAfterRevision().getFile().getIOFile(), true);
            if (isUnder) {
                return isUnder;
            }
        }
        return false;
    }

    private FilePath rebasePath(final FilePath oldBase, final FilePath newBase, final FilePath path) {
        final String relativePath = FileUtil.getRelativePath(oldBase.getPath(), path.getPath(), File.separatorChar);
        //if (StringUtil.isEmptyOrSpaces(relativePath)) return path;
        return ((FilePathImpl) newBase).createChild(relativePath, path.isDirectory());
    }

    private class PreloadChangesContentsForFile extends TaskDescriptor {
        private PreloadChangesContentsForFile() {
            super("Getting base and theirs revisions content", Where.POOLED);
        }

        @Override
        public void run(ContinuationContext context) {
            final SvnContentRevision base = SvnContentRevision.createBaseRevision(myVcs, myNewFilePath,
                    myCommittedRevision.getRevision());
            final SvnContentRevision remote = SvnContentRevision.createRemote(myVcs, myOldFilePath,
                    SVNRevision.create(myDescription.getSourceRightVersion().getPegRevision()));
            try {
                final ContentRevision newBase = new SimpleContentRevision(base.getContent(), myNewFilePath,
                        base.getRevisionNumber().asString());
                final ContentRevision newRemote = new SimpleContentRevision(remote.getContent(), myNewFilePath,
                        remote.getRevisionNumber().asString());
                myTheirsChanges.add(new Change(newBase, newRemote));
            } catch (VcsException e) {
                context.handleException(e, true);
            }
        }
    }

    private class PreloadChangesContentsForDir extends TaskDescriptor {
        private PreloadChangesContentsForDir() {
            super("Getting base and theirs revisions content", Where.POOLED);
        }

        @Override
        public void run(ContinuationContext context) {
            final List<Change> changesForPatch;
            try {
                final List<CommittedChangeList> lst = loadSvnChangeListsForPatch(myDescription);
                changesForPatch = CommittedChangesTreeBrowser.collectChanges(lst, true);
                for (Change change : changesForPatch) {
                    if (change.getBeforeRevision() != null) {
                        preloadRevisionContents(change.getBeforeRevision());
                    }
                    if (change.getAfterRevision() != null) {
                        preloadRevisionContents(change.getAfterRevision());
                    }
                }
            } catch (VcsException e) {
                context.handleException(e, true);
                return;
            }
            final List<Change> binaryChanges = filterOutBinary(changesForPatch);
            if (binaryChanges != null && !binaryChanges.isEmpty()) {
                myTheirsBinaryChanges.addAll(binaryChanges);
            }
            if (!changesForPatch.isEmpty()) {
                myTheirsChanges.addAll(changesForPatch);
            }
        }
    }

    private void preloadRevisionContents(ContentRevision cr) throws VcsException {
        if (cr instanceof BinaryContentRevision) {
            ((BinaryContentRevision) cr).getBinaryContent();
        } else {
            cr.getContent();
        }
    }

    private List<CommittedChangeList> loadSvnChangeListsForPatch(SVNTreeConflictDescription description)
            throws VcsException {
        long max = description.getSourceRightVersion().getPegRevision();
        long min = description.getSourceLeftVersion().getPegRevision();

        final ChangeBrowserSettings settings = new ChangeBrowserSettings();
        settings.USE_CHANGE_BEFORE_FILTER = settings.USE_CHANGE_AFTER_FILTER = true;
        settings.CHANGE_BEFORE = "" + max;
        settings.CHANGE_AFTER = "" + min;
        final List<SvnChangeList> committedChanges = myVcs.getCachingCommittedChangesProvider().getCommittedChanges(
                settings,
                new SvnRepositoryLocation(description.getSourceRightVersion().getRepositoryRoot().toString()), 0);
        final List<CommittedChangeList> lst = new ArrayList<CommittedChangeList>(committedChanges.size() - 1);
        for (SvnChangeList change : committedChanges) {
            if (change.getNumber() == min) {
                continue;
            }
            lst.add(change);
        }
        return lst;
    }

    private void initAddOption() {
        ApplicationManager.getApplication().assertIsDispatchThread();
        if (myAdd == null) {
            myAdd = getAddedFilesPlaceOption();
        }
    }

    private boolean getAddedFilesPlaceOption() {
        final SvnConfiguration configuration = SvnConfiguration.getInstance(myVcs.getProject());
        boolean add = Boolean.TRUE.equals(configuration.isKeepNewFilesAsIsForTreeConflictMerge());
        if (configuration.isKeepNewFilesAsIsForTreeConflictMerge() != null) {
            return add;
        }
        if (!containAdditions(myTheirsChanges) && !containAdditions(myTheirsBinaryChanges)) {
            return false;
        }
        return Messages.YES == MessageDialogBuilder
                .yesNo(TreeConflictRefreshablePanel.TITLE, "Keep newly created file(s) in their original place?")
                .yesText("Keep").noText("Move").doNotAsk(new DialogWrapper.DoNotAskOption() {
                    @Override
                    public boolean isToBeShown() {
                        return true;
                    }

                    @Override
                    public void setToBeShown(boolean value, int exitCode) {
                        if (!value) {
                            if (exitCode == 0) {
                                // yes
                                configuration.setKeepNewFilesAsIsForTreeConflictMerge(true);
                            } else {
                                configuration.setKeepNewFilesAsIsForTreeConflictMerge(false);
                            }
                        }
                    }

                    @Override
                    public boolean canBeHidden() {
                        return true;
                    }

                    @Override
                    public boolean shouldSaveOptionsOnCancel() {
                        return true;
                    }

                    @Override
                    public String getDoNotShowMessage() {
                        return CommonBundle.message("dialog.options.do.not.ask");
                    }
                }).show();
    }

    private boolean containAdditions(final List<Change> changes) {
        boolean addFound = false;
        for (Change change : changes) {
            if (change.getBeforeRevision() == null || change.isMoved() || change.isRenamed()) {
                addFound = true;
                break;
            }
        }
        return addFound;
    }
}