[Mercurial] HgMergeProvider: handled several different merge situations. Added HgMerg...
authorKirill Likhodedov <kirill.likhodedov@jetbrains.com>
Wed, 11 Aug 2010 13:17:02 +0000 (17:17 +0400)
committerKirill Likhodedov <kirill.likhodedov@jetbrains.com>
Wed, 11 Aug 2010 13:17:02 +0000 (17:17 +0400)
plugins/hg4idea/src/org/zmlx/hg4idea/command/HgWorkingCopyRevisionsCommand.java
plugins/hg4idea/src/org/zmlx/hg4idea/provider/HgMergeProvider.java
plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMergeProviderTestCase.java [new file with mode: 0644]

index a81b8ff89b302833d89fb9d65c0a258b2480d69c..0db93dffb08f5dded842fad4d8fc46fb13fcf291 100644 (file)
@@ -20,10 +20,7 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.zmlx.hg4idea.HgRevisionNumber;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.*;
 
 /**
  * Commands to get revision numbers. These are: parents, id, tip.
@@ -57,7 +54,8 @@ public class HgWorkingCopyRevisionsCommand {
   }
 
   /**
-   * Parent(s) of the given revision of the given file.
+   * Parent(s) of the given revision of the given file. If there are two of them (in the case of merge) the first element of the pair
+   * is the latest parent (i.e. having greater revision number), second one is the earlier parent (having smaller revision number).
    * @param repo     repository to work on.
    * @param file     file which revision's parents we are interested in. If null, the history of the whole repository is considered.
    * @param revision revision number which parent is wanted. If null, the last revision is taken. 
@@ -120,6 +118,8 @@ public class HgWorkingCopyRevisionsCommand {
   /**
    * Returns the list of revisions returned by one mercurial commands (parents, identify, tip).
    * Executed a command on the whole repository or on the given file.
+   * If the list contains more than one element ('hg parents' executed during merge), it is sorted by revision number, so that
+   * the latest revision number is the first element of the list.
    * @param repo     repository to execute on.
    * @param command  command to execute.
    * @param file     file which revisions are wanted. If <code><b>null</b></code> then repository revisions are considered.
@@ -149,7 +149,15 @@ public class HgWorkingCopyRevisionsCommand {
       final String[] parts = StringUtils.split(line, '|');
       revisions.add(HgRevisionNumber.getInstance(parts[0], parts[1]));
     }
-    
+
+    // sort by descending revision number
+    if (revisions.size() > 1) {
+      Collections.sort(revisions, new Comparator<HgRevisionNumber>() {
+        @Override public int compare(HgRevisionNumber o1, HgRevisionNumber o2) {
+          return o2.compareTo(o1);
+        }
+      });
+    }
     return revisions;
   }
 
index 12c37709171edd35ea28b5567a335797a5e8cc22..1d31179d512aa82d5c33584b680d0e0e4d749315 100644 (file)
@@ -33,6 +33,9 @@ import org.zmlx.hg4idea.HgUtil;
 import org.zmlx.hg4idea.command.HgResolveCommand;
 import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand;
 
+import java.io.File;
+import java.io.IOException;
+
 /**
  * @author Kirill Likhodedov
  */
