blob: 90761f16e0f3301f28a68db20e5769566047f4a1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2019 SAP AG and others.
*
* 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
*
* Contributors:
* Mathias Kinzler (SAP AG) - initial implementation
* Laurent Goubet <laurent.goubet@obeo.fr - 404121
*******************************************************************************/
package org.eclipse.egit.ui.internal.repository;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import org.eclipse.core.commands.State;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.egit.core.RepositoryCache;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.repository.tree.AdditionalRefNode;
import org.eclipse.egit.ui.internal.repository.tree.AdditionalRefsNode;
import org.eclipse.egit.ui.internal.repository.tree.BranchHierarchyNode;
import org.eclipse.egit.ui.internal.repository.tree.BranchesNode;
import org.eclipse.egit.ui.internal.repository.tree.ErrorNode;
import org.eclipse.egit.ui.internal.repository.tree.FetchNode;
import org.eclipse.egit.ui.internal.repository.tree.FileNode;
import org.eclipse.egit.ui.internal.repository.tree.FolderNode;
import org.eclipse.egit.ui.internal.repository.tree.LocalNode;
import org.eclipse.egit.ui.internal.repository.tree.PushNode;
import org.eclipse.egit.ui.internal.repository.tree.RefNode;
import org.eclipse.egit.ui.internal.repository.tree.RemoteNode;
import org.eclipse.egit.ui.internal.repository.tree.RemoteTrackingNode;
import org.eclipse.egit.ui.internal.repository.tree.RemotesNode;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryNode;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryTreeNode;
import org.eclipse.egit.ui.internal.repository.tree.StashNode;
import org.eclipse.egit.ui.internal.repository.tree.StashedCommitNode;
import org.eclipse.egit.ui.internal.repository.tree.SubmodulesNode;
import org.eclipse.egit.ui.internal.repository.tree.TagNode;
import org.eclipse.egit.ui.internal.repository.tree.TagsNode;
import org.eclipse.egit.ui.internal.repository.tree.WorkingDirNode;
import org.eclipse.egit.ui.internal.repository.tree.command.ToggleBranchHierarchyCommand;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RefsChangedEvent;
import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.RegistryToggleState;
/**
* Content Provider for the Git Repositories View
*/
public class RepositoriesViewContentProvider implements ITreeContentProvider {
private static final Object[] NO_CHILDREN = new Object[0];
private final RepositoryCache repositoryCache = org.eclipse.egit.core.Activator
.getDefault().getRepositoryCache();
private final State branchHierarchy;
private boolean showUnbornHead = false;
private Map<Repository, Map<String, Ref>> branchRefs = new WeakHashMap<>();
private Map<Repository, ListenerHandle> refsChangedListeners = new WeakHashMap<>();
/**
* Constructs a new {@link RepositoriesViewContentProvider} that doesn't
* show an unborn branch as HEAD.
*/
public RepositoriesViewContentProvider() {
this(false);
}
/**
* Constructs a new {@link RepositoriesViewContentProvider}.
*
* @param showUnbornHead
* whether to show HEAD even if it is an unborn branch
*/
public RepositoriesViewContentProvider(boolean showUnbornHead) {
super();
this.showUnbornHead = showUnbornHead;
ICommandService srv = CommonUtils.getService(PlatformUI.getWorkbench(), ICommandService.class);
branchHierarchy = srv.getCommand(ToggleBranchHierarchyCommand.ID)
.getState(RegistryToggleState.STATE_ID);
}
@Override
@SuppressWarnings("unchecked")
public Object[] getElements(Object inputElement) {
List<RepositoryTreeNode> nodes = new ArrayList<>();
List<String> directories = new ArrayList<>();
RepositoryUtil repositoryUtil = Activator.getDefault()
.getRepositoryUtil();
if (inputElement instanceof Collection) {
for (Object next : ((Collection) inputElement)) {
if (next instanceof RepositoryTreeNode) {
nodes.add((RepositoryTreeNode) next);
} else if (next instanceof String) {
directories.add((String) next);
}
}
} else if (inputElement instanceof IWorkspaceRoot) {
directories.addAll(repositoryUtil.getConfiguredRepositories());
}
for (String directory : directories) {
try {
File gitDir = new File(directory);
if (gitDir.exists()) {
RepositoryNode rNode = new RepositoryNode(null,
repositoryCache.lookupRepository(gitDir));
nodes.add(rNode);
} else
repositoryUtil.removeDir(gitDir);
} catch (IOException e) {
// ignore for now
}
}
Collections.sort(nodes);
return nodes.toArray();
}
@Override
public void dispose() {
refsChangedListeners.values().forEach(ListenerHandle::remove);
refsChangedListeners.clear();
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// nothing
}
@Override
public Object[] getChildren(Object parentElement) {
RepositoryTreeNode node = (RepositoryTreeNode) parentElement;
Repository repo = node.getRepository();
switch (node.getType()) {
case BRANCHES: {
List<RepositoryTreeNode> nodes = new ArrayList<>();
nodes.add(new LocalNode(node, repo));
nodes.add(new RemoteTrackingNode(node, repo));
return nodes.toArray();
}
case LOCAL:
return getBranchChildren(node, repo, Constants.R_HEADS);
case REMOTETRACKING:
return getBranchChildren(node, repo, Constants.R_REMOTES);
case BRANCHHIERARCHY:
return getBranchHierarchyChildren(node, repo,
((BranchHierarchyNode) node).getObject()
.toPortableString());
case TAGS:
return getTagsChildren(node, repo);
case ADDITIONALREFS: {
List<RepositoryTreeNode<Ref>> refs = new ArrayList<>();
try {
for (Entry<String, Ref> refEntry : getRefs(repo, RefDatabase.ALL).entrySet()) {
String name=refEntry.getKey();
if (!(name.startsWith(Constants.R_HEADS) || name.startsWith(Constants.R_TAGS)|| name.startsWith(Constants.R_REMOTES)))
refs.add(new AdditionalRefNode(node, repo, refEntry
.getValue()));
}
for (Ref r : repo.getRefDatabase().getAdditionalRefs()) {
refs.add(new AdditionalRefNode(node, repo, r));
}
if (showUnbornHead) {
Ref head = repo.exactRef(Constants.HEAD);
if (head != null && head.isSymbolic()
&& head.getObjectId() == null) {
refs.add(new AdditionalRefNode(node, repo, head));
}
}
} catch (Exception e) {
return handleException(e, node);
}
return refs.toArray();
}
case REMOTES: {
List<RepositoryTreeNode<String>> remotes = new ArrayList<>();
Repository rep = node.getRepository();
Set<String> configNames = rep.getConfig().getSubsections(
RepositoriesView.REMOTE);
for (String configName : configNames) {
remotes.add(new RemoteNode(node, repo, configName));
}
return remotes.toArray();
}
case REPO: {
List<RepositoryTreeNode<? extends Object>> nodeList = new ArrayList<>();
nodeList.add(new BranchesNode(node, repo));
nodeList.add(new TagsNode(node, repo));
nodeList.add(new AdditionalRefsNode(node, repo));
final boolean bare = repo.isBare();
if (!bare)
nodeList.add(new WorkingDirNode(node, repo));
nodeList.add(new RemotesNode(node, repo));
if(!bare && hasStashedCommits(repo))
nodeList.add(new StashNode(node, repo));
if (!bare && hasConfiguredSubmodules(repo))
nodeList.add(new SubmodulesNode(node, repo));
return nodeList.toArray();
}
case WORKINGDIR:
if (repo.isBare()) {
return NO_CHILDREN;
}
return getDirectoryChildren(node, repo.getWorkTree());
case FOLDER:
return getDirectoryChildren(node, (File) node.getObject());
case REMOTE: {
List<RepositoryTreeNode<String>> children = new ArrayList<>();
String remoteName = (String) node.getObject();
RemoteConfig rc;
try {
rc = new RemoteConfig(node.getRepository().getConfig(),
remoteName);
} catch (URISyntaxException e) {
return handleException(e, node);
}
if (!rc.getURIs().isEmpty())
children.add(new FetchNode(node, node.getRepository(), rc
.getURIs().get(0).toPrivateString()));
int uriCount = rc.getPushURIs().size();
if (uriCount == 0 && !rc.getURIs().isEmpty())
uriCount++;
// show push if either a fetch or push URI is specified and
// at least one push specification
if (uriCount > 0) {
URIish firstUri;
if (!rc.getPushURIs().isEmpty())
firstUri = rc.getPushURIs().get(0);
else
firstUri = rc.getURIs().get(0);
if (uriCount == 1)
children.add(new PushNode(node, node.getRepository(),
firstUri.toPrivateString()));
else
children.add(new PushNode(node, node.getRepository(),
firstUri.toPrivateString() + "...")); //$NON-NLS-1$
}
return children.toArray();
}
case SUBMODULES:
List<RepositoryNode> children = new ArrayList<>();
try (SubmoduleWalk walk = SubmoduleWalk
.forIndex(node.getRepository())) {
while (walk.next()) {
Repository subRepo = walk.getRepository();
if (subRepo != null) {
Repository cachedRepo = null;
try {
cachedRepo = repositoryCache
.lookupRepository(subRepo.getDirectory());
} finally {
subRepo.close();
}
if (cachedRepo != null)
children.add(new RepositoryNode(node, cachedRepo));
}
}
} catch (IOException e) {
handleException(e, node);
}
return children.toArray();
case STASH:
List<StashedCommitNode> stashNodes = new ArrayList<>();
int index = 0;
try {
for (RevCommit commit : Git.wrap(repo).stashList().call())
stashNodes.add(new StashedCommitNode(node, repo, index++,
commit));
} catch (Exception e) {
handleException(e, node);
}
return stashNodes.toArray();
case FILE:
// fall through
case REF:
// fall through
case PUSH:
// fall through
case TAG:
// fall through
case FETCH:
// fall through
case ERROR:
// fall through
case STASHED_COMMIT:
// fall through
case ADDITIONALREF:
return null;
}
return null;
}
private Object[] getBranchChildren(RepositoryTreeNode node, Repository repo,
String prefix) {
if (isHierarchical()) {
return getBranchHierarchyChildren(node, repo, prefix);
} else {
try {
return getRefs(repo, prefix).values().stream()
.filter(ref -> !ref.isSymbolic())
.map(ref -> new RefNode(node, repo, ref)).toArray();
} catch (IOException e) {
return handleException(e, node);
}
}
}
private Object[] getBranchHierarchyChildren(RepositoryTreeNode node,
Repository repo, String prefix) {
try {
Set<String> folderChildren = new HashSet<>();
return getRefs(repo, prefix).entrySet().stream()
.filter(e -> !e.getValue().isSymbolic()).map(e -> {
int i = e.getKey().indexOf('/', prefix.length());
if (i < 0) {
return new RefNode(node, repo, e.getValue());
} else {
String name = e.getKey().substring(prefix.length(),
i);
if (folderChildren.add(name)) {
return new BranchHierarchyNode(node, repo,
Path.fromPortableString(prefix + name));
}
return null;
}
}).filter(Objects::nonNull).toArray();
} catch (IOException e) {
return handleException(e, node);
}
}
private Object[] getDirectoryChildren(RepositoryTreeNode parentNode,
File parent) {
Repository repo = parentNode.getRepository();
List<RepositoryTreeNode<File>> children = new ArrayList<>();
try {
Files.walkFileTree(parent.toPath(),
EnumSet.noneOf(FileVisitOption.class), 1,
new SimpleFileVisitor<java.nio.file.Path>() {
@Override
public FileVisitResult visitFile(
java.nio.file.Path file,
BasicFileAttributes attrs) throws IOException {
if (attrs.isDirectory()) {
children.add(new FolderNode(parentNode, repo,
file.toFile()));
} else {
children.add(new FileNode(parentNode, repo,
file.toFile()));
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(
java.nio.file.Path file, IOException exc)
throws IOException {
// Just ignore it
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
// Ignore
}
return children.toArray();
}
private Object[] getTagsChildren(RepositoryTreeNode parentNode,
Repository repo) {
List<RepositoryTreeNode<Ref>> nodes = new ArrayList<>();
try (RevWalk walk = new RevWalk(repo)) {
walk.setRetainBody(true);
for (Ref tagRef : getRefs(repo, Constants.R_TAGS).values()) {
ObjectId objectId = tagRef.getLeaf().getObjectId();
RevObject revObject = walk.parseAny(objectId);
RevObject peeledObject = walk.peel(revObject);
TagNode tagNode = createTagNode(parentNode, repo, tagRef,
revObject, peeledObject);
nodes.add(tagNode);
}
} catch (IOException e) {
return handleException(e, parentNode);
}
return nodes.toArray();
}
private TagNode createTagNode(RepositoryTreeNode parentNode,
Repository repo, Ref ref, RevObject revObject,
RevObject peeledObject) {
boolean annotated = (revObject instanceof RevTag);
if (peeledObject instanceof RevCommit) {
RevCommit commit = (RevCommit) peeledObject;
String id = commit.getId().name();
String message = commit.getShortMessage();
return new TagNode(parentNode, repo, ref, annotated, id, message);
} else {
return new TagNode(parentNode, repo, ref, annotated, "", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
private Object[] handleException(Exception e, RepositoryTreeNode parentNode) {
Activator.handleError(e.getMessage(), e, false);
// add a node indicating that there was an Exception
String message = e.getMessage();
if (message == null)
return new Object[] { new ErrorNode(parentNode, parentNode
.getRepository(),
UIText.RepositoriesViewContentProvider_ExceptionNodeText) };
else
return new Object[] { new ErrorNode(parentNode, parentNode
.getRepository(), message) };
}
@Override
public Object getParent(Object element) {
if (element instanceof RepositoryTreeNode)
return ((RepositoryTreeNode) element).getParent();
return null;
}
@Override
public boolean hasChildren(Object element) {
// for some of the nodes we can optimize this call
RepositoryTreeNode node = (RepositoryTreeNode) element;
Repository repo = node.getRepository();
switch (node.getType()) {
case BRANCHES:
case REPO:
case ADDITIONALREFS:
case SUBMODULES:
return true;
case TAGS:
return hasTagsChildren(repo);
case WORKINGDIR:
return !repo.isBare() && hasDirectoryChildren(repo.getWorkTree());
case FOLDER:
return !repo.isBare()
&& hasDirectoryChildren((File) node.getObject());
case FILE:
return false;
default:
Object[] children = getChildren(element);
return children != null && children.length > 0;
}
}
private boolean hasDirectoryChildren(File file) {
try (DirectoryStream<java.nio.file.Path> dir = Files
.newDirectoryStream(file.toPath())) {
return dir.iterator().hasNext();
} catch (DirectoryIteratorException | IOException e) {
return false;
}
}
/**
* As long as the ref database has not been read, assume there are tags, and
* start reading the database in the background. This should avoid long
* blocking during startup.
*
* @param repo
* @return whether the tags node has children.
*/
private boolean hasTagsChildren(Repository repo) {
try {
if (branchRefs.get(repo) == null) {
WorkspaceJob job = new WorkspaceJob(
UIText.RepositoriesViewContentProvider_ReadReferencesJob) {
@Override
public IStatus runInWorkspace(IProgressMonitor monitor)
throws CoreException {
try {
// trigger reading the reference database
getRefs(repo, Constants.R_TAGS);
} catch (IOException e) {
return Status.CANCEL_STATUS;
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
return true;
}
return !getRefs(repo, Constants.R_TAGS).isEmpty();
} catch (IOException e) {
return true;
}
}
private synchronized Map<String, Ref> getRefs(final Repository repo, final String prefix) throws IOException {
Map<String, Ref> allRefs = branchRefs.get(repo);
if (allRefs == null) {
allRefs = repo.getRefDatabase().getRefs(RefDatabase.ALL);
branchRefs.put(repo, allRefs);
if (refsChangedListeners.get(repo) == null) {
RefsChangedListener listener = new RefsChangedListener() {
@Override
public void onRefsChanged(RefsChangedEvent event) {
synchronized (RepositoriesViewContentProvider.this) {
branchRefs.remove(repo);
}
}
};
refsChangedListeners.put(repo, repo.getListenerList()
.addRefsChangedListener(listener));
}
}
if (prefix.equals(RefDatabase.ALL))
return allRefs;
Map<String, Ref> filtered = new HashMap<>();
for (Map.Entry<String, Ref> entry : allRefs.entrySet()) {
if (entry.getKey().startsWith(prefix))
filtered.put(entry.getKey(), entry.getValue());
}
return filtered;
}
/**
* Does the repository have any submodule configurations?
* <p>
* This method checks for a '.gitmodules' file at the root of the working
* directory or any 'submodule' sections in the repository's config file
*
* @param repository
* @return true if submodules, false otherwise
*/
private boolean hasConfiguredSubmodules(final Repository repository) {
if (new File(repository.getWorkTree(), Constants.DOT_GIT_MODULES)
.isFile())
return true;
return !repository.getConfig()
.getSubsections(ConfigConstants.CONFIG_SUBMODULE_SECTION)
.isEmpty();
}
/**
* Does the repository have any stashed commits?
* <p>
* This method checks for a {@link Constants#R_STASH} ref in the given
* repository
*
* @param repository
* @return true if stashed commits, false otherwise
*/
private boolean hasStashedCommits(final Repository repository) {
try {
return repository.exactRef(Constants.R_STASH) != null;
} catch (IOException e) {
return false;
}
}
/**
* Tells whether this content provider is using a hierarchical branch
* layout.
*
* @return {@code true} if this content provider uses a hierarchical branch
* layout; {@code false} otherwise
*/
public boolean isHierarchical() {
return ((Boolean) branchHierarchy.getValue()).booleanValue();
}
}