mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[Mercurial] HgMergeProvider: handled several different merge situations. Added HgMergeProviderTestCase.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user