/******************************************************************************* * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2011, Dariusz Luksza <dariusz@luksza.org> * Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org> * Copyright (C) 2012, 2013 François Rey <eclipse.org_@_francois_._rey_._name> * Copyright (C) 2013 Laurent Goubet <laurent.goubet@obeo.fr> * Copyright (C) 2015, IBM Corporation (Dani Megert <daniel_megert@ch.ibm.com>) * Copyright (C) 2016, Stefan Dirix <sdirix@eclipsesource.com> * * 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.actions; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.expressions.IEvaluationContext; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.mapping.ResourceMapping; import org.eclipse.core.runtime.IPath; import org.eclipse.egit.core.AdapterUtils; import org.eclipse.egit.core.internal.CompareCoreUtils; import org.eclipse.egit.core.project.RepositoryMapping; import org.eclipse.egit.ui.internal.CommonUtils; import org.eclipse.egit.ui.internal.selection.SelectionUtils; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jgit.diff.DiffConfig; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.FollowFilter; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.filter.OrTreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindowActionDelegate; import org.eclipse.ui.handlers.HandlerUtil; /** * A helper class for Team Actions on Git controlled projects */ abstract class RepositoryActionHandler extends AbstractHandler { private IEvaluationContext evaluationContext; IStructuredSelection mySelection; /** * Set the selection when used by {@link RepositoryAction} as * {@link IWorkbenchWindowActionDelegate} * * @param selection * the new selection */ public void setSelection(ISelection selection) { mySelection = SelectionUtils.getStructuredSelection(selection); } /** * Retrieve the list of projects that contains the given resources. All * resources must actually map to a project shared with egit, otherwise an * empty array is returned. In case of a linked resource, the project * returned is the one that contains the link target and is shared with * egit, if any, otherwise an empty array is also returned. * * @param selection * @return the projects hosting the selected resources */ private IProject[] getProjectsForSelectedResources( IStructuredSelection selection) { Set<IProject> ret = new LinkedHashSet<>(); for (IResource resource : getSelectedAdaptables(selection, IResource.class)) { RepositoryMapping mapping = RepositoryMapping.getMapping(resource); if (mapping != null && (mapping.getContainer() instanceof IProject)) ret.add((IProject) mapping.getContainer()); else return new IProject[0]; } ret.addAll(extractProjectsFromMappings(selection)); return ret.toArray(new IProject[ret.size()]); } private Set<IProject> extractProjectsFromMappings( IStructuredSelection selection) { Set<IProject> ret = new LinkedHashSet<>(); for (ResourceMapping mapping : getSelectedAdaptables(selection, ResourceMapping.class)) { IProject[] mappedProjects = mapping.getProjects(); if (mappedProjects != null && mappedProjects.length != 0) { // Some mappings (WorkingSetResourceMapping) return the projects // in unpredictable order. Sort them like the navigator to // correspond to the order the user usually sees. List<IProject> projects = new ArrayList<>( Arrays.asList(mappedProjects)); Collections .sort(projects, CommonUtils.RESOURCE_NAME_COMPARATOR); ret.addAll(projects); } } return ret; } /** * Retrieve the list of projects that contains the selected resources. All * resources must actually map to a project shared with egit, otherwise an * empty array is returned. In case of a linked resource, the project * returned is the one that contains the link target and is shared with * egit, if any, otherwise an empty array is also returned. * * @param event * @return the projects hosting the selected resources * @throws ExecutionException */ protected IProject[] getProjectsForSelectedResources(ExecutionEvent event) throws ExecutionException { IStructuredSelection selection = getSelection(event); return getProjectsForSelectedResources(selection); } /** * Retrieve the list of projects that contains the selected resources. All * resources must actually map to a project shared with egit, otherwise an * empty array is returned. In case of a linked resource, the project * returned is the one that contains the link target and is shared with * egit, if any, otherwise an empty array is also returned. * * @return the projects hosting the selected resources */ protected IProject[] getProjectsForSelectedResources() { IStructuredSelection selection = getSelection(); return getProjectsForSelectedResources(selection); } /** * @param projects * a list of projects * @return the repositories that projects map to if all projects are mapped */ protected Repository[] getRepositoriesFor(final IProject[] projects) { Set<Repository> ret = new LinkedHashSet<>(); for (IProject project : projects) { RepositoryMapping repositoryMapping = RepositoryMapping .getMapping(project); if (repositoryMapping == null) return new Repository[0]; ret.add(repositoryMapping.getRepository()); } return ret.toArray(new Repository[ret.size()]); } /** * Determines whether the selection contains only resources that are in some * git repository. * * @return {@code true} if all resources in the selection belong to a git * repository known to EGit. */ protected boolean haveSelectedResourcesWithRepository() { IStructuredSelection selection = getSelection(); if (selection != null) { IResource[] resources = SelectionUtils .getSelectedResources(selection); if (resources.length > 0) { for (IResource resource : resources) { if (resource == null || RepositoryMapping.getMapping(resource) == null) { return false; } } return true; } } return false; } /** * Figure out which repository to use. All selected resources must map to * the same Git repository. * * @param warn * Put up a message dialog to warn why a resource was not * selected * @param event * @return repository for current project, or null * @throws ExecutionException */ protected Repository getRepository(boolean warn, ExecutionEvent event) throws ExecutionException { IStructuredSelection selection = getSelection(event); if (warn) { Shell shell = getShell(event); return SelectionUtils.getRepositoryOrWarn(selection, shell); } else { return SelectionUtils.getRepository(selection); } } /** * Figure out which repository to use. All selected resources must map to * the same Git repository. * * @return repository for current project, or null */ protected Repository getRepository() { IStructuredSelection selection = getSelection(); return SelectionUtils.getRepository(selection); } /** * Figure out which repositories to use. All selected resources must map to * a Git repository. * * @param event * * @return repositories for selection, or an empty array * @throws ExecutionException */ protected Repository[] getRepositories(ExecutionEvent event) throws ExecutionException { IProject[] selectedProjects = getProjectsForSelectedResources(event); if (selectedProjects.length > 0) { return getRepositoriesFor(selectedProjects); } IStructuredSelection selection = getSelection(event); if (!selection.isEmpty()) { Set<Repository> repos = new LinkedHashSet<>(); for (Object o : selection.toArray()) { Repository repo = AdapterUtils.adapt(o, Repository.class); if (repo != null) { repos.add(repo); } } return repos.toArray(new Repository[repos.size()]); } return new Repository[0]; } /** * Get the currently selected repositories. All selected projects must map * to a repository. * * @return repositories for selection, or an empty array */ public Repository[] getRepositories() { IProject[] selectedProjects = getProjectsForSelectedResources(); if (selectedProjects.length > 0) return getRepositoriesFor(selectedProjects); IStructuredSelection selection = getSelection(); if (!selection.isEmpty()) { Set<Repository> repos = new LinkedHashSet<>(); for (Object o : selection.toArray()) { Repository repo = AdapterUtils.adapt(o, Repository.class); if (repo != null) { repos.add(repo); } else { // no repository found for one of the objects! return new Repository[0]; } } return repos.toArray(new Repository[repos.size()]); } return new Repository[0]; } /** * @param event * the execution event, must not be null * @return the current selection * @throws ExecutionException * if the selection can't be determined */ protected static IStructuredSelection getSelection(ExecutionEvent event) throws ExecutionException { if (event == null) throw new IllegalArgumentException("event must not be NULL"); //$NON-NLS-1$ Object context = event.getApplicationContext(); if (context instanceof IEvaluationContext) return SelectionUtils.getSelection((IEvaluationContext) context); return StructuredSelection.EMPTY; } /** * @return the current selection */ protected IStructuredSelection getSelection() { // if the selection was set explicitly, use it if (mySelection != null) return mySelection; return SelectionUtils.getSelection(evaluationContext); } @Override public void setEnabled(Object evaluationContext) { this.evaluationContext = (IEvaluationContext) evaluationContext; } /** * Creates an array of the given class type containing all the objects in * the selection that adapt to the given class. * * @param selection * @param c * @return the selected adaptables */ private <T> List<T> getSelectedAdaptables(ISelection selection, Class<T> c) { List<T> result; if (selection != null && !selection.isEmpty()) { result = new ArrayList<>(); Iterator elements = ((IStructuredSelection) selection).iterator(); while (elements.hasNext()) { T adapter = AdapterUtils.adapt(elements.next(), c); if (adapter != null) { result.add(adapter); } } } else { result = Collections.emptyList(); } return result; } /** * @param event * @return the resources in the selection * @throws ExecutionException */ protected IResource[] getSelectedResources(ExecutionEvent event) throws ExecutionException { IStructuredSelection selection = getSelection(event); return SelectionUtils.getSelectedResources(selection); } protected IPath[] getSelectedLocations(ExecutionEvent event) throws ExecutionException { IStructuredSelection selection = getSelection(event); return SelectionUtils.getSelectedLocations(selection); } /** * @return the resources in the selection */ protected IResource[] getSelectedResources() { IStructuredSelection selection = getSelection(); return SelectionUtils.getSelectedResources(selection); } /** * @return the locations in the selection */ protected IPath[] getSelectedLocations() { IStructuredSelection selection = getSelection(); return SelectionUtils.getSelectedLocations(selection); } /** * @return true if all selected items map to the same repository, false otherwise. */ protected boolean selectionMapsToSingleRepository() { return getRepository() != null; } /** * @param event * @return the shell * @throws ExecutionException */ protected Shell getShell(ExecutionEvent event) throws ExecutionException { return HandlerUtil.getActiveShellChecked(event); } /** * @param event * @return the page * @throws ExecutionException */ protected IWorkbenchPage getPartPage(ExecutionEvent event) throws ExecutionException { return getPart(event).getSite().getPage(); } /** * @param event * @return the page * @throws ExecutionException */ protected IWorkbenchPart getPart(ExecutionEvent event) throws ExecutionException { return HandlerUtil.getActivePartChecked(event); } /** * * @param repository * the repository to check * @return {@code true} when {@link Constants#HEAD} can be resolved, * {@code false} otherwise */ protected boolean containsHead(Repository repository) { try { return repository != null ? repository.resolve(Constants.HEAD) != null : false; } catch (Exception e) { // do nothing } return false; } protected boolean isLocalBranchCheckedout(Repository repository) { try { String fullBranch = repository.getFullBranch(); return fullBranch != null && fullBranch.startsWith(Constants.R_HEADS); } catch (Exception e) { // do nothing } return false; } protected String getPreviousPath(Repository repository, ObjectReader reader, RevCommit headCommit, RevCommit previousCommit, String path) throws IOException { DiffEntry diffEntry = CompareCoreUtils.getChangeDiffEntry(repository, path, headCommit, previousCommit, reader); if (diffEntry != null) return diffEntry.getOldPath(); else return path; } protected RevCommit getHeadCommit(IResource resource) throws IOException { Repository repository = getRepository(); if (resource == null) { return null; } RepositoryMapping mapping = RepositoryMapping.getMapping(resource); if (mapping == null) { return null; } String path = mapping.getRepoRelativePath(resource); if (path == null) { return null; } try (RevWalk rw = new RevWalk(repository)) { rw.sort(RevSort.COMMIT_TIME_DESC, true); rw.sort(RevSort.BOUNDARY, true); if (path.length() > 0) { DiffConfig diffConfig = repository.getConfig().get( DiffConfig.KEY); FollowFilter filter = FollowFilter.create(path, diffConfig); rw.setTreeFilter(filter); } Ref head = repository.findRef(Constants.HEAD); if (head == null) { return null; } RevCommit headCommit = rw.parseCommit(head.getObjectId()); rw.close(); return headCommit; } } /** * Returns the previous commit of the given resources. * * @param resources * The {@link IResource} for which the previous commit shall be * determined. * @return The second to last commit which touched any of the given * resources. * @throws IOException * When the commit can not be parsed. */ protected List<RevCommit> findPreviousCommits( Collection<IResource> resources) throws IOException { List<RevCommit> result = new ArrayList<>(); Repository repository = getRepository(); RepositoryMapping mapping = RepositoryMapping.getMapping(resources .iterator().next() .getProject()); if (mapping == null) { return result; } try (RevWalk rw = new RevWalk(repository)) { rw.sort(RevSort.COMMIT_TIME_DESC, true); rw.sort(RevSort.BOUNDARY, true); List<TreeFilter> filters = new ArrayList<>(); DiffConfig diffConfig = repository.getConfig().get(DiffConfig.KEY); for (IResource resource : resources) { String path = mapping.getRepoRelativePath(resource); if (path != null && path.length() > 0) { filters.add(FollowFilter.create(path, diffConfig)); } } if (filters.size() >= 2) { TreeFilter filter = OrTreeFilter.create(filters); rw.setTreeFilter(filter); } else if (filters.size() == 1) { rw.setTreeFilter(filters.get(0)); } Ref head = repository.findRef(Constants.HEAD); if (head == null) { return result; } RevCommit headCommit = rw.parseCommit(head.getObjectId()); rw.markStart(headCommit); headCommit = rw.next(); if (headCommit == null) return result; List<RevCommit> directParents = Arrays.asList(headCommit .getParents()); RevCommit previousCommit = rw.next(); while (previousCommit != null && result.size() < directParents.size()) { if (directParents.contains(previousCommit)) { result.add(previousCommit); } previousCommit = rw.next(); } rw.dispose(); } return result; } // keep track of the path of an ancestor (for following renames) protected static final class PreviousCommit { final RevCommit commit; final String path; PreviousCommit(final RevCommit commit, final String path) { this.commit = commit; this.path = path; } } /** * By default egit operates only on resources that map to a project shared * with egit. For linked resources the project that contains the link * target, if any, must be shared with egit. * * @return the projects hosting the selected resources */ @Override public boolean isEnabled() { return getProjectsForSelectedResources().length > 0; } /** * Determines whether the enablement state shall always be recomputed or * only when the selection changes. This default implementation returns * {@code false}. * * @return {@code false} if the enablement state depends solely on the * selection, {@code true} if the enablement must be recomputed even * if the selection did not change. */ protected boolean alwaysCheckEnabled() { return false; } }