@@ -50,24 +53,49 @@ public class HgMergeProvider implements MergeProvider {
     final MergeData mergeData = new MergeData();
     final VcsRunnable runnable = new VcsRunnable() {
       public void run() throws VcsException {
-        // we have a merge in progress, which means we have 2 heads (parents).
-        // the latest one is "their" revision pulled from the parent repo,
-        // the earlier parent is the local change.
-        // to retrieve the base version we get the parent of the local change, i.e. the [only] parent of the second parent.
         final HgWorkingCopyRevisionsCommand command = new HgWorkingCopyRevisionsCommand(myProject);
-        final Pair<HgRevisionNumber, HgRevisionNumber> parents = command.parents(HgUtil.getHgRootOrThrow(myProject, file), file);
-        // we are sure that we have a grandparent, because otherwise we'll get "repository is unrelated" error while pulling,
-        // due to different root changesets which is prohibited.
-        final HgRevisionNumber grandParent = command.parents(HgUtil.getHgRootOrThrow(myProject, file), file, parents.second).first;
-
+        final VirtualFile repo = HgUtil.getHgRootOrThrow(myProject, file);
         final HgFile hgFile = new HgFile(myProject, file);
-        final HgContentRevision server = new HgContentRevision(myProject, hgFile, parents.first);
-        final HgContentRevision local  = new HgContentRevision(myProject, hgFile, parents.second);
-        final HgContentRevision base = new HgContentRevision(myProject, hgFile, grandParent);
 
-        mergeData.ORIGINAL = base.getContentAsBytes();
-        mergeData.CURRENT = local.getContentAsBytes();
+        HgRevisionNumber serverRevisionNumber, baseRevisionNumber;
+        // there are two possibilities: we have checked in local changes in the selected file or we didn't.
+        if (wasFileCheckedIn(repo, file)) {
+          // 1. We checked in.
+          // We have a merge in progress, which means we have 2 heads (parents).
+          // the latest one is "their" revision pulled from the parent repo,
+          // the earlier parent is the local change.
+          // to retrieve the base version we get the parent of the local change, i.e. the [only] parent of the second parent.
+          final Pair<HgRevisionNumber, HgRevisionNumber> parents = command.parents(repo, file);
+          serverRevisionNumber = parents.first;
+          final HgContentRevision local = new HgContentRevision(myProject, hgFile, parents.second);
+          mergeData.CURRENT = local.getContentAsBytes();
+          // we are sure that we have a grandparent, because otherwise we'll get "repository is unrelated" error while pulling,
+          // due to different root changesets which is prohibited.
+          baseRevisionNumber = command.parents(repo, file, parents.second).first;
+        } else {
+          // 2. local changes are not checked in.
+          // then there is only one parent, which is server changes.
+          // local changes are retrieved from the file system, they are not in the Mercurial yet.
+          // base is the only parent of server changes.
+          serverRevisionNumber = command.parents(repo, file).first;
+          baseRevisionNumber = command.parents(repo, file, serverRevisionNumber).first;
+          final File origFile = new File(file.getPath() + ".orig");
+          try {
+            mergeData.CURRENT = VcsUtil.getFileByteContent(origFile);
+          } catch (IOException e) {
+            LOG.info("Couldn't retrieve byte content of the file: " + origFile.getPath(), e);
+          }
+        }
+
+        if (baseRevisionNumber != null) {
+          final HgContentRevision base = new HgContentRevision(myProject, hgFile, baseRevisionNumber);
+          mergeData.ORIGINAL = base.getContentAsBytes();
+        } else { // no base revision means that the file was added simultaneously with different content in both repositories
+          mergeData.ORIGINAL = new byte[0];
+        }
+        final HgContentRevision server = new HgContentRevision(myProject, hgFile, serverRevisionNumber);
         mergeData.LAST = server.getContentAsBytes();
+        file.refresh(false, false);
       }
     };
     VcsUtil.runVcsProcessWithProgress(runnable, VcsBundle.message("multiple.file.merge.loading.progress.title"), false, myProject);
@@ -88,4 +116,16 @@ public class HgMergeProvider implements MergeProvider {
     return file.getFileType().isBinary();
   }
 
+  /**
+   * Checks if the given file was checked in before the merge start.
+   * @param repo repository to work on.
+   * @param file file to be checked.
+   * @return True if the file was checked in before merge, false if it wasn't.
+   */
+  private boolean wasFileCheckedIn(VirtualFile repo, VirtualFile file) {
+    // in the case of merge if the file was checked in, it will have 2 parents after hg pull. If it wasn't, it would have only one parent
+    final Pair<HgRevisionNumber, HgRevisionNumber> parents = new HgWorkingCopyRevisionsCommand(myProject).parents(repo, file);
+    return parents.second != null;
+  }
+
 }
