diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgWorkingCopyRevisionsCommand.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgWorkingCopyRevisionsCommand.java index a81b8ff89b30..0db93dffb08f 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgWorkingCopyRevisionsCommand.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgWorkingCopyRevisionsCommand.java @@ -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 null 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() { + @Override public int compare(HgRevisionNumber o1, HgRevisionNumber o2) { + return o2.compareTo(o1); + } + }); + } return revisions; } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/HgMergeProvider.java b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/HgMergeProvider.java index 12c37709171e..1d31179d512a 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/provider/HgMergeProvider.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/provider/HgMergeProvider.java @@ -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 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 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 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 index 000000000000..06bac7d6b687 --- /dev/null +++ b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMergeProviderTestCase.java @@ -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 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 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 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")); + } + +}