/******************************************************************************* * Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch> * * 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.ui.internal.submodules; import static org.eclipse.egit.ui.JobFamilies.ADD_TO_INDEX; import static org.eclipse.egit.ui.JobFamilies.GENERATE_HISTORY; import static org.eclipse.egit.ui.JobFamilies.REMOVE_FROM_INDEX; import static org.eclipse.swtbot.eclipse.finder.waits.Conditions.waitForEditor; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.util.Collections; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.JobFamilies; import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry; import org.eclipse.egit.core.project.GitProjectData; import org.eclipse.egit.core.project.RepositoryMapping; import org.eclipse.egit.core.test.TestRepository; import org.eclipse.egit.ui.common.LocalRepositoryTestCase; import org.eclipse.egit.ui.internal.clone.ProjectRecord; import org.eclipse.egit.ui.internal.clone.ProjectUtils; import org.eclipse.egit.ui.internal.resources.IResourceState; import org.eclipse.egit.ui.internal.resources.ResourceStateFactory; import org.eclipse.egit.ui.test.ContextMenuHelper; import org.eclipse.egit.ui.test.TestUtil; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.submodule.SubmoduleWalk; import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView; import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IViewPart; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(SWTBotJunit4ClassRunner.class) public class SubmoduleFolderTest extends LocalRepositoryTestCase { private static final String SUBFOLDER = "sub"; private static final String CHILD = "child"; private static final String CHILDPROJECT = "ChildProject"; private Repository parentRepository; private Repository childRepository; private Repository subRepository; private IProject parentProject; private IProject childProject; private IFolder childFolder; private File parentRepositoryGitDir; private File childRepositoryGitDir; private File subRepositoryGitDir; @Before public void setUp() throws Exception { parentRepositoryGitDir = createProjectAndCommitToRepository(); childRepositoryGitDir = createProjectAndCommitToRepository(CHILDREPO, CHILDPROJECT); Activator.getDefault().getRepositoryUtil() .addConfiguredRepository(parentRepositoryGitDir); parentRepository = lookupRepository(parentRepositoryGitDir); childRepository = lookupRepository(childRepositoryGitDir); parentProject = ResourcesPlugin.getWorkspace().getRoot() .getProject(PROJ1); IFolder folder = parentProject.getFolder(FOLDER); IFolder subfolder = folder.getFolder(SUBFOLDER); subfolder.create(false, true, null); assertTrue(subfolder.exists()); IFile someFile = subfolder.getFile("dummy.txt"); touch(PROJ1, someFile.getProjectRelativePath().toOSString(), "Dummy content"); addAndCommit(someFile, "Commit sub/dummy.txt"); childFolder = subfolder.getFolder(CHILD); Git.wrap(parentRepository).submoduleAdd() .setPath(childFolder.getFullPath().toPortableString()) .setURI(childRepository.getDirectory().toURI() .toString()) .call(); TestRepository parentRepo = new TestRepository(parentRepository); parentRepo.trackAllFiles(parentProject); parentRepo.commit("Commit submodule"); assertTrue(SubmoduleWalk.containsGitModulesFile(parentRepository)); parentProject.refreshLocal(IResource.DEPTH_INFINITE, null); assertTrue(childFolder.exists()); // Let's get rid of the child project imported directly from the child // repository. childProject = ResourcesPlugin.getWorkspace().getRoot() .getProject(CHILDPROJECT); childProject.delete(false, true, null); // Re-import it from the parent repo's submodule! IFile projectFile = childFolder.getFolder(CHILDPROJECT) .getFile(IProjectDescription.DESCRIPTION_FILE_NAME); assertTrue(projectFile.exists()); ProjectRecord pr = new ProjectRecord( projectFile.getLocation().toFile()); ProjectUtils.createProjects(Collections.singleton(pr), null, null); assertTrue(childProject.isOpen()); // Now we have a parent repo in a state as if we had recursively // cloned some remote repo with a submodule and then imported all // projects. Look up the submodule repository instance through the // repository cache, so that we get the same instance that EGit // uses. subRepository = SubmoduleWalk.getSubmoduleRepository( childFolder.getParent().getLocation().toFile(), CHILD); assertNotNull(subRepository); subRepositoryGitDir = subRepository.getDirectory(); subRepository.close(); subRepository = lookupRepository(subRepositoryGitDir); assertNotNull(subRepository); } @After public void removeConfiguredRepositories() { if (parentRepositoryGitDir != null) { Activator.getDefault().getRepositoryUtil() .removeDir(parentRepositoryGitDir); } if (childRepositoryGitDir != null) { Activator.getDefault().getRepositoryUtil() .removeDir(childRepositoryGitDir); } childRepository = null; parentRepository = null; subRepository = null; } @Test public void testChildProjectMapsToSubRepo() { RepositoryMapping mapping = RepositoryMapping.getMapping(childProject); assertNotNull("Child project should have a mapping", mapping); assertEquals(subRepository, mapping.getRepository()); } @Test public void testChildFolderMapsToSubRepo() { RepositoryMapping mapping = RepositoryMapping.getMapping(childFolder); assertNotNull("Child folder should have a mapping", mapping); assertEquals(subRepository, mapping.getRepository()); } @Test public void testParentFolderMapsToParentRepo() { RepositoryMapping mapping = RepositoryMapping .getMapping(childFolder.getParent()); assertNotNull("Child folder's parent should have a mapping", mapping); assertEquals(parentRepository, mapping.getRepository()); } /** * Tests AddToIndex and RemoveFromIndex commands on a file from a submodule * folder. Verifies the execution of the command by testing the state of the * file in the index diff after it has been executed. Additionally verifies * that decorations do get updated. * * @throws Exception */ @Test public void testStageUnstageInSubRepo() throws Exception { IFolder childProjectFolder = childFolder.getFolder(CHILDPROJECT); IFolder folder = childProjectFolder.getFolder(FOLDER); IFile file = folder.getFile(FILE1); touch(PROJ1, file.getProjectRelativePath().toOSString(), "Modified"); TestUtil.joinJobs(JobFamilies.INDEX_DIFF_CACHE_UPDATE); SWTBotTree projectExplorerTree = TestUtil.getExplorerTree(); SWTBotTreeItem node = TestUtil.navigateTo(projectExplorerTree, file.getFullPath().segments()); TestUtil.waitForDecorations(); assertTrue(node.getText().startsWith("> " + file.getName())); node.select(); ContextMenuHelper.clickContextMenuSync(projectExplorerTree, "Team", util.getPluginLocalizedValue("AddToIndexAction_label")); TestUtil.joinJobs(ADD_TO_INDEX); TestUtil.joinJobs(JobFamilies.INDEX_DIFF_CACHE_UPDATE); IndexDiffCacheEntry cache = Activator.getDefault().getIndexDiffCache() .getIndexDiffCacheEntry(subRepository); IResourceState state = ResourceStateFactory.getInstance() .get(cache.getIndexDiff(), file); assertTrue("File should be staged", state.isStaged()); TestUtil.waitForDecorations(); assertFalse(node.getText().startsWith("> ")); ContextMenuHelper.clickContextMenuSync(projectExplorerTree, "Team", util.getPluginLocalizedValue("RemoveFromIndexAction_label")); TestUtil.joinJobs(REMOVE_FROM_INDEX); TestUtil.joinJobs(JobFamilies.INDEX_DIFF_CACHE_UPDATE); state = ResourceStateFactory.getInstance().get(cache.getIndexDiff(), file); assertFalse("File should not be staged", state.isStaged()); assertTrue("File should be dirty", state.isDirty()); TestUtil.waitForDecorations(); assertTrue(node.getText().startsWith("> " + file.getName())); } /** * Tests that a CompareWithHeadAction on a file from a submodule folder does * open the right compare editor, comparing against the version from the * submodule (as opposed to the version from the parent repo). * * @throws Exception */ @Test public void compareWithHeadInSubmoduleFolder() throws Exception { // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=446344#c11 // If the compare editor's title does not contain the HEAD id of // the subrepo, then either no compare editor got opened, or // it was opened using the parent repo. IFolder childProjectFolder = childFolder.getFolder(CHILDPROJECT); IFolder folder = childProjectFolder.getFolder(FOLDER); IFile file = folder.getFile(FILE1); touch(PROJ1, file.getProjectRelativePath().toOSString(), "Modified"); SWTBotTree projectExplorerTree = TestUtil.getExplorerTree(); SWTBotTreeItem node = TestUtil.navigateTo(projectExplorerTree, file.getFullPath().segments()); node.select(); Ref headRef = subRepository.findRef(Constants.HEAD); final String headId = headRef.getObjectId().abbreviate(6).name(); ContextMenuHelper.clickContextMenuSync(projectExplorerTree, "Compare With", util.getPluginLocalizedValue("CompareWithHeadAction_label")); bot.waitUntil(waitForEditor(new BaseMatcher<IEditorReference>() { @Override public boolean matches(Object item) { return (item instanceof IEditorReference) && ((IEditorReference) item).getTitle() .contains(headId); } @Override public void describeTo(Description description) { description.appendText("Wait for editor containing " + headId); } }), 5000); } @Test public void testDisconnect() throws Exception { SWTBotTree projectExplorerTree = TestUtil.getExplorerTree(); getProjectItem(projectExplorerTree, PROJ1).select(); String menuString = util .getPluginLocalizedValue("DisconnectAction_label"); ContextMenuHelper.clickContextMenuSync(projectExplorerTree, "Team", menuString); TestUtil.waitForJobs(500, 5000); TestUtil.joinJobs(JobFamilies.INDEX_DIFF_CACHE_UPDATE); ResourcesPlugin.getWorkspace().getRoot() .refreshLocal(IResource.DEPTH_INFINITE, null); // Access the session property directly: RepositoryMapping.getMapping() // checks whether the project is shared with git. Object mapping = childFolder.getSessionProperty(new QualifiedName( GitProjectData.class.getName(), "RepositoryMapping")); assertNull("Should have no RepositoryMapping", mapping); } @Test public void testDecoration() throws Exception { SWTBotTree projectExplorerTree = TestUtil.getExplorerTree(); SWTBotTreeItem node = TestUtil.navigateTo(projectExplorerTree, childFolder.getFullPath().segments()); TestUtil.waitForDecorations(); assertTrue("Folder should have repo/branch decoration", node.getText().contains("[master")); TestUtil.expandAndWait(node); node = TestUtil.getChildNode(node, CHILDPROJECT); TestUtil.waitForDecorations(); assertFalse("Folder should not have repo/branch decoration", node.getText().contains("[")); node = TestUtil.navigateTo(projectExplorerTree, CHILDPROJECT); TestUtil.waitForDecorations(); assertTrue("Project should have subrepo/branch decoration", node.getText().contains("[child")); } /** * Tests that unrelated changes to the configured repositories do not * prematurely remove submodules from the cache. */ @Test public void testRepoRemoval() { Activator.getDefault().getRepositoryUtil() .addConfiguredRepository(childRepositoryGitDir); assertTrue("Should still have the subrepo in the cache", containsRepo(Activator.getDefault().getRepositoryCache() .getAllRepositories(), subRepository)); assertTrue("Should have changed the preference", Activator.getDefault() .getRepositoryUtil().removeDir(childRepositoryGitDir)); assertTrue("Should still have the subrepo in the cache", containsRepo(Activator.getDefault().getRepositoryCache() .getAllRepositories(), subRepository)); } @SuppressWarnings("restriction") @Test public void testHistoryFromProjectExplorerIsFromSubRepository() throws Exception { // Open history view SWTBotView historyBot = TestUtil.showHistoryView(); IViewPart viewPart = historyBot.getViewReference().getView(false); assertTrue( viewPart instanceof org.eclipse.team.internal.ui.history.GenericHistoryView); // Set link with selection ((org.eclipse.team.internal.ui.history.GenericHistoryView) viewPart) .setLinkingEnabled(true); // Select PROJ1 (has 3 commits) TestUtil.navigateTo(TestUtil.getExplorerTree(), PROJ1).select(); assertRowCountInHistory(PROJ1, 3); // Select the child folder (from the submodule; has 2 commits) TestUtil.navigateTo(TestUtil.getExplorerTree(), childFolder.getFullPath().segments()).select(); assertRowCountInHistory(childFolder.getFullPath() + " from submodule", 2); } private boolean containsRepo(Repository[] repositories, Repository needle) { for (Repository repo : repositories) { if (needle.equals(repo)) { return true; } } return false; } private void assertRowCountInHistory(String msg, int expected) throws Exception { SWTBotView historyBot = TestUtil.showHistoryView(); Job.getJobManager().join(GENERATE_HISTORY, null); historyBot.getWidget().getDisplay().syncExec(new Runnable() { @Override public void run() { // Joins UI update triggered by GenerateHistoryJob } }); assertEquals(msg + " should show " + expected + " commits", expected, historyBot.bot().table().rowCount()); } }