// Copyright (C) 2009 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.server.project; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.common.WebLinkInfo; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.StringUtil; import com.google.gerrit.server.WebLinks; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.util.TreeFormatter; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; /** List projects visible to the calling user. */ public class ListProjects implements RestReadView<TopLevelResource> { private static final Logger log = LoggerFactory.getLogger(ListProjects.class); public static enum FilterType { CODE { @Override boolean matches(Repository git) throws IOException { return !PERMISSIONS.matches(git); } }, PARENT_CANDIDATES { @Override boolean matches(Repository git) { return true; } }, PERMISSIONS { @Override boolean matches(Repository git) throws IOException { Ref head = git.getRef(Constants.HEAD); return head != null && head.isSymbolic() && RefNames.REFS_CONFIG.equals(head.getLeaf().getName()); } }, ALL { @Override boolean matches(Repository git) { return true; } }; abstract boolean matches(Repository git) throws IOException; } private final CurrentUser currentUser; private final ProjectCache projectCache; private final GroupCache groupCache; private final GroupControl.Factory groupControlFactory; private final GitRepositoryManager repoManager; private final ProjectNode.Factory projectNodeFactory; private final Provider<WebLinks> webLinks; @Deprecated @Option(name = "--format", usage = "(deprecated) output format") private OutputFormat format = OutputFormat.TEXT; @Option(name = "--show-branch", aliases = {"-b"}, usage = "displays the sha of each project in the specified branch") public void addShowBranch(String branch) { showBranch.add(branch); } @Option(name = "--tree", aliases = {"-t"}, usage = "displays project inheritance in a tree-like format\n" + "this option does not work together with the show-branch option") public void setShowTree(boolean showTree) { this.showTree = showTree; } @Option(name = "--type", usage = "type of project") public void setFilterType(FilterType type) { this.type = type; } @Option(name = "--description", aliases = {"-d"}, usage = "include description of project in list") public void setShowDescription(boolean showDescription) { this.showDescription = showDescription; } @Option(name = "--all", usage = "display all projects that are accessible by the calling user") public void setAll(boolean all) { this.all = all; } @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list") public void setLimit(int limit) { this.limit = limit; } @Option(name = "-S", metaVar = "CNT", usage = "number of projects to skip") public void setStart(int start) { this.start = start; } @Option(name = "-p", metaVar = "PREFIX", usage = "match project prefix") public void setMatchPrefix(String matchPrefix) { this.matchPrefix = matchPrefix; } @Option(name = "-m", metaVar = "MATCH", usage = "match project substring") public void setMatchSubstring(String matchSubstring) { this.matchSubstring = matchSubstring; } @Option(name = "--has-acl-for", metaVar = "GROUP", usage = "displays only projects on which access rights for this group are directly assigned") public void setGroupUuid(AccountGroup.UUID groupUuid) { this.groupUuid = groupUuid; } private final List<String> showBranch = Lists.newArrayList(); private boolean showTree; private FilterType type = FilterType.CODE; private boolean showDescription; private boolean all; private int limit; private int start; private String matchPrefix; private String matchSubstring; private AccountGroup.UUID groupUuid; @Inject protected ListProjects(CurrentUser currentUser, ProjectCache projectCache, GroupCache groupCache, GroupControl.Factory groupControlFactory, GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory, Provider<WebLinks> webLinks) { this.currentUser = currentUser; this.projectCache = projectCache; this.groupCache = groupCache; this.groupControlFactory = groupControlFactory; this.repoManager = repoManager; this.projectNodeFactory = projectNodeFactory; this.webLinks = webLinks; } public List<String> getShowBranch() { return showBranch; } public boolean isShowTree() { return showTree; } public boolean isShowDescription() { return showDescription; } public OutputFormat getFormat() { return format; } public ListProjects setFormat(OutputFormat fmt) { format = fmt; return this; } @Override public Object apply(TopLevelResource resource) { if (format == OutputFormat.TEXT) { ByteArrayOutputStream buf = new ByteArrayOutputStream(); display(buf); return BinaryResult.create(buf.toByteArray()) .setContentType("text/plain") .setCharacterEncoding("UTF-8"); } return apply(); } public Map<String, ProjectInfo> apply() { format = OutputFormat.JSON; return display(null); } public Map<String, ProjectInfo> display(OutputStream displayOutputStream) { PrintWriter stdout = null; if (displayOutputStream != null) { try { stdout = new PrintWriter(new BufferedWriter( new OutputStreamWriter(displayOutputStream, "UTF-8"))); } catch (UnsupportedEncodingException e) { throw new RuntimeException("JVM lacks UTF-8 encoding", e); } } int foundIndex = 0; int found = 0; Map<String, ProjectInfo> output = Maps.newTreeMap(); Map<String, String> hiddenNames = Maps.newHashMap(); Set<String> rejected = new HashSet<>(); final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>(); try { for (final Project.NameKey projectName : scan()) { final ProjectState e = projectCache.get(projectName); if (e == null) { // If we can't get it from the cache, pretend its not present. // continue; } final ProjectControl pctl = e.controlFor(currentUser); if (groupUuid != null) { try { if (!groupControlFactory.controlFor(groupUuid).isVisible()) { break; } } catch (NoSuchGroupException ex) { break; } if (!pctl.getLocalGroups().contains( GroupReference.forGroup(groupCache.get(groupUuid)))) { continue; } } ProjectInfo info = new ProjectInfo(); if (type == FilterType.PARENT_CANDIDATES) { ProjectState parentState = Iterables.getFirst(e.parents(), null); if (parentState != null && !output.keySet().contains(parentState.getProject().getName()) && !rejected.contains(parentState.getProject().getName())) { ProjectControl parentCtrl = parentState.controlFor(currentUser); if (parentCtrl.isVisible() || parentCtrl.isOwner()) { info.name = parentState.getProject().getName(); info.description = Strings.emptyToNull( parentState.getProject().getDescription()); info.state = parentState.getProject().getState(); } else { rejected.add(parentState.getProject().getName()); continue; } } else { continue; } } else { final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner()); if (showTree && !format.isJson()) { treeMap.put(projectName, projectNodeFactory.create(pctl.getProject(), isVisible)); continue; } if (!isVisible && !(showTree && pctl.isOwner())) { // Require the project itself to be visible to the user. // continue; } info.name = projectName.get(); if (showTree && format.isJson()) { ProjectState parent = Iterables.getFirst(e.parents(), null); if (parent != null) { ProjectControl parentCtrl = parent.controlFor(currentUser); if (parentCtrl.isVisible() || parentCtrl.isOwner()) { info.parent = parent.getProject().getName(); } else { info.parent = hiddenNames.get(parent.getProject().getName()); if (info.parent == null) { info.parent = "?-" + (hiddenNames.size() + 1); hiddenNames.put(parent.getProject().getName(), info.parent); } } } } if (showDescription) { info.description = Strings.emptyToNull(e.getProject().getDescription()); } info.state = e.getProject().getState(); try { if (!showBranch.isEmpty()) { Repository git = repoManager.openRepository(projectName); try { if (!type.matches(git)) { continue; } List<Ref> refs = getBranchRefs(projectName, pctl); if (!hasValidRef(refs)) { continue; } for (int i = 0; i < showBranch.size(); i++) { Ref ref = refs.get(i); if (ref != null && ref.getObjectId() != null) { if (info.branches == null) { info.branches = Maps.newLinkedHashMap(); } info.branches.put(showBranch.get(i), ref.getObjectId().name()); } } } finally { git.close(); } } else if (!showTree && type != FilterType.ALL) { Repository git = repoManager.openRepository(projectName); try { if (!type.matches(git)) { continue; } } finally { git.close(); } } } catch (RepositoryNotFoundException err) { // If the Git repository is gone, the project doesn't actually exist anymore. continue; } catch (IOException err) { log.warn("Unexpected error reading " + projectName, err); continue; } info.webLinks = Lists.newArrayList(); for (WebLinks.Link link : webLinks.get().getProjectLinks(projectName.get())) { if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) { info.webLinks.add(new WebLinkInfo(link.name, link.url)); } } } if (foundIndex++ < start) { continue; } if (limit > 0 && ++found > limit) { break; } if (stdout == null || format.isJson()) { output.put(info.name, info); continue; } if (!showBranch.isEmpty()) { for (String name : showBranch) { String ref = info.branches != null ? info.branches.get(name) : null; if (ref == null) { // Print stub (forty '-' symbols) ref = "----------------------------------------"; } stdout.print(ref); stdout.print(' '); } } stdout.print(info.name); if (info.description != null) { // We still want to list every project as one-liners, hence escaping \n. stdout.print(" - " + StringUtil.escapeString(info.description)); } stdout.print('\n'); } for (ProjectInfo info : output.values()) { info.id = Url.encode(info.name); info.name = null; } if (stdout == null) { return output; } else if (format.isJson()) { format.newGson().toJson( output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout); stdout.print('\n'); } else if (showTree && treeMap.size() > 0) { printProjectTree(stdout, treeMap); } return null; } finally { if (stdout != null) { stdout.flush(); } } } private Iterable<Project.NameKey> scan() { if (matchPrefix != null) { return projectCache.byName(matchPrefix); } else if (matchSubstring != null) { return Iterables.filter(projectCache.all(), new Predicate<Project.NameKey>() { public boolean apply(Project.NameKey in) { return in.get().toLowerCase(Locale.US) .contains(matchSubstring.toLowerCase(Locale.US)); } }); } else { return projectCache.all(); } } private void printProjectTree(final PrintWriter stdout, final TreeMap<Project.NameKey, ProjectNode> treeMap) { final SortedSet<ProjectNode> sortedNodes = new TreeSet<>(); // Builds the inheritance tree using a list. // for (final ProjectNode key : treeMap.values()) { if (key.isAllProjects()) { sortedNodes.add(key); continue; } ProjectNode node = treeMap.get(key.getParentName()); if (node != null) { node.addChild(key); } else { sortedNodes.add(key); } } final TreeFormatter treeFormatter = new TreeFormatter(stdout); treeFormatter.printTree(sortedNodes); stdout.flush(); } private List<Ref> getBranchRefs(Project.NameKey projectName, ProjectControl projectControl) { Ref[] result = new Ref[showBranch.size()]; try { Repository git = repoManager.openRepository(projectName); try { for (int i = 0; i < showBranch.size(); i++) { Ref ref = git.getRef(showBranch.get(i)); if (ref != null && ref.getObjectId() != null && (projectControl.controlForRef(ref.getLeaf().getName()).isVisible()) || (all && projectControl.isOwner())) { result[i] = ref; } } } finally { git.close(); } } catch (IOException ioe) { // Fall through and return what is available. } return Arrays.asList(result); } private static boolean hasValidRef(List<Ref> refs) { for (Ref ref : refs) { if (ref != null) { return true; } } return false; } }