diff --git a/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMergeProviderTestCase.java b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMergeProviderTestCase.java
new file mode 100644 (file)
index 0000000..06bac7d
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2000-2010 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.zmlx.hg4idea.test;
+
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vcs.merge.MergeData;
+import com.intellij.openapi.vcs.merge.MergeProvider;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.zmlx.hg4idea.HgVcs;
+
+import java.io.IOException;
+
+import static org.testng.Assert.assertNotNull;
+
+/**
+ * Tests HgMergeProvider for different merge situations.
+ * All Mercurial operations are performed natively to test only HgMergeProvider functionality.
+ * @author Kirill Likhodedov
+ */
+public class HgMergeProviderTestCase extends HgCollaborativeTestCase {
+
+  private MergeProvider myMergeProvider;
+
+  @BeforeMethod
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    myMergeProvider = HgVcs.getInstance(myProject).getMergeProvider();
+    assertNotNull(myMergeProvider);
+  }
+
+  /**
+   * Start with a file in both repositories.
+   * 1. Edit the file in parent repository, commit the change.
+   * 2. Edit the file in child repository, commit the change.
+   * 3. Update.
+   * 4. Test the MergeData from the MergeProvider to have correct data.
+   */
+  @Test
+  public void mergeWithCommittedLocalChange() throws Exception {
+    final Pair<VirtualFile, VirtualFile> files = prepareFileInBothRepositories();
+    final VirtualFile parentFile = files.first;
+    final VirtualFile childFile = files.second;
+
+    HgTestUtil.printToFile(parentFile, "server");
+    myParentRepo.commit();
+    // committing conflicting change
+    HgTestUtil.printToFile(childFile, "local");
+    myRepo.commit();
+
+    myRepo.pullUpdateMerge();
+
+    verifyMergeData(childFile, "basic", "local", "server");
+  }
+
+  /**
+   * Start with a file in both repositories.
+   * 1. Edit the file in parent repository, commit the change.
+   * 2. Edit the file in child repository, don't commit the change.
+   * 3. Update.
+   * 4. Test the MergeData from the MergeProvider to have correct data.
+   */
+  @Test
+  public void mergeWithUncommittedLocalChange() throws Exception {
+    final Pair<VirtualFile, VirtualFile> files = prepareFileInBothRepositories();
+    final VirtualFile parentFile = files.first;
+    final VirtualFile childFile = files.second;
+
+    HgTestUtil.printToFile(parentFile, "server");
+    myParentRepo.commit();
+
+    // uncommitted conflicting change
+    HgTestUtil.printToFile(childFile, "local");
+
+    myRepo.pullUpdateMerge();
+    
+    verifyMergeData(childFile, "basic", "local", "server");
+  }
+
+  /**
+   * Start with a non fresh repository.
+   * 1. Add a file in parent repository, commit.
+   * 2. Add a file with the same name, but different content in child repository, commit.
+   * 3. Update.
+   * 4. Test the MergeData from the MergeProvider to have correct data (there is no basic version, but it shouldn't be null - just empty).
+   */
+  @Test
+  public void fileAddedAndCommitted() throws Exception {
+    // this is needed to have the same root changeset - otherwise conflicting root changeset will cause
+    // an error during 'hg pull': "abort: repository is unrelated"
+    prepareFileInBothRepositories();
+
+    myParentRepo.createFile("b.txt", "server");
+    myParentRepo.addCommit();
+
+    final VirtualFile childFile = myRepo.createFile("b.txt", "local");
+    myRepo.addCommit();
+
+    myRepo.pullUpdateMerge();
+
+    verifyMergeData(childFile, "", "local", "server");
+  }
+
+  /**
+   * Start with a non fresh repository.
+   * 1. Add a file in parent repository, commit.
+   * 2. Add a file with the same name, but different content in child repository, don't commit.
+   * 3. Update.
+   * 4. Test the MergeData from the MergeProvider to have correct data (there is no basic version, but it shouldn't be null - just empty).
+   */
+  @Test
+  public void fileAddedNotCommited() throws Exception {
+    // this is needed to have the same root changeset - otherwise conflicting root changeset will cause
+    // an error during 'hg pull': "abort: repository is unrelated"
+    prepareFileInBothRepositories();
+
+    myParentRepo.createFile("b.txt", "server");
+    myParentRepo.addCommit();
+
+    final VirtualFile childFile = myRepo.createFile("b.txt", "local");
+    myRepo.add();
+
+    myRepo.pullUpdateMerge();
+
+    verifyMergeData(childFile, "", "local", "server");
+  }
+
+  private void verifyMergeData(VirtualFile file, String expectedBase, String expectedLocal, String expectedServer) throws Exception {
+    final MergeData mergeData = myMergeProvider.loadRevisions(file);
+    assertEquals(mergeData.ORIGINAL, expectedBase);
+    assertEquals(mergeData.CURRENT, expectedLocal);
+    assertEquals(mergeData.LAST, expectedServer);
+  }
+
+  private static void assertEquals(byte[] bytes, String s) {
+    Assert.assertEquals(new String(bytes), s);
+  }
+
+  /**
+   * Creates a file with initial content in the parent repository, pulls & updates it to the child repository.
+   * @return References to the files in parent and child repositories respectively.
+   */
+  private Pair<VirtualFile, VirtualFile> prepareFileInBothRepositories() throws IOException {
+    final VirtualFile parentFile = myParentRepo.createFile("a.txt", "basic");
+    myParentRepo.add();
+    myParentRepo.commit();
+    myRepo.pull();
+    myRepo.update();
+    return Pair.create(parentFile, myRepo.getDirFixture().getFile("a.txt"));
+  }
+
+}