summaryrefslogtreecommitdiff
path: root/plugins/svn4idea/src/org/jetbrains/idea/svn/integrate/MergeCalculatorTask.java
blob: 782bd789e00ed5f72d813b6bee0009870f3fbbd4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
/*
 * 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.integrate;

import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.TransparentlyFailedValueI;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.util.Consumer;
import com.intellij.util.PairConsumer;
import com.intellij.util.continuation.ContinuationContext;
import com.intellij.util.continuation.TaskDescriptor;
import com.intellij.util.continuation.Where;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.actions.ChangeListsMergerFactory;
import org.jetbrains.idea.svn.dialogs.MergeContext;
import org.jetbrains.idea.svn.dialogs.QuickMergeContentsVariants;
import org.jetbrains.idea.svn.dialogs.SvnBranchPointsCalculator;
import org.jetbrains.idea.svn.history.SvnChangeList;
import org.jetbrains.idea.svn.history.SvnCommittedChangesProvider;
import org.jetbrains.idea.svn.history.SvnRepositoryLocation;
import org.jetbrains.idea.svn.history.TreeStructureNode;
import org.jetbrains.idea.svn.mergeinfo.MergeChecker;
import org.jetbrains.idea.svn.mergeinfo.OneShotMergeInfoHelper;
import org.jetbrains.idea.svn.mergeinfo.SvnMergeInfoCache;
import org.jetbrains.idea.svn.update.UpdateEventHandler;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;

import java.io.File;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Konstantin Kolosovsky.
 */
