/*******************************************************************************
* Copyright (c) 2010, 2013 SAP AG 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
*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import org.eclipse.core.commands.IStateListener;
import org.eclipse.core.commands.State;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
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.StashedCommitNode;
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.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;
/**
* Content Provider for the Git Repositories View
*/
public class RepositoriesViewContentProvider implements ITreeContentProvider,
IStateListener {
private final RepositoryCache repositoryCache = org.eclipse.egit.core.Activator
.getDefault().getRepositoryCache();
private final State commandState;
private boolean branchHierarchyMode = false;
private Map<Repository, Map<String, Ref>> branchRefs = new WeakHashMap<>();
private Map<Repository, ListenerHandle> refsChangedListeners = new WeakHashMap<>();
/**
* Constructs this instance
*/
public RepositoriesViewContentProvider() {
ICommandService srv = CommonUtils.getService(PlatformUI.getWorkbench(), ICommandService.class);
commandState = srv.getCommand(
ToggleBranchHierarchyCommand.ID)
.getState(ToggleBranchHierarchyCommand.TOGGLE_STATE);
commandState.addListener(this);
try {
this.branchHierarchyMode = ((Boolean) commandState.getValue())
.booleanValue();
} catch (Exception e) {
Activator.handleError(e.getMessage(), e, false);
}
}
@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 (Iterator it = ((Collection) inputElement).iterator(); it
.hasNext();) {
Object next = it.next();
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() {
commandState.removeListener(this);
for (ListenerHandle handle : refsChangedListeners.values())
handle.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: {
if (branchHierarchyMode) {
BranchHierarchyNode hierNode = new BranchHierarchyNode(node,
repo, new Path(Constants.R_HEADS));
List<RepositoryTreeNode> children = new ArrayList<>();
try {
for (IPath path : hierNode.getChildPaths()) {
children.add(new BranchHierarchyNode(node, node
.getRepository(), path));
}
for (Ref ref : hierNode.getChildRefs()) {
children.add(new RefNode(node, node.getRepository(),
ref));
}
} catch (Exception e) {
return handleException(e, node);
}
return children.toArray();
} else {
List<RepositoryTreeNode<Ref>> refs = new ArrayList<>();
try {
for (Entry<String, Ref> refEntry : getRefs(repo, Constants.R_HEADS).entrySet()) {
if (!refEntry.getValue().isSymbolic())
refs.add(new RefNode(node, repo, refEntry
.getValue()));
}
} catch (Exception e) {
return handleException(e, node);
}
return refs.toArray();
}
}
case REMOTETRACKING: {
if (branchHierarchyMode) {
BranchHierarchyNode hierNode = new BranchHierarchyNode(node,
repo, new Path(Constants.R_REMOTES));
List<RepositoryTreeNode> children = new ArrayList<>();
try {
for (IPath path : hierNode.getChildPaths()) {
children.add(new BranchHierarchyNode(node, node
.getRepository(), path));
}
for (Ref ref : hierNode.getChildRefs()) {
children.add(new RefNode(node, node.getRepository(),
ref));
}
} catch (Exception e) {
return handleException(e, node);
}
return children.toArray();
} else {
List<RepositoryTreeNode<Ref>> refs = new ArrayList<>();
try {
for (Entry<String, Ref> refEntry : getRefs(repo, Constants.R_REMOTES).entrySet()) {
if (!refEntry.getValue().isSymbolic())
refs.add(new RefNode(node, repo, refEntry
.getValue()));
}
} catch (Exception e) {
return handleException(e, node);
}
return refs.toArray();
}
}
case BRANCHHIERARCHY: {
BranchHierarchyNode hierNode = (BranchHierarchyNode) node;
List<RepositoryTreeNode> children = new ArrayList<>();
try {
for (IPath path : hierNode.getChildPaths()) {
children.add(new BranchHierarchyNode(node, node
.getRepository(), path));
}
for (Ref ref : hierNode.getChildRefs()) {
children.add(new RefNode(node, node.getRepository(), ref));
}
} catch (IOException e) {
return handleException(e, node);
}
return children.toArray();
}
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));
} 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: {
List<RepositoryTreeNode<File>> children = new ArrayList<>();
if (node.getRepository().isBare())
return children.toArray();
File workingDir = repo.getWorkTree();
if (!workingDir.exists())
return children.toArray();
File[] childFiles = workingDir.listFiles();
Arrays.sort(childFiles, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if (o1.isDirectory()) {
if (o2.isDirectory()) {
return o1.compareTo(o2);
}
return -1;
} else if (o2.isDirectory()) {
return 1;
}
return o1.compareTo(o2);
}
});
for (File file : childFiles) {
if (file.isDirectory()) {
children.add(new FolderNode(node, repo, file));
} else {
children.add(new FileNode(node, repo, file));
}
}
return children.toArray();
}
case FOLDER: {
List<RepositoryTreeNode<File>> children = new ArrayList<>();
File parent = ((File) node.getObject());
File[] childFiles = parent.listFiles();
if (childFiles == null)
return children.toArray();
Arrays.sort(childFiles, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if (o1.isDirectory()) {
if (o2.isDirectory()) {
return o1.compareTo(o2);
}
return -1;
} else if (o2.isDirectory()) {
return 1;
}
return o1.compareTo(o2);
}
});
for (File file : childFiles) {
if (file.isDirectory()) {
children.add(new FolderNode(node, repo, file));
} else {
children.add(new FileNode(node, repo, file));
}
}
return children.toArray();
}
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[] getTagsChildren(RepositoryTreeNode parentNode,
Repository repo) {
List<RepositoryTreeNode<Ref>> nodes = new ArrayList<>();
try (RevWalk walk = new RevWalk(repo)) {
walk.setRetainBody(true);
Map<String, Ref> tagRefs = getRefs(repo, Constants.R_TAGS);
for (Ref tagRef : tagRefs.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:
return true;
case REPO:
return true;
case ADDITIONALREFS:
return true;
case SUBMODULES:
return true;
case TAGS:
try {
return !getRefs(repo, Constants.R_TAGS).isEmpty();
} catch (IOException e) {
return true;
}
case WORKINGDIR:
if (node.getRepository().isBare())
return false;
File workingDir = repo.getWorkTree();
if (!workingDir.exists())
return false;
return workingDir.listFiles().length > 0;
default:
Object[] children = getChildren(element);
return children != null && children.length > 0;
}
}
@Override
public void handleStateChange(State state, Object oldValue) {
try {
this.branchHierarchyMode = ((Boolean) state.getValue())
.booleanValue();
} catch (Exception e) {
Activator.handleError(e.getMessage(), e, false);
}
}
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 branchHierarchyMode;
}
}