Skip to content

Content of file SubmoduleFolderTest.java

/*******************************************************************************
 * 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 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
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.egit.ui.JobFamilies.REPO_VIEW_REFRESH;
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.commands.State;
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.JobFamilies;
import org.eclipse.egit.core.RepositoryCache;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.info.GitItemState;
import org.eclipse.egit.core.internal.Utils;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
import org.eclipse.egit.core.internal.info.GitItemStateFactory;
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.UIText;
import org.eclipse.egit.ui.internal.clone.ProjectRecord;
import org.eclipse.egit.ui.internal.clone.ProjectUtils;
import org.eclipse.egit.ui.internal.repository.RepositoriesView;
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.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
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.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.RegistryToggleState;
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);
		RepositoryUtil.INSTANCE.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);
		Git.wrap(parentRepository).add().addFilepattern(".").call();
		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) {
			RepositoryUtil.INSTANCE.removeDir(parentRepositoryGitDir);
		}
		if (childRepositoryGitDir != null) {
			RepositoryUtil.INSTANCE.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 = IndexDiffCache.INSTANCE
				.getIndexDiffCacheEntry(subRepository);
		GitItemState state = GitItemStateFactory.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 = GitItemStateFactory.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 the Team->Switch To... menu item has content by clicking on
	 * "New Branch..." and then closing the resulting "Create Branch" dialog.
	 *
	 * @throws Exception
	 */
	@Test
	public void testSwitchToMenu() throws Exception {
		SWTBotTree projectExplorerTree = TestUtil.getExplorerTree();
		SWTBotTreeItem node = TestUtil.navigateTo(projectExplorerTree,
				childFolder.getFullPath().segments());
		TestUtil.waitForDecorations();
		node.select();
		ContextMenuHelper.clickContextMenu(projectExplorerTree, "Team",
				util.getPluginLocalizedValue("SwitchToMenu.label"),
				UIText.SwitchToMenu_NewBranchMenuLabel);

		SWTBotShell shell = bot.shell(UIText.CreateBranchWizard_NewBranchTitle);
		shell.close();
	}

	@Test
	public void testRepoViewFollowSelection() throws Exception {
		ICommandService srv = PlatformUI.getWorkbench()
				.getService(ICommandService.class);
		State commandState = srv
				.getCommand(RepositoriesView.LINK_WITH_SELECTION_ID)
				.getState(RegistryToggleState.STATE_ID);
		Boolean followsSelection = (Boolean) commandState.getValue();
		commandState.setValue(Boolean.TRUE);
		SWTBotView view = TestUtil.showView(RepositoriesView.VIEW_ID);
		TestUtil.joinJobs(REPO_VIEW_REFRESH);
		try {
			SWTBotTree projectExplorerTree = TestUtil.getExplorerTree();
			SWTBotTreeItem node = TestUtil.navigateTo(projectExplorerTree,
					childFolder.getFullPath().segments());
			node.select();
			TestUtil.waitForDecorations();
			TestUtil.joinJobs(REPO_VIEW_REFRESH);
			SWTBotTree tree = view.bot().tree();
			int[] numberOfSelected = { 0 };
			boolean[] parentFound = { false };
			view.getWidget().getDisplay().syncExec(() -> {
				Tree t = tree.widget;
				TreeItem[] selected = t.getSelection();
				numberOfSelected[0] = selected.length;
				if (selected.length == 1) {
					TreeItem root = null;
					TreeItem parent = selected[0].getParentItem();
					String parentRepoName = parentRepositoryGitDir
							.getParentFile().getName();
					while (parent != null) {
						root = parent;
						parent = parent.getParentItem();
					}
					if (root != null
							&& root.getText().startsWith(parentRepoName)) {
						parentFound[0] = true;
					}
				}
			});
			assertEquals("One node selected", 1, numberOfSelected[0]);
			assertTrue("Selected node not under parent repository",
					parentFound[0]);
		} finally {
			// Reset "follow selection"
			commandState.setValue(followsSelection);
		}
	}

	/**
	 * 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 = Utils.getShortObjectId(headRef.getObjectId());
		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() {
		RepositoryUtil.INSTANCE.addConfiguredRepository(childRepositoryGitDir);
		assertTrue("Should still have the subrepo in the cache",
				containsRepo(RepositoryCache.INSTANCE.getAllRepositories(),
						subRepository));
		assertTrue("Should have changed the preference",
				RepositoryUtil.INSTANCE.removeDir(childRepositoryGitDir));
		assertTrue("Should still have the subrepo in the cache",
				containsRepo(RepositoryCache.INSTANCE.getAllRepositories(),
						subRepository));
	}

	@SuppressWarnings("restriction")
	@Test
	public void testHistoryFromProjectExplorerIsFromSubRepository()
			throws Exception {
		// Open history view
		SWTBotView historyBot = TestUtil.showHistoryView();
		IViewPart viewPart = historyBot.getViewReference().getView(false);
		assertTrue(
Assertion of type org.eclipse.team.internal.ui.history.GenericHistoryView in org.eclipse.egit.ui.internal.submodules.SubmoduleFolderTest.testHistoryFromProjectExplorerIsFromSubRepository() at SubmoduleFolderTest.java:[line 415] may hide useful information about why a cast may have failed.
no message found
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 4 commits) TestUtil.navigateTo(TestUtil.getExplorerTree(), PROJ1).select(); assertRowCountInHistory(PROJ1, 4); // 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()); } }