public class MergeCalculatorTask extends BaseMergeTask implements
                                                       Consumer<TransparentlyFailedValueI<SvnBranchPointsCalculator.WrapperInvertor, VcsException>> {
  private final static String ourOneShotStrategy = "svn.quickmerge.oneShotStrategy";
  private final
  AtomicReference<TransparentlyFailedValueI<SvnBranchPointsCalculator.WrapperInvertor, VcsException>>
    myCopyData;
  private boolean myIsReintegrate;

  private final List<CommittedChangeList> myNotMerged;
  private String myMergeTitle;
  private final MergeChecker myMergeChecker;

  @Override
  public void consume(TransparentlyFailedValueI<SvnBranchPointsCalculator.WrapperInvertor, VcsException> value) {
    myCopyData.set(value);
  }

  public MergeCalculatorTask(@NotNull MergeContext mergeContext, @NotNull QuickMergeInteraction interaction) throws VcsException {
    super(mergeContext, interaction, "Calculating not merged revisions", Where.POOLED);
    myNotMerged = new LinkedList<CommittedChangeList>();
    myMergeTitle = "Merge from " + myMergeContext.getBranchName();
    //      if (Boolean.TRUE.equals(Boolean.getBoolean(ourOneShotStrategy))) {
    myMergeChecker = new OneShotMergeInfoHelper(myMergeContext);
    ((OneShotMergeInfoHelper)myMergeChecker).prepare();
/*      } else {
      myMergeChecker = new BranchInfo.MyMergeCheckerWrapper(myWcInfo.getPath(), new BranchInfo(myVcs, myWcInfo.getRepositoryRoot(),
                                                                                               myWcInfo.getRootUrl(), mySourceUrl,
                                                                                               mySourceUrl, myVcs.createWCClient()));
    }*/
    myCopyData = new AtomicReference<TransparentlyFailedValueI<SvnBranchPointsCalculator.WrapperInvertor, VcsException>>();
  }

  //"Calculating not merged revisions"
  @Override
  public void run(ContinuationContext context) {
    SvnBranchPointsCalculator.WrapperInvertor copyDataValue = null;
    try {
      copyDataValue = myCopyData.get().get();
    }
    catch (VcsException e) {
      finishWithError(context, "Merge start wasn't found", Collections.singletonList(e));
      return;
    }
    if (copyDataValue == null) {
      finishWithError(context, "Merge start wasn't found", true);
      return;
    }

    final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
    myIsReintegrate = copyDataValue.isInvertedSense();
    if (!myMergeContext.getWcInfo().getFormat().supportsMergeInfo()) return;
    final SvnBranchPointsCalculator.BranchCopyData data = copyDataValue.getTrue();
    final long sourceLatest = data.getTargetRevision();

    final SvnCommittedChangesProvider committedChangesProvider =
      (SvnCommittedChangesProvider)myMergeContext.getVcs().getCommittedChangesProvider();
    final ChangeBrowserSettings settings = new ChangeBrowserSettings();
    settings.CHANGE_AFTER = Long.toString(sourceLatest);
    settings.USE_CHANGE_AFTER_FILTER = true;

    String local = SVNPathUtil.getRelativePath(myMergeContext.getWcInfo().getRepositoryRoot(), myMergeContext.getWcInfo().getRootUrl());
    final String relativeLocal = (local.startsWith("/") ? local : "/" + local);
    String relativeBranch = SVNPathUtil.getRelativePath(myMergeContext.getWcInfo().getRepositoryRoot(), myMergeContext.getSourceUrl());
    relativeBranch = (relativeBranch.startsWith("/") ? relativeBranch : "/" + relativeBranch);

    final LinkedList<Pair<SvnChangeList, TreeStructureNode<SVNLogEntry>>> list =
      new LinkedList<Pair<SvnChangeList, TreeStructureNode<SVNLogEntry>>>();
    try {
      committedChangesProvider.getCommittedChangesWithMergedRevisons(settings, new SvnRepositoryLocation(myMergeContext.getSourceUrl()), 0,
                                                                     new PairConsumer<SvnChangeList, TreeStructureNode<SVNLogEntry>>() {
                                                                       public void consume(SvnChangeList svnList,
                                                                                           TreeStructureNode<SVNLogEntry> tree) {
                                                                         indicator.checkCanceled();
                                                                         if (sourceLatest >= svnList.getNumber()) return;
                                                                         list.add(
                                                                           Pair.create(svnList,
                                                                                       tree)
                                                                         );
                                                                       }
                                                                     }
      );
    }
    catch (VcsException e) {
      finishWithError(context, "Checking revisions for merge fault", Collections.singletonList(e));
    }

    indicator.setText("Checking merge information...");
    // to do not go into file system while asking something on the net
    for (Pair<SvnChangeList, TreeStructureNode<SVNLogEntry>> pair : list) {
      final SvnChangeList svnList = pair.getFirst();
      final SvnMergeInfoCache.MergeCheckResult checkResult = myMergeChecker.checkList(svnList);
      indicator.setText2("Processing revision " + svnList.getNumber());

      if (SvnMergeInfoCache.MergeCheckResult.NOT_MERGED.equals(checkResult)) {
        // additionally check for being 'local'
        boolean localChange = checkListForPaths(relativeLocal, relativeBranch, pair);

        if (!localChange) {
          myNotMerged.add(svnList);
        }
      }
    }

    if (myNotMerged.isEmpty()) {
      finishWithError(context, "Everything is up-to-date", false);
      return;
    }
    context.next(new ShowRevisionSelector(copyDataValue));
  }

  private class ShowRevisionSelector extends TaskDescriptor {
    private final SvnBranchPointsCalculator.WrapperInvertor myCopyPoint;

    private ShowRevisionSelector(SvnBranchPointsCalculator.WrapperInvertor copyPoint) {
      super("show revisions to merge", Where.AWT);
      myCopyPoint = copyPoint;
    }

    @Override
    public void run(ContinuationContext context) {
      final QuickMergeInteraction.SelectMergeItemsResult result = myInteraction.selectMergeItems(myNotMerged, myMergeTitle, myMergeChecker);
      if (QuickMergeContentsVariants.cancel == result.getResultCode()) {
        context.cancelEverything();
        return;
      }
      if (QuickMergeContentsVariants.all == result.getResultCode()) {
        insertMergeAll(context);
      }
      else {
        final List<CommittedChangeList> lists = result.getSelectedLists();
        if (lists.isEmpty()) return;
        final MergerFactory factory = new ChangeListsMergerFactory(lists) {
          @Override
          public IMerger createMerger(SvnVcs vcs, File target, UpdateEventHandler handler, SVNURL currentBranchUrl, String branchName) {
            return new GroupMerger(vcs, lists, target, handler, currentBranchUrl, branchName, false, false, false);
          }
        };
        context.next(new LocalChangesPromptTask(myMergeContext, myInteraction, false, lists, myCopyPoint), new MergeTask(myMergeContext,
                                                                                                                         myInteraction,
                                                                                                                         factory,
                                                                                                                         myMergeTitle
        ));
      }
    }
  }

  // true if errors found
  static boolean checkListForPaths(String relativeLocal,
                                   String relativeBranch, Pair<SvnChangeList, TreeStructureNode<SVNLogEntry>> pair) {
    // TODO: Such filtering logic is not clear enough so far (and probably not correct for all cases - for instance when we perform merge
    // TODO: from branch1 to branch2 and have revision which contain merge changes from branch3 to branch1.
    // TODO: In this case paths of child log entries will not contain neither urls from branch1 nor from branch2 - and checkEntry() method
    // TODO: will return true => so such revision will not be used (and displayed) further.

    // TODO: Why do we check entries recursively - we have a revision - set of changes in the "merge from" branch? Why do we need to check
    // TODO: where they came from - we want avoid some circular merges or what? Does subversion itself perform such checks or not?
    final List<TreeStructureNode<SVNLogEntry>> children = pair.getSecond().getChildren();
    boolean localChange = false;
    for (TreeStructureNode<SVNLogEntry> child : children) {
      if (checkForSubtree(child, relativeLocal, relativeBranch)) {
        localChange = true;
        break;
      }
    }
    if (!localChange) {
      // check self
      return checkForEntry(pair.getSecond().getMe(), relativeLocal, relativeBranch);
    }
    return localChange;
  }

  // true if errors found
  private static boolean checkForSubtree(final TreeStructureNode<SVNLogEntry> tree,
                                         String relativeBranch, final String localURL) {
    final LinkedList<TreeStructureNode<SVNLogEntry>> queue = new LinkedList<TreeStructureNode<SVNLogEntry>>();
    queue.addLast(tree);

    while (!queue.isEmpty()) {
      final TreeStructureNode<SVNLogEntry> element = queue.removeFirst();
      ProgressManager.checkCanceled();

      if (checkForEntry(element.getMe(), localURL, relativeBranch)) return true;
      queue.addAll(element.getChildren());
    }
    return false;
  }

  // true if errors found
  // checks if either some changed path is in current branch => treat as local change
  // or if no changed paths in current branch, checks if at least one path in "merge from" branch
  // NOTE: this fails for "merge-source" log entries from other branches - when all changed paths are from some
  // third branch - this logic treats such log entry as local.
  private static boolean checkForEntry(final SVNLogEntry entry, final String localURL, String relativeBranch) {
    boolean atLeastOneUnderBranch = false;
    final Map map = entry.getChangedPaths();
    for (Object o : map.values()) {
      final SVNLogEntryPath path = (SVNLogEntryPath)o;
      if (SVNPathUtil.isAncestor(localURL, path.getPath())) {
        return true;
      }
      if (!atLeastOneUnderBranch && SVNPathUtil.isAncestor(relativeBranch, path.getPath())) {
        atLeastOneUnderBranch = true;
      }
    }
    return !atLeastOneUnderBranch;
  }
}