diff options
author | Laurent Delaigue | 2014-02-28 09:36:45 +0000 |
---|---|---|
committer | Laurent Delaigue | 2015-05-04 13:44:00 +0000 |
commit | 274817ca5ed97a4ce20255ceb8b0056809063f5c (patch) | |
tree | d7121d1e0a940a3e5b114b849e6d69055f31e8ff | |
parent | 767f77b7c86a9e03834957bf9848f9e9fabc5732 (diff) | |
download | egit-274817ca5ed97a4ce20255ceb8b0056809063f5c.tar.gz egit-274817ca5ed97a4ce20255ceb8b0056809063f5c.tar.xz egit-274817ca5ed97a4ce20255ceb8b0056809063f5c.zip |
Introduce internal API to integrate Team merging.
Team provides a number of APIs to provide access to merge,
comparison and synchronization features. This introduces a
git-specific Subscriber implementation that can retrieve
information from three different sources, along with
implementations to use the index or a TreeWalk to populate
these three 'sources'.
CQ: 9453
Bug: 418151
JGit-Dependency: I722352e619794988791c452545d57b17dada407a
Change-Id: Ieda23bb556d342f421f03b93c7faa160998598aa
Also-by: Laurent Goubet <laurent.goubet@obeo.fr>
Signed-off-by: Axel Richard <axel.richard@obeo.fr>
Signed-off-by: Laurent Delaigue <laurent.delaigue@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
19 files changed, 2233 insertions, 1 deletions
diff --git a/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProviderTest.java b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProviderTest.java new file mode 100644 index 0000000000..37929acd17 --- /dev/null +++ b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProviderTest.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (C) 2015 Obeo and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.egit.core.op.MergeOperation; +import org.junit.Test; + +public class DirCacheResourceVariantTreeProviderTest extends VariantsTestCase { + @Test + public void testDirCacheAddToIndex() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + testRepo.appendFileContent(file1, INITIAL_CONTENT_1); + + // untracked file : not part of the index + DirCacheResourceVariantTreeProvider treeProvider = new DirCacheResourceVariantTreeProvider( + repo); + assertTrue(treeProvider.getKnownResources().isEmpty()); + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getSourceTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getRemoteTree().hasResourceVariant(iFile1)); + + testRepo.addToIndex(iFile1); + + // We now have a stage 0, but this isn't represented in the resource + // variant tree provider + treeProvider = new DirCacheResourceVariantTreeProvider(repo); + assertTrue(treeProvider.getKnownResources().isEmpty()); + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getSourceTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getRemoteTree().hasResourceVariant(iFile1)); + } + + @Test + public void testDirCacheTreesNoConflict() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2, + "second file - initial commit"); + + IFile iFile1 = testRepo.getIFile(iProject, file1); + IFile iFile2 = testRepo.getIFile(iProject, file2); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + final String branchChanges = "branch changes\n"; + setContentsAndCommit(testRepo, iFile2, branchChanges + + INITIAL_CONTENT_2, "branch commit"); + + testRepo.checkoutBranch(MASTER); + + final String masterChanges = "\nsome changes"; + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + masterChanges, "master commit"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + // end setup + + // try and merge the branch into master + new MergeOperation(repo, BRANCH).execute(null); + + // no conflict on either file : nothing in the trees + DirCacheResourceVariantTreeProvider treeProvider = new DirCacheResourceVariantTreeProvider( + repo); + assertTrue(treeProvider.getKnownResources().isEmpty()); + + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile2)); + + assertFalse(treeProvider.getSourceTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getSourceTree().hasResourceVariant(iFile2)); + + assertFalse(treeProvider.getRemoteTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getRemoteTree().hasResourceVariant(iFile2)); + } + + @Test + public void testDirCacheTreesConflictOnOne() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2, + "second file - initial commit"); + + IFile iFile1 = testRepo.getIFile(iProject, file1); + IFile iFile2 = testRepo.getIFile(iProject, file2); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + final String branchChanges = "branch changes\n"; + setContentsAndCommit(testRepo, iFile1, branchChanges + + INITIAL_CONTENT_1, "branch commit"); + setContentsAndCommit(testRepo, iFile2, branchChanges + + INITIAL_CONTENT_2, "branch commit"); + + testRepo.checkoutBranch(MASTER); + + final String masterChanges = "\nsome changes"; + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + masterChanges, "master commit"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + // end setup + + // try and merge the branch into master + new MergeOperation(repo, BRANCH).execute(null); + + // conflict on file 1 : present in all three trees + // no conflict on file 2 : not present in any tree + DirCacheResourceVariantTreeProvider treeProvider = new DirCacheResourceVariantTreeProvider( + repo); + assertTrue(treeProvider.getKnownResources().contains(iFile1)); + assertFalse(treeProvider.getKnownResources().contains(iFile2)); + + assertTrue(treeProvider.getBaseTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile2)); + + assertTrue(treeProvider.getSourceTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getSourceTree().hasResourceVariant(iFile2)); + + assertTrue(treeProvider.getRemoteTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getRemoteTree().hasResourceVariant(iFile2)); + } + + @Test + public void testDirCacheTreesConflict() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + + IFile iFile1 = testRepo.getIFile(iProject, file1); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + final String branchChanges = "branch changes\n"; + setContentsAndCommit(testRepo, iFile1, branchChanges + + INITIAL_CONTENT_1, "branch commit"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2 + + "branch", "second file - initial commit - branch"); + + testRepo.checkoutBranch(MASTER); + + final String masterChanges = "some changes\n"; + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + masterChanges, "master commit - file1"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2 + + "master", "second file - initial commit - master"); + IFile iFile2 = testRepo.getIFile(iProject, file2); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + // end setup + + // try and merge the branch into master + new MergeOperation(repo, BRANCH).execute(null); + + // conflict on file 1 : file 1 has three stages. + // conflict on file 2, but was not in the base : only stage 2 and 3 + DirCacheResourceVariantTreeProvider treeProvider = new DirCacheResourceVariantTreeProvider( + repo); + assertTrue(treeProvider.getKnownResources().contains(iFile1)); + assertTrue(treeProvider.getKnownResources().contains(iFile2)); + + assertTrue(treeProvider.getBaseTree().hasResourceVariant(iFile1)); + assertFalse(treeProvider.getBaseTree().hasResourceVariant(iFile2)); + + assertTrue(treeProvider.getSourceTree().hasResourceVariant(iFile1)); + assertTrue(treeProvider.getSourceTree().hasResourceVariant(iFile2)); + + assertTrue(treeProvider.getRemoteTree().hasResourceVariant(iFile1)); + assertTrue(treeProvider.getRemoteTree().hasResourceVariant(iFile2)); + } +} diff --git a/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriberTest.java b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriberTest.java new file mode 100644 index 0000000000..11046f2985 --- /dev/null +++ b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriberTest.java @@ -0,0 +1,357 @@ +/******************************************************************************* + * Copyright (C) 2015 Obeo and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.team.core.diff.IDiff; +import org.eclipse.team.core.diff.IThreeWayDiff; +import org.eclipse.team.core.history.IFileRevision; +import org.eclipse.team.core.mapping.provider.ResourceDiff; +import org.eclipse.team.core.synchronize.SyncInfo; +import org.eclipse.team.core.variants.IResourceVariant; +import org.junit.Before; +import org.junit.Test; + +public class GitResourceVariantTreeSubscriberTest extends VariantsTestCase { + private static final String BRANCH_CHANGES = "branch changes\n"; + + private static final String MASTER_CHANGES = "\nsome changes"; + + private static final String BASE = "base"; + + private File file1; + + private File file2; + + private IFile iFile1; + + private IFile iFile2; + + @Before + public void setUp() throws Exception { + super.setUp(); + + file1 = testRepo.createFile(iProject, "file1"); + file2 = testRepo.createFile(iProject, "file2"); + + iFile1 = testRepo.getIFile(iProject, file1); + iFile2 = testRepo.getIFile(iProject, file2); + } + + @Test + public void testSubscriber() throws Exception { + GitResourceVariantTreeProvider provider = createTreeProvider(); + GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( + provider); + + assertTrue(subscriber.isSupervised(iProject)); + assertTrue(subscriber.isSupervised(iFile1)); + assertTrue(subscriber.isSupervised(iFile2)); + + assertSame(provider.getBaseTree(), subscriber.getBaseTree()); + assertSame(provider.getRemoteTree(), subscriber.getRemoteTree()); + assertSame(provider.getSourceTree(), subscriber.getSourceTree()); + + assertNotNull(subscriber.getDiff(iProject)); + assertNotNull(subscriber.getDiff(iFile1)); + assertNotNull(subscriber.getDiff(iFile2)); + + assertNotNull(subscriber.getSyncInfo(iProject)); + assertNotNull(subscriber.getSyncInfo(iFile1)); + assertNotNull(subscriber.getSyncInfo(iFile2)); + + } + + @Test + public void testSyncInfo() throws Exception { + GitResourceVariantTreeProvider provider = createTreeProvider(); + GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( + provider); + + final SyncInfo projectInfo = subscriber.getSyncInfo(iProject); + assertNotNull(projectInfo); + assertEquals(SyncInfo.CONFLICTING | SyncInfo.CHANGE, + projectInfo.getKind()); + + final SyncInfo syncInfo1 = subscriber.getSyncInfo(iFile1); + assertNotNull(syncInfo1); + assertEquals(SyncInfo.OUTGOING | SyncInfo.CHANGE, syncInfo1.getKind()); + IResourceVariant baseVariant1 = syncInfo1.getBase(); + IResourceVariant remoteVariant1 = syncInfo1.getRemote(); + assertContentEquals(baseVariant1, INITIAL_CONTENT_1); + assertContentEquals(remoteVariant1, INITIAL_CONTENT_1); + + final SyncInfo syncInfo2 = subscriber.getSyncInfo(iFile2); + assertNotNull(syncInfo2); + assertEquals(SyncInfo.INCOMING | SyncInfo.CHANGE, syncInfo2.getKind()); + IResourceVariant baseVariant2 = syncInfo2.getBase(); + IResourceVariant remoteVariant2 = syncInfo2.getRemote(); + assertContentEquals(baseVariant2, INITIAL_CONTENT_2); + assertContentEquals(remoteVariant2, BRANCH_CHANGES + INITIAL_CONTENT_2); + } + + @Test + public void testDiff() throws Exception { + GitResourceVariantTreeProvider provider = createTreeProvider(); + GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( + provider); + + final IDiff diff1 = subscriber.getDiff(iFile1); + assertTrue(diff1 instanceof IThreeWayDiff); + assertEquals(IDiff.CHANGE, diff1.getKind()); + assertEquals(IThreeWayDiff.OUTGOING, + ((IThreeWayDiff) diff1).getDirection()); + final IDiff localDiff1 = ((IThreeWayDiff) diff1).getLocalChange(); + final IDiff remoteDiff1 = ((IThreeWayDiff) diff1).getRemoteChange(); + assertNull(remoteDiff1); + assertTrue(localDiff1 instanceof ResourceDiff); + final IFileRevision localState1 = ((ResourceDiff) localDiff1) + .getAfterState(); + final IFileRevision baseState1 = ((ResourceDiff) localDiff1) + .getBeforeState(); + assertNotNull(localState1); + assertNotNull(baseState1); + assertTrue(iFile1.getName().equals(localState1.getName())); + assertTrue(iFile1.getName().equals(baseState1.getName())); + final IStorage localStorage1 = localState1 + .getStorage(new NullProgressMonitor()); + final IStorage baseStorage1 = baseState1 + .getStorage(new NullProgressMonitor()); + assertContentEquals(localStorage1, INITIAL_CONTENT_1 + MASTER_CHANGES); + assertContentEquals(baseStorage1, INITIAL_CONTENT_1); + + final IDiff diff2 = subscriber.getDiff(iFile2); + assertTrue(diff2 instanceof IThreeWayDiff); + assertEquals(IDiff.CHANGE, diff2.getKind()); + assertEquals(IThreeWayDiff.INCOMING, + ((IThreeWayDiff) diff2).getDirection()); + final IDiff localDiff2 = ((IThreeWayDiff) diff2).getLocalChange(); + final IDiff remoteDiff2 = ((IThreeWayDiff) diff2).getRemoteChange(); + assertTrue(remoteDiff2 instanceof ResourceDiff); + assertNull(localDiff2); + final IFileRevision remoteState2 = ((ResourceDiff) remoteDiff2) + .getAfterState(); + final IFileRevision ancestorState2 = ((ResourceDiff) remoteDiff2) + .getBeforeState(); + assertTrue(iFile2.getName().equals(ancestorState2.getName())); + assertTrue(iFile2.getName().equals(remoteState2.getName())); + final IStorage ancestorStorage2 = ancestorState2 + .getStorage(new NullProgressMonitor()); + final IStorage remoteStorage2 = remoteState2 + .getStorage(new NullProgressMonitor()); + assertContentEquals(ancestorStorage2, INITIAL_CONTENT_2); + assertContentEquals(remoteStorage2, BRANCH_CHANGES + INITIAL_CONTENT_2); + } + + @Test + public void testAddLocalAndRemote() throws Exception { + GitResourceVariantTreeProvider provider = createTreeProviderWithAdditions(); + GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( + provider); + + final IDiff diff1 = subscriber.getDiff(iFile1); + assertTrue(diff1 instanceof IThreeWayDiff); + assertEquals(IDiff.ADD, diff1.getKind()); + assertEquals(IThreeWayDiff.OUTGOING, + ((IThreeWayDiff) diff1).getDirection()); + final IDiff localDiff1 = ((IThreeWayDiff) diff1).getLocalChange(); + final IDiff remoteDiff1 = ((IThreeWayDiff) diff1).getRemoteChange(); + assertTrue(localDiff1 instanceof ResourceDiff); + assertNull(remoteDiff1); + final IFileRevision ancestorState1 = ((ResourceDiff) localDiff1) + .getBeforeState(); + final IFileRevision localState1 = ((ResourceDiff) localDiff1) + .getAfterState(); + assertTrue(iFile1.getName().equals(localState1.getName())); + assertNull(ancestorState1); + final IStorage localStorage1 = localState1 + .getStorage(new NullProgressMonitor()); + assertContentEquals(localStorage1, INITIAL_CONTENT_1); + + final IDiff diff2 = subscriber.getDiff(iFile2); + assertTrue(diff2 instanceof IThreeWayDiff); + assertEquals(IDiff.ADD, diff2.getKind()); + assertEquals(IThreeWayDiff.INCOMING, + ((IThreeWayDiff) diff2).getDirection()); + final IDiff localDiff2 = ((IThreeWayDiff) diff2).getLocalChange(); + final IDiff remoteDiff2 = ((IThreeWayDiff) diff2).getRemoteChange(); + assertTrue(remoteDiff2 instanceof ResourceDiff); + assertNull(localDiff2); + final IFileRevision ancestorState2 = ((ResourceDiff) remoteDiff2) + .getBeforeState(); + final IFileRevision remoteState2 = ((ResourceDiff) remoteDiff2) + .getAfterState(); + assertNull(ancestorState2); + assertTrue(iFile2.getName().equals(remoteState2.getName())); + final IStorage remoteStorage2 = remoteState2 + .getStorage(new NullProgressMonitor()); + assertContentEquals(remoteStorage2, INITIAL_CONTENT_2); + } + + @Test + public void testRemoveLocalAndRemote() throws Exception { + GitResourceVariantTreeProvider provider = createTreeProviderWithDeletions(); + GitResourceVariantTreeSubscriber subscriber = new GitResourceVariantTreeSubscriber( + provider); + + // file1 has been removed locally + final IDiff diff1 = subscriber.getDiff(iFile1); + assertTrue(diff1 instanceof IThreeWayDiff); + assertEquals(IDiff.REMOVE, diff1.getKind()); + assertEquals(IThreeWayDiff.OUTGOING, + ((IThreeWayDiff) diff1).getDirection()); + final IDiff localDiff1 = ((IThreeWayDiff) diff1).getLocalChange(); + final IDiff remoteDiff1 = ((IThreeWayDiff) diff1).getRemoteChange(); + assertTrue(localDiff1 instanceof ResourceDiff); + assertNull(remoteDiff1); + final IFileRevision ancestorState1 = ((ResourceDiff) localDiff1) + .getBeforeState(); + final IFileRevision localState1 = ((ResourceDiff) localDiff1) + .getAfterState(); + assertTrue(iFile1.getName().equals(ancestorState1.getName())); + assertNull(localState1); + final IStorage ancestorStorage1 = ancestorState1 + .getStorage(new NullProgressMonitor()); + assertContentEquals(ancestorStorage1, INITIAL_CONTENT_1); + + // file2 has been removed remotely + final IDiff diff2 = subscriber.getDiff(iFile2); + assertTrue(diff2 instanceof IThreeWayDiff); + assertEquals(IDiff.REMOVE, diff2.getKind()); + assertEquals(IThreeWayDiff.INCOMING, + ((IThreeWayDiff) diff2).getDirection()); + final IDiff localDiff2 = ((IThreeWayDiff) diff2).getLocalChange(); + final IDiff remoteDiff2 = ((IThreeWayDiff) diff2).getRemoteChange(); + assertTrue(remoteDiff2 instanceof ResourceDiff); + assertNull(localDiff2); + final IFileRevision ancestorState2 = ((ResourceDiff) remoteDiff2) + .getBeforeState(); + final IFileRevision remoteState2 = ((ResourceDiff) remoteDiff2) + .getAfterState(); + assertTrue(iFile2.getName().equals(ancestorState2.getName())); + assertNull(remoteState2); + final IStorage rancestorStorage2 = ancestorState2 + .getStorage(new NullProgressMonitor()); + assertContentEquals(rancestorStorage2, INITIAL_CONTENT_2); + } + + private GitResourceVariantTreeProvider createTreeProvider() + throws Exception { + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + testRepo.appendContentAndCommit(iProject, file2, + INITIAL_CONTENT_2, "second file - initial commit"); + testRepo.createBranch(MASTER, BASE); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + setContentsAndCommit(testRepo, iFile2, BRANCH_CHANGES + + INITIAL_CONTENT_2, "branch commit"); + + testRepo.checkoutBranch(MASTER); + + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + MASTER_CHANGES, "master commit"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + + // as if we tried to merge branch into master + try (RevWalk walk = new RevWalk(repo)) { + RevTree baseTree = walk.parseTree(repo.resolve(BASE)); + RevTree sourceTree = walk.parseTree(repo.resolve(MASTER)); + RevTree remoteTree = walk.parseTree(repo.resolve(BRANCH)); + TreeWalk treeWalk = new NameConflictTreeWalk(repo); + treeWalk.addTree(baseTree); + treeWalk.addTree(sourceTree); + treeWalk.addTree(remoteTree); + return new TreeWalkResourceVariantTreeProvider(repo, treeWalk, 0, + 1, 2); + } + } + + private GitResourceVariantTreeProvider createTreeProviderWithAdditions() + throws Exception { + testRepo.createBranch(MASTER, BASE); + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + file2 = testRepo.createFile(iProject, "file2"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2, + "Creation of file2 in branch2."); + + testRepo.checkoutBranch(MASTER); + file1 = testRepo.createFile(iProject, "file1"); + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "Creation of file1 in branch1."); + + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + + // as if we tried to merge branch3 into branch2 + try (RevWalk walk = new RevWalk(repo)) { + RevTree baseTree = walk.parseTree(repo.resolve(BASE)); + RevTree sourceTree = walk.parseTree(repo.resolve(MASTER)); + RevTree remoteTree = walk.parseTree(repo.resolve(BRANCH)); + TreeWalk treeWalk = new NameConflictTreeWalk(repo); + treeWalk.addTree(baseTree); + treeWalk.addTree(sourceTree); + treeWalk.addTree(remoteTree); + return new TreeWalkResourceVariantTreeProvider(repo, treeWalk, 0, + 1, 2); + } + } + + private GitResourceVariantTreeProvider createTreeProviderWithDeletions() + throws Exception { + file1 = testRepo.createFile(iProject, "file1"); + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "Creation of file1 in branch1."); + file2 = testRepo.createFile(iProject, "file2"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2, + "Creation of file2 in branch2."); + testRepo.createBranch(MASTER, BASE); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + testRepo.untrack(file2); + testRepo.commit("Removed file2 in branch."); + + testRepo.checkoutBranch(MASTER); + testRepo.untrack(file1); + testRepo.commit("Removed file1 in master."); + + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + + // as if we tried to merge branch3 into branch2 + try (RevWalk walk = new RevWalk(repo)) { + RevTree baseTree = walk.parseTree(repo.resolve(BASE)); + RevTree sourceTree = walk.parseTree(repo.resolve(MASTER)); + RevTree remoteTree = walk.parseTree(repo.resolve(BRANCH)); + TreeWalk treeWalk = new NameConflictTreeWalk(repo); + treeWalk.addTree(baseTree); + treeWalk.addTree(sourceTree); + treeWalk.addTree(remoteTree); + return new TreeWalkResourceVariantTreeProvider(repo, treeWalk, 0, + 1, 2); + } + } +} diff --git a/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/ResourceVariantTest.java b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/ResourceVariantTest.java new file mode 100644 index 0000000000..ede07fa3d3 --- /dev/null +++ b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/ResourceVariantTest.java @@ -0,0 +1,301 @@ +/******************************************************************************* + * Copyright (C) 2015 Obeo and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.egit.core.internal.storage.AbstractGitResourceVariant; +import org.eclipse.egit.core.internal.storage.IndexResourceVariant; +import org.eclipse.egit.core.internal.storage.TreeParserResourceVariant; +import org.eclipse.egit.core.op.MergeOperation; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.junit.Test; + +public class ResourceVariantTest extends VariantsTestCase { + private final static String BASE_BRANCH = "base"; + + private final static String BRANCH_CHANGE = "branch changes\n"; + + private final static String MASTER_CHANGE = "master changes\n"; + + @Test + public void testIndexVariants() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + IFile iFile2 = testRepo.getIFile(iProject, file2); + + setupUnconflictingBranches(); + + List<String> possibleNames = Arrays.asList(iFile1.getName(), + iFile2.getName()); + DirCache cache = repo.readDirCache(); + for (int i = 0; i < cache.getEntryCount(); i++) { + final DirCacheEntry entry = cache.getEntry(i); + + AbstractGitResourceVariant variant = IndexResourceVariant.create( + repo, entry); + + assertEquals(entry.getObjectId().getName(), + variant.getContentIdentifier()); + assertTrue(possibleNames.contains(variant.getName())); + assertEquals(entry.getObjectId(), variant.getObjectId()); + assertEquals(entry.getRawMode(), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + assertContentEquals(variant, INITIAL_CONTENT_1 + MASTER_CHANGE); + } else { + assertContentEquals(variant, INITIAL_CONTENT_2 + MASTER_CHANGE); + } + } + + testRepo.checkoutBranch(BRANCH); + + cache = repo.readDirCache(); + for (int i = 0; i < cache.getEntryCount(); i++) { + final DirCacheEntry entry = cache.getEntry(i); + + AbstractGitResourceVariant variant = IndexResourceVariant.create( + repo, entry); + assertEquals(entry.getObjectId().getName(), + variant.getContentIdentifier()); + assertTrue(possibleNames.contains(variant.getName())); + assertEquals(entry.getObjectId(), variant.getObjectId()); + assertEquals(entry.getRawMode(), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + assertContentEquals(variant, BRANCH_CHANGE + INITIAL_CONTENT_1); + } else { + assertContentEquals(variant, BRANCH_CHANGE + INITIAL_CONTENT_2); + } + } + } + + @Test + public void testIndexVariantsConflict() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + setupConflictingBranches(); + // end setup + + // create a conflict to force multiple stages + new MergeOperation(repo, BRANCH).execute(null); + + DirCache cache = repo.readDirCache(); + // 3 stages for file 1, 2 stages for file 2 + assertEquals(5, cache.getEntryCount()); + for (int i = 0; i < cache.getEntryCount(); i++) { + final DirCacheEntry entry = cache.getEntry(i); + + AbstractGitResourceVariant variant = IndexResourceVariant.create( + repo, entry); + assertEquals(entry.getObjectId().getName(), + variant.getContentIdentifier()); + assertEquals(entry.getObjectId(), variant.getObjectId()); + assertEquals(entry.getRawMode(), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + switch (entry.getStage()) { + case DirCacheEntry.STAGE_1: + assertContentEquals(variant, INITIAL_CONTENT_1); + break; + case DirCacheEntry.STAGE_2: + assertContentEquals(variant, INITIAL_CONTENT_1 + + MASTER_CHANGE); + break; + case DirCacheEntry.STAGE_3: + assertContentEquals(variant, BRANCH_CHANGE + + INITIAL_CONTENT_1); + break; + case DirCacheEntry.STAGE_0: + default: + fail("Unexpected entry stage " + entry.getStage() + + " in the index for file " + entry.getPathString()); + break; + } + } else { + switch (entry.getStage()) { + case DirCacheEntry.STAGE_2: + assertContentEquals(variant, INITIAL_CONTENT_2 + + MASTER_CHANGE); + break; + case DirCacheEntry.STAGE_3: + assertContentEquals(variant, BRANCH_CHANGE + + INITIAL_CONTENT_2); + break; + case DirCacheEntry.STAGE_0: + case DirCacheEntry.STAGE_1: + default: + fail("Unexpected entry stage " + entry.getStage() + + " in the index for file " + entry.getPathString()); + break; + } + } + } + } + + @Test + public void testTreeWalkBranchVariants() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + setupUnconflictingBranches(); + + ObjectId baseId = repo.resolve(BRANCH); + RevWalk walk = new RevWalk(repo); + TreeWalk tw = new TreeWalk(repo); + tw.addTree(walk.parseTree(baseId)); + + while (tw.next()) { + AbstractGitResourceVariant variant = TreeParserResourceVariant + .create(repo, tw.getTree(0, CanonicalTreeParser.class)); + + assertEquals(tw.getObjectId(0).getName(), + variant.getContentIdentifier()); + assertEquals(tw.getObjectId(0), variant.getObjectId()); + assertEquals(tw.getRawMode(0), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + assertContentEquals(variant, BRANCH_CHANGE + INITIAL_CONTENT_1); + } else if (!tw.isSubtree()) { + assertContentEquals(variant, BRANCH_CHANGE + INITIAL_CONTENT_2); + } + + if (tw.isSubtree()) { + tw.enterSubtree(); + } + } + } + + @Test + public void testTreeWalkMasterVariants() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + setupUnconflictingBranches(); + + ObjectId baseId = repo.resolve(MASTER); + RevWalk walk = new RevWalk(repo); + TreeWalk tw = new TreeWalk(repo); + tw.addTree(walk.parseTree(baseId)); + + while (tw.next()) { + AbstractGitResourceVariant variant = TreeParserResourceVariant + .create(repo, tw.getTree(0, CanonicalTreeParser.class)); + + assertEquals(tw.getObjectId(0).getName(), + variant.getContentIdentifier()); + assertEquals(tw.getObjectId(0), variant.getObjectId()); + assertEquals(tw.getRawMode(0), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + assertContentEquals(variant, INITIAL_CONTENT_1 + MASTER_CHANGE); + } else if (!tw.isSubtree()) { + assertContentEquals(variant, INITIAL_CONTENT_2 + MASTER_CHANGE); + } + + if (tw.isSubtree()) { + tw.enterSubtree(); + } + } + } + + @Test + public void testTreeWalkBaseVariants() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + setupUnconflictingBranches(); + + ObjectId baseId = repo.resolve(BASE_BRANCH); + RevWalk walk = new RevWalk(repo); + TreeWalk tw = new TreeWalk(repo); + tw.addTree(walk.parseTree(baseId)); + + while (tw.next()) { + AbstractGitResourceVariant variant = TreeParserResourceVariant + .create(repo, tw.getTree(0, CanonicalTreeParser.class)); + + assertEquals(tw.getObjectId(0).getName(), + variant.getContentIdentifier()); + assertEquals(tw.getObjectId(0), variant.getObjectId()); + assertEquals(tw.getRawMode(0), variant.getRawMode()); + if (iFile1.getName().equals(variant.getName())) { + assertContentEquals(variant, INITIAL_CONTENT_1); + } else if (!tw.isSubtree()) { + fail("file2 shouldn't exist in our base."); + } + + if (tw.isSubtree()) { + tw.enterSubtree(); + } + } + } + + private void setupUnconflictingBranches() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + + testRepo.createBranch(MASTER, BASE_BRANCH); + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + setContentsAndCommit(testRepo, iFile1, BRANCH_CHANGE + + INITIAL_CONTENT_1, "branch commit"); + testRepo.appendContentAndCommit(iProject, file2, BRANCH_CHANGE + + INITIAL_CONTENT_2, "second file - initial commit - branch"); + + testRepo.checkoutBranch(MASTER); + + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + MASTER_CHANGE, "master commit - file1"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2 + + MASTER_CHANGE, "second file - initial commit - master"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + } + + private void setupConflictingBranches() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + IFile iFile1 = testRepo.getIFile(iProject, file1); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + setContentsAndCommit(testRepo, iFile1, BRANCH_CHANGE + + INITIAL_CONTENT_1, "branch commit"); + testRepo.appendContentAndCommit(iProject, file2, BRANCH_CHANGE + + INITIAL_CONTENT_2, "second file - initial commit - branch"); + + testRepo.checkoutBranch(MASTER); + + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + MASTER_CHANGE, "master commit - file1"); + testRepo.appendContentAndCommit(iProject, file2, INITIAL_CONTENT_2 + + MASTER_CHANGE, "second file - initial commit - master"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + } +} diff --git a/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProviderTest.java b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProviderTest.java new file mode 100644 index 0000000000..d1508c728a --- /dev/null +++ b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProviderTest.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (C) 2015 Obeo and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.team.core.variants.IResourceVariant; +import org.junit.Test; + +public class TreeWalkResourceVariantTreeProviderTest extends VariantsTestCase { + @Test + public void testTreeWalkTrees() throws Exception { + File file1 = testRepo.createFile(iProject, "file1"); + File file2 = testRepo.createFile(iProject, "file2"); + + testRepo.appendContentAndCommit(iProject, file1, INITIAL_CONTENT_1, + "first file - initial commit"); + RevCommit baseCommit = testRepo.appendContentAndCommit(iProject, file2, + INITIAL_CONTENT_2, "second file - initial commit"); + + IFile iFile1 = testRepo.getIFile(iProject, file1); + IFile iFile2 = testRepo.getIFile(iProject, file2); + + testRepo.createAndCheckoutBranch(MASTER, BRANCH); + + final String branchChanges = "branch changes\n"; + setContentsAndCommit(testRepo, iFile2, branchChanges + + INITIAL_CONTENT_2, "branch commit"); + + testRepo.checkoutBranch(MASTER); + + final String masterChanges = "\nsome changes"; + setContentsAndCommit(testRepo, iFile1, INITIAL_CONTENT_1 + + masterChanges, "master commit"); + iProject.refreshLocal(IResource.DEPTH_INFINITE, + new NullProgressMonitor()); + // end setup + + // as if we tried to merge branch into master + try (RevWalk walk = new RevWalk(repo)) { + RevTree baseTree = walk.parseTree(baseCommit.getId()); + RevTree sourceTree = walk.parseTree(repo.resolve(MASTER)); + RevTree remoteTree = walk.parseTree(repo.resolve(BRANCH)); + TreeWalk treeWalk = new NameConflictTreeWalk(repo); + treeWalk.addTree(baseTree); + treeWalk.addTree(sourceTree); + treeWalk.addTree(remoteTree); + TreeWalkResourceVariantTreeProvider treeProvider = new TreeWalkResourceVariantTreeProvider( + repo, treeWalk, 0, 1, 2); + + assertEquals(1, treeProvider.getRoots().size()); + assertTrue(treeProvider.getRoots().contains(iProject)); + + assertTrue(treeProvider.getKnownResources().contains(iFile1)); + assertTrue(treeProvider.getKnownResources().contains(iFile2)); + + IResourceVariant file1BaseVariant = treeProvider.getBaseTree() + .getResourceVariant(iFile1); + IResourceVariant file2BaseVariant = treeProvider.getBaseTree() + .getResourceVariant(iFile2); + assertContentEquals(file1BaseVariant, INITIAL_CONTENT_1); + assertContentEquals(file2BaseVariant, INITIAL_CONTENT_2); + + IResourceVariant file1TheirsVariant = treeProvider.getRemoteTree() + .getResourceVariant(iFile1); + IResourceVariant file2TheirsVariant = treeProvider.getRemoteTree() + .getResourceVariant(iFile2); + assertContentEquals(file1TheirsVariant, INITIAL_CONTENT_1); + assertContentEquals(file2TheirsVariant, branchChanges + + INITIAL_CONTENT_2); + + IResourceVariant file1OursVariant = treeProvider.getSourceTree() + .getResourceVariant(iFile1); + IResourceVariant file2OursVariant = treeProvider.getSourceTree() + .getResourceVariant(iFile2); + assertContentEquals(file1OursVariant, INITIAL_CONTENT_1 + + masterChanges); + assertContentEquals(file2OursVariant, INITIAL_CONTENT_2); + } + } +} diff --git a/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/VariantsTestCase.java b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/VariantsTestCase.java new file mode 100644 index 0000000000..2e2a9e7cb8 --- /dev/null +++ b/org.eclipse.egit.core.test/src/org/eclipse/egit/core/internal/merge/VariantsTestCase.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (C) 2015 Obeo and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.util.Scanner; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.egit.core.project.RepositoryMapping; +import org.eclipse.egit.core.test.GitTestCase; +import org.eclipse.egit.core.test.TestRepository; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.team.core.variants.IResourceVariant; +import org.junit.After; +import org.junit.Before; + +public abstract class VariantsTestCase extends GitTestCase { + protected final String INITIAL_CONTENT_1 = "some content for the first file"; + + protected final String INITIAL_CONTENT_2 = "some content for the second file"; + + protected static final String MASTER = Constants.R_HEADS + Constants.MASTER; + + protected static final String BRANCH = Constants.R_HEADS + "branch"; + + protected Repository repo; + + protected IProject iProject; + + protected TestRepository testRepo; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + iProject = project.project; + testRepo = new TestRepository(gitDir); + testRepo.connect(iProject); + repo = RepositoryMapping.getMapping(iProject).getRepository(); + + // make initial commit + try (Git git = new Git(repo)) { + git.commit().setAuthor("JUnit", "junit@jgit.org") + .setMessage("Initial commit").call(); + } + } + + @After + @Override + public void tearDown() throws Exception { + testRepo.disconnect(iProject); + testRepo.dispose(); + repo = null; + + super.tearDown(); + } + + protected RevCommit setContentsAndCommit(TestRepository testRepository, + IFile targetFile, String newContents, String commitMessage) + throws Exception { + targetFile.setContents( + new ByteArrayInputStream(newContents.getBytes()), + IResource.FORCE, new NullProgressMonitor()); + testRepository.addToIndex(targetFile); + return testRepository.commit(commitMessage); + } + + protected void assertContentEquals(IResourceVariant variant, + String expectedContents) throws Exception { + assertContentEquals(variant.getStorage(new NullProgressMonitor()), + expectedContents); + } + + protected void assertContentEquals(IStorage storage, String expectedContents) + throws Exception { + try (Scanner scanner = new Scanner(storage.getContents())) { + scanner.useDelimiter("\\A"); + String fileContent = ""; + if (scanner.hasNext()) { + fileContent = scanner.next(); + } + assertEquals(expectedContents, fileContent); + } + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java index e92b70be61..297a43729c 100644 --- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/CoreText.java @@ -5,6 +5,7 @@ * Copyright (C) 2012, Markus Duft <markus.duft@salomon.at> * Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2013, Daniel Megert <daniel_megert@ch.ibm.com> + * Copyright (C) 2015, Obeo. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -391,6 +392,9 @@ public class CoreText extends NLS { public static String GitProjectData_UnmappingGoneResourceFailed; /** */ + public static String GitResourceVariantTreeSubscriber_name; + + /** */ public static String GitResourceVariantTreeSubscriber_fetchTaskName; /** */ diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties index 003a867d02..0ce50d79e9 100644 --- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/coretext.properties @@ -156,6 +156,7 @@ GitProjectData_repositoryChangedTaskName=Git repository changed GitProjectData_UnmapJobName=Disconnecting project {0} from Git repository GitProjectData_UnmappingGoneResourceFailed=Unmapping gone mapped resource {0} failed +GitResourceVariantTreeSubscriber_name = Git Resource Variant Tree Subscriber GitResourceVariantTreeSubscriber_fetchTaskName=Fetching data from git repositories GitResourceVariantTreeSubscriber_CouldNotFindSourceVariant=Could not find source variant for resource: {0} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProvider.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProvider.java new file mode 100644 index 0000000000..95bb42a719 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/DirCacheResourceVariantTreeProvider.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.egit.core.internal.storage.IndexResourceVariant; +import org.eclipse.egit.core.internal.util.ResourceUtil; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.team.core.variants.IResourceVariantTree; + +/** + * This will populate its three {@link IResourceVariantTree} by looking up + * information within the repository's DirCache. + * <p> + * Files that are not located within the workspace will be ignored and thus will + * not be accessible through the trees created by this provider. + * </p> + */ +public class DirCacheResourceVariantTreeProvider implements + GitResourceVariantTreeProvider { + private final IResourceVariantTree baseTree; + + private final IResourceVariantTree sourceTree; + + private final IResourceVariantTree remoteTree; + + private final Set<IResource> roots; + + private final Set<IResource> knownResources; + + /** + * Constructs the resource variant trees by iterating over the given + * repository's DirCache entries. + * + * @param repository + * The repository which DirCache info we need to cache as + * IResourceVariantTrees. + * @throws IOException + * if we somehow cannot read the DirCache. + */ + public DirCacheResourceVariantTreeProvider(Repository repository) + throws IOException { + final DirCache cache = repository.readDirCache(); + final GitResourceVariantCache baseCache = new GitResourceVariantCache(); + final GitResourceVariantCache sourceCache = new GitResourceVariantCache(); + final GitResourceVariantCache remoteCache = new GitResourceVariantCache(); + + for (int i = 0; i < cache.getEntryCount(); i++) { + final DirCacheEntry entry = cache.getEntry(i); + final IPath path = new Path(entry.getPathString()); + final IResource resource = ResourceUtil + .getResourceHandleForLocation(path); + // Resource variants only make sense for IResources. Do not consider + // files outside of the workspace or otherwise non accessible. + if (resource == null || resource.getProject() == null + || !resource.getProject().isAccessible()) { + continue; + } + switch (entry.getStage()) { + case DirCacheEntry.STAGE_0: + // Skipped on purpose (no conflict) + break; + case DirCacheEntry.STAGE_1: + baseCache.setVariant(resource, + IndexResourceVariant.create(repository, entry)); + break; + case DirCacheEntry.STAGE_2: + sourceCache.setVariant(resource, + IndexResourceVariant.create(repository, entry)); + break; + case DirCacheEntry.STAGE_3: + remoteCache.setVariant(resource, + IndexResourceVariant.create(repository, entry)); + break; + default: + throw new IllegalStateException( + "Invalid stage: " + entry.getStage()); //$NON-NLS-1$ + } + } + + baseTree = new GitCachedResourceVariantTree(baseCache); + sourceTree = new GitCachedResourceVariantTree(sourceCache); + remoteTree = new GitCachedResourceVariantTree(remoteCache); + + roots = new LinkedHashSet<IResource>(); + roots.addAll(baseCache.getRoots()); + roots.addAll(sourceCache.getRoots()); + roots.addAll(remoteCache.getRoots()); + + knownResources = new LinkedHashSet<IResource>(); + knownResources.addAll(baseCache.getKnownResources()); + knownResources.addAll(sourceCache.getKnownResources()); + knownResources.addAll(remoteCache.getKnownResources()); + } + + public IResourceVariantTree getBaseTree() { + return baseTree; + } + + public IResourceVariantTree getRemoteTree() { + return remoteTree; + } + + public IResourceVariantTree getSourceTree() { + return sourceTree; + } + + public Set<IResource> getKnownResources() { + return knownResources; + } + + public Set<IResource> getRoots() { + return roots; + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitCachedResourceVariantTree.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitCachedResourceVariantTree.java new file mode 100644 index 0000000000..3a0110c05e --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitCachedResourceVariantTree.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.util.Set; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.team.core.TeamException; +import org.eclipse.team.core.variants.IResourceVariant; +import org.eclipse.team.core.variants.IResourceVariantTree; + +/** + * An immutable resource variant tree backed by a + * {@link GitResourceVariantCache}. This will never contact the server. + * <p> + * This will not react to refreshing calls and shouldn't be used for + * synchronization purposes. + * </p> + */ +/* + * Illegal implementation of IResourceVariantTree : we could also extend the + * AbstractResourceVariantTree... but since we don't react to refreshing calls + * anyway, we do not need the extra logic it provides. + */ +class GitCachedResourceVariantTree implements IResourceVariantTree { + private final GitResourceVariantCache cache; + + public GitCachedResourceVariantTree(GitResourceVariantCache cache) { + this.cache = cache; + } + + public IResource[] roots() { + final Set<IResource> roots = cache.getRoots(); + return roots.toArray(new IResource[roots.size()]); + } + + public IResource[] members(IResource resource) throws TeamException { + return cache.members(resource); + } + + public IResourceVariant getResourceVariant(IResource resource) + throws TeamException { + return cache.getVariant(resource); + } + + public boolean hasResourceVariant(IResource resource) throws TeamException { + return cache.getVariant(resource) != null; + } + + public IResource[] refresh(IResource[] resources, int depth, + IProgressMonitor monitor) throws TeamException { + // This does not react to refresh calls + return new IResource[0]; + } + + public void flushVariants(IResource resource, int depth) + throws TeamException { + // Empty implementation + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantCache.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantCache.java new file mode 100644 index 0000000000..523f0d195a --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantCache.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.team.core.variants.IResourceVariant; + +/** + * Caches the resource variants corresponding to local IResources. + */ +class GitResourceVariantCache { + private final Map<IResource, IResourceVariant> cache = new LinkedHashMap<IResource, IResourceVariant>(); + + private final Map<IResource, Set<IResource>> members = new LinkedHashMap<IResource, Set<IResource>>(); + + private final Set<IResource> roots = new LinkedHashSet<IResource>(); + + /** + * Sets the variant associated with the given resource in this cache. + * + * @param resource + * The resource for which we need to cache a variant. + * @param variant + * Variant for the resource. + */ + public void setVariant(IResource resource, IResourceVariant variant) { + cache.put(resource, variant); + + IProject project = resource.getProject(); + roots.add(project); + + members.put(resource, new LinkedHashSet<IResource>()); + + final IResource parent = resource.getParent(); + Set<IResource> parentMembers = members.get(parent); + if (parentMembers == null) { + parentMembers = new LinkedHashSet<IResource>(); + members.put(parent, parentMembers); + } + parentMembers.add(resource); + } + + /** + * @param resource + * The resource which variant we need. + * @return The variant associated with this resource in this cache. + */ + public IResourceVariant getVariant(IResource resource) { + return cache.get(resource); + } + + /** + * @return The known roots of the tree we were populated from. + */ + public Set<IResource> getRoots() { + return Collections.unmodifiableSet(roots); + } + + /** + * @return All resources for which this cache holds variants. + */ + public Set<IResource> getKnownResources() { + return Collections.unmodifiableSet(cache.keySet()); + } + + /** + * Returns all members of the given resource for which we hold variants. + * + * @param resource + * The resource which members we need. + * @return All members of the given resource for which we hold variants; an + * empty array if none. + */ + public IResource[] members(IResource resource) { + final Set<IResource> children = members.get(resource); + return children.toArray(new IResource[children.size()]); + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantFileRevision.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantFileRevision.java new file mode 100644 index 0000000000..5c2b815b66 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantFileRevision.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import org.eclipse.egit.core.synchronize.GitRemoteResource; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.team.internal.core.mapping.ResourceVariantFileRevision; + +/** + * The default implementation of ResourceVariantFileRevision has no author, + * comment, timestamp... or any information that could be provided by the Git + * resource variant. This implementation uses the variant's information. + */ +class GitResourceVariantFileRevision extends ResourceVariantFileRevision { + + public GitResourceVariantFileRevision(GitRemoteResource variant) { + super(variant); + } + + @Override + public GitRemoteResource getVariant() { + return (GitRemoteResource) super.getVariant(); + } + + @Override + public String getContentIdentifier() { + // Use the same ID as would CommitFileRevision + return getVariant().getCommitId().getId().getName(); + } + + @Override + public long getTimestamp() { + final PersonIdent author = getVariant().getCommitId().getAuthorIdent(); + if (author != null) { + return author.getWhen().getTime(); + } + return super.getTimestamp(); + } + + @Override + public String getAuthor() { + final PersonIdent author = getVariant().getCommitId().getAuthorIdent(); + if (author != null) { + return author.getName(); + } + return super.getAuthor(); + } + + @Override + public String getComment() { + return getVariant().getCommitId().getFullMessage(); + + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeProvider.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeProvider.java new file mode 100644 index 0000000000..04e30379fc --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeProvider.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.util.Set; + +import org.eclipse.core.resources.IResource; +import org.eclipse.team.core.variants.IResourceVariantTree; + +/** + * Resource variant trees are in charge of providing the + * {@link org.eclipse.team.core.subscribers.Subscriber Subscribers} with + * resource variants, allowing them to retrieve the content of a given file in + * different states (remote, local, index, workspace...). + */ +public interface GitResourceVariantTreeProvider { + /** + * Returns the base resource variant tree. This should provide access to the + * common ancestor of the "source" and "remote" resource variants. + * + * @return The base resource variant tree. + */ + IResourceVariantTree getBaseTree(); + + /** + * Returns the remote resource variant tree. This is traditionally the + * remote data, or 'right' side of a comparison. In git terms, this is the + * "theirs" side. + * + * @return The remote resource variant tree. + */ + IResourceVariantTree getRemoteTree(); + + /** + * Returns the source resource variant tree. This is traditionally the local + * data, or 'left' side of a comparison. In git terms, this is the "ours" + * side. + * + * @return The source resource variant tree. + */ + IResourceVariantTree getSourceTree(); + + /** + * @return The list of root resources for which this provider's trees may + * hold variants. + */ + Set<IResource> getRoots(); + + /** + * Returns the whole set of resources for which this provider's trees hold + * variants. The returned resources may not necessarily exist in all three + * underlying trees. + * + * @return The whole set of resources for which this provider's trees hold + * variants. + */ + Set<IResource> getKnownResources(); +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriber.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriber.java new file mode 100644 index 0000000000..30368af4b6 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitResourceVariantTreeSubscriber.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.util.Arrays; +import java.util.Set; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.egit.core.internal.CoreText; +import org.eclipse.team.core.TeamException; +import org.eclipse.team.core.diff.IDiff; +import org.eclipse.team.core.synchronize.SyncInfo; +import org.eclipse.team.core.variants.IResourceVariant; +import org.eclipse.team.core.variants.IResourceVariantComparator; +import org.eclipse.team.core.variants.IResourceVariantTree; +import org.eclipse.team.core.variants.ResourceVariantTreeSubscriber; +import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter; + +/** + * This implementation of a {@link ResourceVariantTreeSubscriber} takes its + * input from a {@link GitResourceVariantTreeProvider}. + * <p> + * This allows us to hijack all calls from the default subscriber for "local" + * resources to our actual source tree, which could be the local working + * directory as well as it could be a branch. + * </p> + */ +public class GitResourceVariantTreeSubscriber extends + ResourceVariantTreeSubscriber { + private GitResourceVariantTreeProvider variantTreeProvider; + + private final SyncInfoToDiffConverter syncInfoConverter; + + private final IResourceVariantComparator comparator; + + /** + * @param variantTreeProvider + * The instance that will provide the base, source and remote + * trees to this subscriber. + */ + public GitResourceVariantTreeSubscriber( + GitResourceVariantTreeProvider variantTreeProvider) { + this.variantTreeProvider = variantTreeProvider; + syncInfoConverter = new GitSyncInfoToDiffConverter(variantTreeProvider); + comparator = new GitVariantComparator( + variantTreeProvider.getSourceTree()); + } + + @Override + protected IResourceVariantTree getBaseTree() { + return variantTreeProvider.getBaseTree(); + } + + @Override + protected IResourceVariantTree getRemoteTree() { + return variantTreeProvider.getRemoteTree(); + } + + /** + * @return the source resource variant tree. + */ + protected IResourceVariantTree getSourceTree() { + return variantTreeProvider.getSourceTree(); + } + + @Override + public IDiff getDiff(IResource resource) throws CoreException { + final SyncInfo info = getSyncInfo(resource); + if (info == null || info.getKind() == SyncInfo.IN_SYNC) + return null; + return syncInfoConverter.getDeltaFor(info); + } + + @Override + public SyncInfo getSyncInfo(IResource resource) throws TeamException { + // Overridden here to properly catch and re-throw the forwarded + // TeamException + try { + return super.getSyncInfo(resource); + } catch (ForwardedTeamException e) { + throw (TeamException) e.getCause(); + } + } + + @Override + public String getName() { + return CoreText.GitResourceVariantTreeSubscriber_name; + } + + @Override + public boolean isSupervised(IResource resource) throws TeamException { + return variantTreeProvider.getKnownResources().contains(resource); + } + + @Override + public IResource[] roots() { + final Set<IResource> roots = variantTreeProvider.getRoots(); + return roots.toArray(new IResource[roots.size()]); + } + + @Override + public IResourceVariantComparator getResourceComparator() { + return comparator; + } + + /** + * We have a source tree whereas Team only knows about "local" files. This + * will always use said {@link #oursTree source tree} when comparing + * variants. + */ + private static class GitVariantComparator implements + IResourceVariantComparator { + private final IResourceVariantTree oursTree; + + public GitVariantComparator(IResourceVariantTree oursTree) { + this.oursTree = oursTree; + } + + public boolean compare(IResource local, IResourceVariant remote) { + try { + final IResourceVariant oursVariant = oursTree + .getResourceVariant(local); + if (oursVariant == null) + return remote == null; + return compare(oursVariant, remote); + } catch (TeamException e) { + // We can't throw the TeamException from here, but we can't let + // the comparison go through either. + // This is only called from "getSyncInfo", we'll forward this + // exception and rethrow it from there. + throw new ForwardedTeamException(e); + } + } + + public boolean compare(IResourceVariant base, IResourceVariant remote) { + return Arrays.equals(base.asBytes(), remote.asBytes()); + } + + public boolean isThreeWay() { + return true; + } + } + + /** + * This should never be thrown outside of this class. The only purpose of + * this exception is to encapsulate a TeamException where it cannot be + * thrown. + */ + private static class ForwardedTeamException extends RuntimeException { + /** Generated SUID. */ + private static final long serialVersionUID = 4074010396155542178L; + + public ForwardedTeamException(TeamException e) { + super(e); + } + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitSyncInfoToDiffConverter.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitSyncInfoToDiffConverter.java new file mode 100644 index 0000000000..b82e9a7b60 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/GitSyncInfoToDiffConverter.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.egit.core.Activator; +import org.eclipse.egit.core.internal.CoreText; +import org.eclipse.egit.core.internal.storage.WorkspaceFileRevision; +import org.eclipse.egit.core.synchronize.GitRemoteResource; +import org.eclipse.osgi.util.NLS; +import org.eclipse.team.core.TeamException; +import org.eclipse.team.core.diff.IDiff; +import org.eclipse.team.core.diff.ITwoWayDiff; +import org.eclipse.team.core.diff.provider.ThreeWayDiff; +import org.eclipse.team.core.history.IFileRevision; +import org.eclipse.team.core.mapping.provider.ResourceDiff; +import org.eclipse.team.core.synchronize.SyncInfo; +import org.eclipse.team.core.variants.IResourceVariant; +import org.eclipse.team.internal.core.mapping.ResourceVariantFileRevision; +import org.eclipse.team.internal.core.mapping.SyncInfoToDiffConverter; + +/** + * The default implementation of SyncInfoToDiffConverter uses inaccurate + * information with regards to some of EGit features. + * <p> + * SyncInfoToDiffConverter#asFileRevision(IResourceVariant) is called when a + * user double-clicks a revision from the synchronize view (among others). + * However, the default implementation returns an IFileRevision with no comment, + * author or timestamp information. + * </p> + * <p> + * SyncInfoToDiffConverter#getDeltaFor(SyncInfo) had been originally thought by + * Team to be used for synchronizations that considered local changes. This is + * not always the case with EGit. For example, a user might try and compare two + * refs together from the Git repository explorer (right click > synchronize + * with each other). In such a case, the local files must not be taken into + * account. + * </p> + * <p> + * Most of the private methods here were copy/pasted from the super + * implementation. + * </p> + */ +public class GitSyncInfoToDiffConverter extends SyncInfoToDiffConverter { + private GitResourceVariantTreeProvider variantTreeProvider; + + /** + * Creates our diff converter given the provider of our variant trees. + * + * @param variantTreeProvider + * Provides the resource variant trees that should be used to + * query file revisions. + */ + public GitSyncInfoToDiffConverter( + GitResourceVariantTreeProvider variantTreeProvider) { + this.variantTreeProvider = variantTreeProvider; + } + + @Override + public IDiff getDeltaFor(SyncInfo info) { + if (info.getComparator().isThreeWay()) { + ITwoWayDiff local = getLocalDelta(info); + ITwoWayDiff remote = getRemoteDelta(info); + return new ThreeWayDiff(local, remote); + } else { + if (info.getKind() != SyncInfo.IN_SYNC) { + IResourceVariant remote = info.getRemote(); + IResource local = info.getLocal(); + + int kind; + if (remote == null) { + kind = IDiff.REMOVE; + } else if (!local.exists()) { + kind = IDiff.ADD; + } else { + kind = IDiff.CHANGE; + } + if (local.getType() == IResource.FILE) { + IFileRevision after = asFileState(remote); + IFileRevision before = getLocalFileRevision((IFile) local); + return new ResourceDiff(info.getLocal(), kind, 0, before, + after); + } + // For folders, we don't need file states + return new ResourceDiff(info.getLocal(), kind); + } + return null; + } + } + + private ITwoWayDiff getLocalDelta(SyncInfo info) { + int direction = SyncInfo.getDirection(info.getKind()); + if (direction == SyncInfo.OUTGOING || direction == SyncInfo.CONFLICTING) { + IResourceVariant ancestor = info.getBase(); + IResource local = info.getLocal(); + + int kind; + if (ancestor == null) { + kind = IDiff.ADD; + } else if (!local.exists()) { + kind = IDiff.REMOVE; + } else { + kind = IDiff.CHANGE; + } + if (local.getType() == IResource.FILE) { + IFileRevision before = asFileState(ancestor); + IFileRevision after = getLocalFileRevision((IFile) local); + return new ResourceDiff(info.getLocal(), kind, 0, before, after); + } + // For folders, we don't need file states + return new ResourceDiff(info.getLocal(), kind); + } + return null; + } + + /** + * Returns a file revision from the source tree for this local file. + * + * @param local + * The local file. + * @return The file revision that should be considered for the local (left) + * side of a delta + */ + public IFileRevision getLocalFileRevision(IFile local) { + try { + return asFileState(variantTreeProvider.getSourceTree() + .getResourceVariant(local)); + } catch (TeamException e) { + String error = NLS + .bind(CoreText.GitResourceVariantTreeSubscriber_CouldNotFindSourceVariant, + local.getName()); + Activator.logError(error, e); + // fall back to the working tree version + return new WorkspaceFileRevision(local); + } + } + + /* + * copied from the private implementation in SyncInfoToDiffConverter + */ + private ITwoWayDiff getRemoteDelta(SyncInfo info) { + int direction = SyncInfo.getDirection(info.getKind()); + if (direction == SyncInfo.INCOMING || direction == SyncInfo.CONFLICTING) { + IResourceVariant ancestor = info.getBase(); + IResourceVariant remote = info.getRemote(); + + int kind; + if (ancestor == null) + kind = IDiff.ADD; + else if (remote == null) + kind = IDiff.REMOVE; + else + kind = IDiff.CHANGE; + + // For folders, we don't need file states + if (info.getLocal().getType() == IResource.FILE) { + IFileRevision before = asFileState(ancestor); + IFileRevision after = asFileState(remote); + return new ResourceDiff(info.getLocal(), kind, 0, before, after); + } + + return new ResourceDiff(info.getLocal(), kind); + } + return null; + } + + /* + * copied from the private implementation in SyncInfoToDiffConverter + */ + private IFileRevision asFileState(final IResourceVariant variant) { + if (variant == null) + return null; + return asFileRevision(variant); + } + + @Override + protected ResourceVariantFileRevision asFileRevision( + IResourceVariant variant) { + if (variant instanceof GitRemoteResource) + return new GitResourceVariantFileRevision( + (GitRemoteResource) variant); + return new ResourceVariantFileRevision(variant); + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProvider.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProvider.java new file mode 100644 index 0000000000..24c1b86aa2 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/merge/TreeWalkResourceVariantTreeProvider.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.merge; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.egit.core.internal.storage.TreeParserResourceVariant; +import org.eclipse.egit.core.internal.util.ResourceUtil; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.team.core.variants.IResourceVariantTree; + +/** + * This will populate its three {@link IResourceVariantTree} by walking over a + * tree walk and caching the IResources it spans. + * <p> + * Files that are not located within the workspace will be ignored and thus will + * not be accessible through the trees created by this provider. + * </p> + */ +public class TreeWalkResourceVariantTreeProvider implements + GitResourceVariantTreeProvider { + private final IResourceVariantTree baseTree; + + private final IResourceVariantTree oursTree; + + private final IResourceVariantTree theirsTree; + + private final Set<IResource> roots; + + private final Set<IResource> knownResources; + + /** + * Constructs the resource variant trees by iterating over the given tree + * walk. This TreeWalk must contain at least three trees corresponding to + * the three "sides" we need. + * <p> + * The tree walk will be reset to its initial state when we are done with + * the iteration. + * </p> + * + * @param repository + * The repository this tree walk has been created for. + * @param treeWalk + * The tree walk to iterate over. + * @param baseIndex + * Index of the ancestor tree in the given TreeWalk (value + * returned by {@link TreeWalk#addTree(AbstractTreeIterator)}) + * @param ourIndex + * Index of our tree in the given TreeWalk (value returned by + * {@link TreeWalk#addTree(AbstractTreeIterator)}) + * @param theirIndex + * Index of their tree in the given TreeWalk (value returned by + * {@link TreeWalk#addTree(AbstractTreeIterator)}) + * @throws IOException + * if we somehow cannot iterate over the treewalk. + */ + public TreeWalkResourceVariantTreeProvider(Repository repository, + TreeWalk treeWalk, int baseIndex, int ourIndex, int theirIndex) + throws IOException { + // Record the initial state of this tree walk before iterating + final AbstractTreeIterator[] initialTrees = new AbstractTreeIterator[treeWalk + .getTreeCount()]; + for (int i = 0; i < treeWalk.getTreeCount(); i++) { + initialTrees[i] = treeWalk.getTree(i, AbstractTreeIterator.class); + } + + final GitResourceVariantCache baseCache = new GitResourceVariantCache(); + final GitResourceVariantCache theirsCache = new GitResourceVariantCache(); + final GitResourceVariantCache oursCache = new GitResourceVariantCache(); + + while (treeWalk.next()) { + final int modeBase = treeWalk.getRawMode(baseIndex); + final int modeOurs = treeWalk.getRawMode(ourIndex); + final int modeTheirs = treeWalk.getRawMode(theirIndex); + if (modeBase == 0 && modeOurs == 0 && modeTheirs == 0) { + // untracked + continue; + } + + final CanonicalTreeParser base = treeWalk.getTree(baseIndex, + CanonicalTreeParser.class); + final CanonicalTreeParser ours = treeWalk.getTree(ourIndex, + CanonicalTreeParser.class); + final CanonicalTreeParser theirs = treeWalk.getTree(theirIndex, + CanonicalTreeParser.class); + + final IPath path = new Path(treeWalk.getPathString()); + final IResource resource = ResourceUtil + .getResourceHandleForLocation(path); + // Resource variants only make sense for IResources. Do not consider + // files outside of the workspace or otherwise non accessible. + if (resource != null && resource.getProject().isAccessible()) { + if (modeBase != 0) { + baseCache.setVariant(resource, + TreeParserResourceVariant.create(repository, base)); + } + if (modeOurs != 0) { + oursCache.setVariant(resource, + TreeParserResourceVariant.create(repository, ours)); + } + if (modeTheirs != 0) { + theirsCache.setVariant(resource, + TreeParserResourceVariant.create(repository, theirs)); + } + } + + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + } + } + + // TODO any better way to reset the tree walk after an iteration? + treeWalk.reset(); + for (int i = 0; i < initialTrees.length; i++) { + initialTrees[i].reset(); + treeWalk.addTree(initialTrees[i]); + } + + baseTree = new GitCachedResourceVariantTree(baseCache); + theirsTree = new GitCachedResourceVariantTree(theirsCache); + oursTree = new GitCachedResourceVariantTree(oursCache); + + roots = new LinkedHashSet<IResource>(); + roots.addAll(baseCache.getRoots()); + roots.addAll(oursCache.getRoots()); + roots.addAll(theirsCache.getRoots()); + + knownResources = new LinkedHashSet<IResource>(); + knownResources.addAll(baseCache.getKnownResources()); + knownResources.addAll(oursCache.getKnownResources()); + knownResources.addAll(theirsCache.getKnownResources()); + } + + public IResourceVariantTree getBaseTree() { + return baseTree; + } + + public IResourceVariantTree getRemoteTree() { + return theirsTree; + } + + public IResourceVariantTree getSourceTree() { + return oursTree; + } + + public Set<IResource> getKnownResources() { + return knownResources; + } + + public Set<IResource> getRoots() { + return roots; + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/AbstractGitResourceVariant.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/AbstractGitResourceVariant.java new file mode 100644 index 0000000000..22effd8371 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/AbstractGitResourceVariant.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.storage; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.team.core.variants.IResourceVariant; + +/** + * Base class of the git-related resource variants. + */ +public abstract class AbstractGitResourceVariant implements IResourceVariant { + /** Repository in which this variant's content will be accessed. */ + protected final Repository repository; + + /** Repository-relative path of this resource. */ + protected final String path; + + /** + * Whether this resource is a container or not in this particular variant. + * This may be different than the local resource's state (if there is a + * file/folder conflict for example). + */ + protected final boolean isContainer; + + /** Object id of this variant in its repository. */ + protected final ObjectId objectId; + + /** Raw mode bits of this variant. */ + protected final int rawMode; + + /** + * @param repository + * Repository in which this variant's content will be accessed. + * @param path + * Repository-relative path of this resource. + * @param isContainer + * Whether this resource is a container or not in this particular + * variant. + * @param objectId + * Object id of this variant in its repository. + * @param rawMode + * Raw mode bits of this variant. + */ + protected AbstractGitResourceVariant(Repository repository, String path, + boolean isContainer, ObjectId objectId, int rawMode) { + this.repository = repository; + this.path = path; + this.isContainer = isContainer; + this.objectId = objectId; + this.rawMode = rawMode; + } + + public String getName() { + int lastSeparator = path.lastIndexOf('/'); + return path.substring(lastSeparator + 1); + } + + public boolean isContainer() { + return isContainer; + } + + public String getContentIdentifier() { + return objectId.name(); + } + + public byte[] asBytes() { + return objectId.name().getBytes(); + } + + /** + * @return the object id of this variant in its backing repository. + */ + public ObjectId getObjectId() { + return objectId; + } + + /** + * @return the raw mode of this variant. + */ + public int getRawMode() { + return rawMode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj instanceof AbstractGitResourceVariant) { + AbstractGitResourceVariant other = (AbstractGitResourceVariant) obj; + return this.path.equals(other.path) + && this.repository.equals(other.repository) + && this.objectId.equals(other.objectId); + } + return false; + } + + @Override + public int hashCode() { + int hash = 37; + hash = 37 * hash + (path != null ? path.hashCode() : 0); + hash = 37 * hash + (repository != null ? repository.hashCode() : 0); + hash = 37 * hash + (objectId != null ? objectId.hashCode() : 0); + return hash; + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/IndexResourceVariant.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/IndexResourceVariant.java new file mode 100644 index 0000000000..14b3b0be44 --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/IndexResourceVariant.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.storage; + +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.team.core.TeamException; + +/** + * Implementation of a resource variant populated through a Repository's + * DirCache information. + */ +public class IndexResourceVariant extends AbstractGitResourceVariant { + private IndexResourceVariant(Repository repository, String path, + boolean isContainer, ObjectId objectId, int rawMode) { + super(repository, path, isContainer, objectId, rawMode); + } + + /** + * Constructs a resource variant corresponding to the given DirCache entry. + * + * @param repository + * Repository from which this DirCacheEntry was extracted. + * @param entry + * The DirCacheEntry for which content we need an + * IResourceVariant. + * @return The created variant. + */ + public static IndexResourceVariant create(Repository repository, + DirCacheEntry entry) { + final String path = entry.getPathString(); + final boolean isContainer = FileMode.TREE.equals(entry.getFileMode()); + final ObjectId objectId = entry.getObjectId(); + final int rawMode = entry.getRawMode(); + + return new IndexResourceVariant(repository, path, isContainer, + objectId, rawMode); + } + + public IStorage getStorage(IProgressMonitor monitor) throws TeamException { + return new IndexBlobStorage(repository, path, objectId); + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/TreeParserResourceVariant.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/TreeParserResourceVariant.java new file mode 100644 index 0000000000..772ae80aee --- /dev/null +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/storage/TreeParserResourceVariant.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (C) 2015, Obeo. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.core.internal.storage; + +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.egit.core.storage.GitBlobStorage; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.team.core.TeamException; + +/** + * Implementation of a resource variant populated through a CanonicalTreeParser. + * This can provide access to local resources as well as their remote variants. + */ +public class TreeParserResourceVariant extends AbstractGitResourceVariant { + private TreeParserResourceVariant(Repository repository, String path, + boolean isContainer, ObjectId objectId, int rawMode) { + super(repository, path, isContainer, objectId, rawMode); + } + + /** + * Constructs a resource variant corresponding to the current entry of the + * given CanonicalTreeParser. + * + * @param repository + * Repository from which this CanonicalTreeParser was created. + * @param treeParser + * A CanonicalTreeParser to retrieve information from. This will + * only read information about the current entry on which this + * parser is positioned and will not change its state. + * @return The created variant. + */ + public static TreeParserResourceVariant create(Repository repository, + CanonicalTreeParser treeParser) { + final String path = treeParser.getEntryPathString(); + final boolean isContainer = FileMode.TREE.equals(treeParser + .getEntryFileMode()); + final ObjectId objectId = treeParser.getEntryObjectId(); + final int rawMode = treeParser.getEntryRawMode(); + + return new TreeParserResourceVariant(repository, path, isContainer, + objectId, rawMode); + } + + public IStorage getStorage(IProgressMonitor monitor) throws TeamException { + return new GitBlobStorage(repository, path, objectId); + } +} diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/util/ResourceUtil.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/util/ResourceUtil.java index 1ac64f5f73..ef0b49b965 100644 --- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/util/ResourceUtil.java +++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/util/ResourceUtil.java @@ -1,7 +1,7 @@ /******************************************************************************* * Copyright (C) 2011, Jens Baumgart <jens.baumgart@sap.com> * Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org> - * Copyright (C) 2012, 2013 Laurent Goubet <laurent.goubet@obeo.fr> + * Copyright (C) 2012, 2015 Laurent Goubet <laurent.goubet@obeo.fr> * Copyright (C) 2012, Gunnar Wagenknecht <gunnar@wagenknecht.org> * * All rights reserved. This program and the accompanying materials @@ -198,6 +198,27 @@ public class ResourceUtil { } /** + * Returns a resource handle for this path in the workspace. Note that + * neither the resource nor the result need exist in the workspace : this + * may return inexistant or otherwise non-accessible IResources. + * + * @param path + * Path for which we need a resource handle. + * @return The resource handle for the given path in the workspace. + */ + public static IResource getResourceHandleForLocation(IPath path) { + final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace() + .getRoot(); + + final IResource resource; + if (path.segmentCount() > 1) + resource = workspaceRoot.getFile(path); + else + resource = workspaceRoot.getProject(path.toString()); + return resource; + } + + /** * The method splits the given resources by their repository. For each * occurring repository a list is built containing the repository relative * paths of the related resources. |