LCOV - code coverage report
Current view: top level - server/restapi/project - ListProjects.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 221 300 73.7 %
Date: 2022-11-19 15:00:39 Functions: 49 58 84.5 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.restapi.project;
      16             : 
      17             : import static com.google.common.base.Strings.emptyToNull;
      18             : import static com.google.common.base.Strings.isNullOrEmpty;
      19             : import static com.google.common.collect.Ordering.natural;
      20             : import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
      21             : import static java.nio.charset.StandardCharsets.UTF_8;
      22             : 
      23             : import com.google.common.base.Joiner;
      24             : import com.google.common.collect.ImmutableList;
      25             : import com.google.common.collect.ImmutableSortedMap;
      26             : import com.google.common.collect.Iterables;
      27             : import com.google.common.flogger.FluentLogger;
      28             : import com.google.gerrit.common.Nullable;
      29             : import com.google.gerrit.entities.AccountGroup;
      30             : import com.google.gerrit.entities.GroupReference;
      31             : import com.google.gerrit.entities.Project;
      32             : import com.google.gerrit.entities.RefNames;
      33             : import com.google.gerrit.exceptions.NoSuchGroupException;
      34             : import com.google.gerrit.exceptions.StorageException;
      35             : import com.google.gerrit.extensions.common.ProjectInfo;
      36             : import com.google.gerrit.extensions.common.WebLinkInfo;
      37             : import com.google.gerrit.extensions.restapi.AuthException;
      38             : import com.google.gerrit.extensions.restapi.BadRequestException;
      39             : import com.google.gerrit.extensions.restapi.BinaryResult;
      40             : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
      41             : import com.google.gerrit.extensions.restapi.Response;
      42             : import com.google.gerrit.extensions.restapi.RestReadView;
      43             : import com.google.gerrit.extensions.restapi.TopLevelResource;
      44             : import com.google.gerrit.extensions.restapi.Url;
      45             : import com.google.gerrit.json.OutputFormat;
      46             : import com.google.gerrit.server.CurrentUser;
      47             : import com.google.gerrit.server.WebLinks;
      48             : import com.google.gerrit.server.account.GroupControl;
      49             : import com.google.gerrit.server.config.GerritServerConfig;
      50             : import com.google.gerrit.server.git.GitRepositoryManager;
      51             : import com.google.gerrit.server.group.GroupResolver;
      52             : import com.google.gerrit.server.ioutil.RegexListSearcher;
      53             : import com.google.gerrit.server.ioutil.StringUtil;
      54             : import com.google.gerrit.server.permissions.PermissionBackend;
      55             : import com.google.gerrit.server.permissions.PermissionBackendException;
      56             : import com.google.gerrit.server.permissions.ProjectPermission;
      57             : import com.google.gerrit.server.permissions.RefPermission;
      58             : import com.google.gerrit.server.project.ProjectCache;
      59             : import com.google.gerrit.server.project.ProjectState;
      60             : import com.google.gerrit.server.util.TreeFormatter;
      61             : import com.google.gson.reflect.TypeToken;
      62             : import com.google.inject.Inject;
      63             : import com.google.inject.Provider;
      64             : import java.io.BufferedWriter;
      65             : import java.io.ByteArrayOutputStream;
      66             : import java.io.IOException;
      67             : import java.io.OutputStream;
      68             : import java.io.OutputStreamWriter;
      69             : import java.io.PrintWriter;
      70             : import java.util.ArrayList;
      71             : import java.util.Arrays;
      72             : import java.util.Collections;
      73             : import java.util.HashMap;
      74             : import java.util.Iterator;
      75             : import java.util.LinkedHashMap;
      76             : import java.util.List;
      77             : import java.util.Locale;
      78             : import java.util.Map;
      79             : import java.util.NavigableSet;
      80             : import java.util.Optional;
      81             : import java.util.SortedMap;
      82             : import java.util.TreeMap;
      83             : import java.util.TreeSet;
      84             : import java.util.stream.Stream;
      85             : import java.util.stream.StreamSupport;
      86             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      87             : import org.eclipse.jgit.lib.Config;
      88             : import org.eclipse.jgit.lib.Constants;
      89             : import org.eclipse.jgit.lib.Ref;
      90             : import org.eclipse.jgit.lib.Repository;
      91             : import org.kohsuke.args4j.Option;
      92             : 
      93             : /**
      94             :  * List projects visible to the calling user.
      95             :  *
      96             :  * <p>Implement {@code GET /projects/}, without a {@code query=} parameter.
      97             :  */
      98             : public class ListProjects implements RestReadView<TopLevelResource> {
      99           7 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     100             : 
     101           7 :   public enum FilterType {
     102           7 :     CODE {
     103             :       @Override
     104             :       boolean matches(Repository git) throws IOException {
     105           0 :         return !PERMISSIONS.matches(git);
     106             :       }
     107             : 
     108             :       @Override
     109             :       boolean useMatch() {
     110           0 :         return true;
     111             :       }
     112             :     },
     113           7 :     PERMISSIONS {
     114             :       @Override
     115             :       boolean matches(Repository git) throws IOException {
     116           1 :         Ref head = git.getRefDatabase().exactRef(Constants.HEAD);
     117           1 :         return head != null
     118           1 :             && head.isSymbolic()
     119           1 :             && RefNames.REFS_CONFIG.equals(head.getLeaf().getName());
     120             :       }
     121             : 
     122             :       @Override
     123             :       boolean useMatch() {
     124           1 :         return true;
     125             :       }
     126             :     },
     127           7 :     ALL {
     128             :       @Override
     129             :       boolean matches(Repository git) {
     130           1 :         return true;
     131             :       }
     132             : 
     133             :       @Override
     134             :       boolean useMatch() {
     135           7 :         return false;
     136             :       }
     137             :     };
     138             : 
     139             :     abstract boolean matches(Repository git) throws IOException;
     140             : 
     141             :     abstract boolean useMatch();
     142             :   }
     143             : 
     144             :   private final CurrentUser currentUser;
     145             :   private final ProjectCache projectCache;
     146             :   private final GroupResolver groupResolver;
     147             :   private final GroupControl.Factory groupControlFactory;
     148             :   private final GitRepositoryManager repoManager;
     149             :   private final PermissionBackend permissionBackend;
     150             :   private final ProjectNode.Factory projectNodeFactory;
     151             :   private final WebLinks webLinks;
     152             : 
     153           7 :   @Deprecated
     154             :   @Option(name = "--format", usage = "(deprecated) output format")
     155             :   private OutputFormat format = OutputFormat.TEXT;
     156             : 
     157             :   @Option(
     158             :       name = "--show-branch",
     159             :       aliases = {"-b"},
     160             :       usage = "displays the sha of each project in the specified branch")
     161             :   public void addShowBranch(String branch) {
     162           1 :     showBranch.add(branch);
     163           1 :   }
     164             : 
     165             :   @Option(
     166             :       name = "--tree",
     167             :       aliases = {"-t"},
     168             :       usage =
     169             :           "displays project inheritance in a tree-like format\n"
     170             :               + "this option does not work together with the show-branch option")
     171             :   public void setShowTree(boolean showTree) {
     172           3 :     this.showTree = showTree;
     173           3 :   }
     174             : 
     175             :   @Option(name = "--type", usage = "type of project")
     176             :   public void setFilterType(FilterType type) {
     177           3 :     this.type = type;
     178           3 :   }
     179             : 
     180             :   @Option(
     181             :       name = "--description",
     182             :       aliases = {"-d"},
     183             :       usage = "include description of project in list")
     184             :   public void setShowDescription(boolean showDescription) {
     185           3 :     this.showDescription = showDescription;
     186           3 :   }
     187             : 
     188             :   @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
     189             :   public void setAll(boolean all) {
     190           3 :     this.all = all;
     191           3 :   }
     192             : 
     193             :   @Option(
     194             :       name = "--state",
     195             :       aliases = {"-s"},
     196             :       usage = "filter by project state")
     197             :   public void setState(com.google.gerrit.extensions.client.ProjectState state) {
     198           3 :     this.state = state;
     199           3 :   }
     200             : 
     201             :   @Option(
     202             :       name = "--limit",
     203             :       aliases = {"-n"},
     204             :       metaVar = "CNT",
     205             :       usage = "maximum number of projects to list")
     206             :   public void setLimit(int limit) {
     207           5 :     this.limit = limit;
     208           5 :   }
     209             : 
     210             :   @Option(
     211             :       name = "--start",
     212             :       aliases = {"-S"},
     213             :       metaVar = "CNT",
     214             :       usage = "number of projects to skip")
     215             :   public void setStart(int start) {
     216           3 :     this.start = start;
     217           3 :   }
     218             : 
     219             :   @Option(
     220             :       name = "--prefix",
     221             :       aliases = {"-p"},
     222             :       metaVar = "PREFIX",
     223             :       usage = "match project prefix")
     224             :   public void setMatchPrefix(String matchPrefix) {
     225           3 :     this.matchPrefix = matchPrefix;
     226           3 :   }
     227             : 
     228             :   @Option(
     229             :       name = "--match",
     230             :       aliases = {"-m"},
     231             :       metaVar = "MATCH",
     232             :       usage = "match project substring")
     233             :   public void setMatchSubstring(String matchSubstring) {
     234           3 :     this.matchSubstring = matchSubstring;
     235           3 :   }
     236             : 
     237             :   @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
     238             :   public void setMatchRegex(String matchRegex) {
     239           3 :     this.matchRegex = matchRegex;
     240           3 :   }
     241             : 
     242             :   @Option(
     243             :       name = "--has-acl-for",
     244             :       metaVar = "GROUP",
     245             :       usage = "displays only projects on which access rights for this group are directly assigned")
     246             :   public void setGroupUuid(AccountGroup.UUID groupUuid) {
     247           0 :     this.groupUuid = groupUuid;
     248           0 :   }
     249             : 
     250           7 :   private final List<String> showBranch = new ArrayList<>();
     251             :   private boolean showTree;
     252           7 :   private FilterType type = FilterType.ALL;
     253             :   private boolean showDescription;
     254             :   private boolean all;
     255             :   private com.google.gerrit.extensions.client.ProjectState state;
     256             :   private int limit;
     257             :   private int start;
     258             :   private String matchPrefix;
     259             :   private String matchSubstring;
     260             :   private String matchRegex;
     261             :   private AccountGroup.UUID groupUuid;
     262             :   private final Provider<QueryProjects> queryProjectsProvider;
     263             :   private final boolean listProjectsFromIndex;
     264             : 
     265             :   @Inject
     266             :   protected ListProjects(
     267             :       CurrentUser currentUser,
     268             :       ProjectCache projectCache,
     269             :       GroupResolver groupResolver,
     270             :       GroupControl.Factory groupControlFactory,
     271             :       GitRepositoryManager repoManager,
     272             :       PermissionBackend permissionBackend,
     273             :       ProjectNode.Factory projectNodeFactory,
     274             :       WebLinks webLinks,
     275             :       Provider<QueryProjects> queryProjectsProvider,
     276           7 :       @GerritServerConfig Config config) {
     277           7 :     this.currentUser = currentUser;
     278           7 :     this.projectCache = projectCache;
     279           7 :     this.groupResolver = groupResolver;
     280           7 :     this.groupControlFactory = groupControlFactory;
     281           7 :     this.repoManager = repoManager;
     282           7 :     this.permissionBackend = permissionBackend;
     283           7 :     this.projectNodeFactory = projectNodeFactory;
     284           7 :     this.webLinks = webLinks;
     285           7 :     this.queryProjectsProvider = queryProjectsProvider;
     286           7 :     this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
     287           7 :   }
     288             : 
     289             :   public List<String> getShowBranch() {
     290           1 :     return showBranch;
     291             :   }
     292             : 
     293             :   public boolean isShowTree() {
     294           1 :     return showTree;
     295             :   }
     296             : 
     297             :   public boolean isShowDescription() {
     298           0 :     return showDescription;
     299             :   }
     300             : 
     301             :   public OutputFormat getFormat() {
     302           1 :     return format;
     303             :   }
     304             : 
     305             :   public ListProjects setFormat(OutputFormat fmt) {
     306           4 :     format = fmt;
     307           4 :     return this;
     308             :   }
     309             : 
     310             :   @Override
     311             :   public Response<Object> apply(TopLevelResource resource)
     312             :       throws BadRequestException, PermissionBackendException {
     313           3 :     if (format == OutputFormat.TEXT) {
     314           0 :       ByteArrayOutputStream buf = new ByteArrayOutputStream();
     315           0 :       displayToStream(buf);
     316           0 :       return Response.ok(
     317           0 :           BinaryResult.create(buf.toByteArray())
     318           0 :               .setContentType("text/plain")
     319           0 :               .setCharacterEncoding(UTF_8));
     320             :     }
     321           3 :     return Response.ok(apply());
     322             :   }
     323             : 
     324             :   public SortedMap<String, ProjectInfo> apply()
     325             :       throws BadRequestException, PermissionBackendException {
     326           6 :     Optional<String> projectQuery = expressAsProjectsQuery();
     327           6 :     if (projectQuery.isPresent()) {
     328           1 :       return applyAsQuery(projectQuery.get());
     329             :     }
     330             : 
     331           6 :     format = OutputFormat.JSON;
     332           6 :     return display(null);
     333             :   }
     334             : 
     335             :   private Optional<String> expressAsProjectsQuery() {
     336           7 :     return listProjectsFromIndex
     337             :             && !all
     338             :             && state != HIDDEN
     339           1 :             && isNullOrEmpty(matchPrefix)
     340           1 :             && isNullOrEmpty(matchRegex)
     341           1 :             && isNullOrEmpty(matchSubstring) // TODO: see Issue 10446
     342             :             && type == FilterType.ALL
     343           1 :             && showBranch.isEmpty()
     344           7 :             && !showTree
     345           1 :         ? Optional.of(stateToQuery())
     346           7 :         : Optional.empty();
     347             :   }
     348             : 
     349             :   private String stateToQuery() {
     350           1 :     List<String> queries = new ArrayList<>();
     351           1 :     if (state == null) {
     352           1 :       queries.add("(state:active OR state:read-only)");
     353             :     } else {
     354           0 :       queries.add(String.format("(state:%s)", state.name()));
     355             :     }
     356             : 
     357           1 :     return Joiner.on(" AND ").join(queries);
     358             :   }
     359             : 
     360             :   private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
     361             :     try {
     362           1 :       return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
     363           1 :           .stream()
     364           1 :           .collect(
     365           1 :               ImmutableSortedMap.toImmutableSortedMap(
     366           1 :                   natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
     367           0 :     } catch (StorageException | MethodNotAllowedException e) {
     368           0 :       logger.atWarning().withCause(e).log(
     369             :           "Internal error while processing the query '%s' request", query);
     370           0 :       throw new BadRequestException("Internal error while processing the query request");
     371             :     }
     372             :   }
     373             : 
     374             :   private ProjectInfo nullifyDescription(ProjectInfo p) {
     375           1 :     p.description = null;
     376           1 :     return p;
     377             :   }
     378             : 
     379             :   private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
     380             :     try {
     381           0 :       if (format.isJson()) {
     382           0 :         format.newGson().toJson(applyAsQuery(query), out);
     383             :       } else {
     384           0 :         newProjectsNamesStream(query).forEach(out::println);
     385             :       }
     386           0 :       out.flush();
     387           0 :     } catch (StorageException | MethodNotAllowedException e) {
     388           0 :       logger.atWarning().withCause(e).log(
     389             :           "Internal error while processing the query '%s' request", query);
     390           0 :       throw new BadRequestException("Internal error while processing the query request");
     391           0 :     }
     392           0 :   }
     393             : 
     394             :   private Stream<String> newProjectsNamesStream(String query)
     395             :       throws MethodNotAllowedException, BadRequestException {
     396           0 :     Stream<String> projects =
     397           0 :         queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
     398           0 :     if (limit > 0) {
     399           0 :       projects = projects.limit(limit);
     400             :     }
     401             : 
     402           0 :     return projects;
     403             :   }
     404             : 
     405             :   public void displayToStream(OutputStream displayOutputStream)
     406             :       throws BadRequestException, PermissionBackendException {
     407           2 :     PrintWriter stdout =
     408             :         new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
     409           2 :     Optional<String> projectsQuery = expressAsProjectsQuery();
     410             : 
     411           2 :     if (projectsQuery.isPresent()) {
     412           0 :       printQueryResults(projectsQuery.get(), stdout);
     413             :     } else {
     414           2 :       display(stdout);
     415             :     }
     416           2 :   }
     417             : 
     418             :   @Nullable
     419             :   public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
     420             :       throws BadRequestException, PermissionBackendException {
     421           7 :     if (all && state != null) {
     422           1 :       throw new BadRequestException("'all' and 'state' may not be used together");
     423             :     }
     424           7 :     if (!isGroupVisible()) {
     425           0 :       return Collections.emptySortedMap();
     426             :     }
     427             : 
     428           7 :     int foundIndex = 0;
     429           7 :     int found = 0;
     430           7 :     TreeMap<String, ProjectInfo> output = new TreeMap<>();
     431           7 :     Map<String, String> hiddenNames = new HashMap<>();
     432           7 :     Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
     433           7 :     PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
     434           7 :     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
     435             :     try {
     436           7 :       Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
     437           7 :       while (projectStatesIt.hasNext()) {
     438           7 :         ProjectState e = projectStatesIt.next();
     439           7 :         Project.NameKey projectName = e.getNameKey();
     440           7 :         if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
     441             :           // If we can't get it from the cache, pretend it's not present.
     442             :           // If all wasn't selected, and it's HIDDEN, pretend it's not present.
     443             :           // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
     444           1 :           continue;
     445             :         }
     446             : 
     447           7 :         if (state != null && e.getProject().getState() != state) {
     448           1 :           continue;
     449             :         }
     450             : 
     451           7 :         if (groupUuid != null
     452           0 :             && !e.getLocalGroups()
     453           0 :                 .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
     454           0 :           continue;
     455             :         }
     456             : 
     457           7 :         if (showTree && !format.isJson()) {
     458           0 :           treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
     459           0 :           continue;
     460             :         }
     461             : 
     462           7 :         if (foundIndex++ < start) {
     463           1 :           continue;
     464             :         }
     465           7 :         if (limit > 0 && ++found > limit) {
     466           3 :           break;
     467             :         }
     468             : 
     469           7 :         ProjectInfo info = new ProjectInfo();
     470           7 :         info.name = projectName.get();
     471           7 :         if (showTree && format.isJson()) {
     472           1 :           addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
     473             :         }
     474             : 
     475           7 :         if (showDescription) {
     476           1 :           info.description = emptyToNull(e.getProject().getDescription());
     477             :         }
     478           7 :         info.state = e.getProject().getState();
     479             : 
     480             :         try {
     481           7 :           if (!showBranch.isEmpty()) {
     482           1 :             try (Repository git = repoManager.openRepository(projectName)) {
     483           1 :               if (!type.matches(git)) {
     484           0 :                 continue;
     485             :               }
     486             : 
     487           1 :               List<Ref> refs = retrieveBranchRefs(e, git);
     488           1 :               if (!hasValidRef(refs)) {
     489           1 :                 continue;
     490             :               }
     491             : 
     492           1 :               addProjectBranchesInfo(info, refs);
     493           1 :             }
     494           7 :           } else if (!showTree && type.useMatch()) {
     495           1 :             try (Repository git = repoManager.openRepository(projectName)) {
     496           1 :               if (!type.matches(git)) {
     497           1 :                 continue;
     498             :               }
     499           1 :             }
     500             :           }
     501           0 :         } catch (RepositoryNotFoundException err) {
     502             :           // If the Git repository is gone, the project doesn't actually exist anymore.
     503           0 :           continue;
     504           0 :         } catch (IOException err) {
     505           0 :           logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
     506           0 :           continue;
     507           7 :         }
     508             : 
     509           7 :         ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
     510           7 :         info.webLinks = links.isEmpty() ? null : links;
     511             : 
     512           7 :         if (stdout == null || format.isJson()) {
     513           6 :           output.put(info.name, info);
     514           6 :           continue;
     515             :         }
     516             : 
     517           2 :         if (!showBranch.isEmpty()) {
     518           0 :           printProjectBranches(stdout, info);
     519             :         }
     520           2 :         stdout.print(info.name);
     521             : 
     522           2 :         if (info.description != null) {
     523             :           // We still want to list every project as one-liners, hence escaping \n.
     524           0 :           stdout.print(" - " + StringUtil.escapeString(info.description));
     525             :         }
     526           2 :         stdout.print('\n');
     527           2 :       }
     528             : 
     529           7 :       for (ProjectInfo info : output.values()) {
     530           6 :         info.id = Url.encode(info.name);
     531           6 :         info.name = null;
     532           6 :       }
     533           7 :       if (stdout == null) {
     534           6 :         return output;
     535           2 :       } else if (format.isJson()) {
     536           1 :         format
     537           1 :             .newGson()
     538           1 :             .toJson(output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
     539           1 :         stdout.print('\n');
     540           2 :       } else if (showTree && treeMap.size() > 0) {
     541           0 :         printProjectTree(stdout, treeMap);
     542             :       }
     543           2 :       return null;
     544             :     } finally {
     545           7 :       if (stdout != null) {
     546           2 :         stdout.flush();
     547             :       }
     548             :     }
     549             :   }
     550             : 
     551             :   private boolean isGroupVisible() {
     552             :     try {
     553           7 :       return groupUuid == null || groupControlFactory.controlFor(groupUuid).isVisible();
     554           0 :     } catch (NoSuchGroupException ex) {
     555           0 :       return false;
     556             :     }
     557             :   }
     558             : 
     559             :   private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
     560           0 :     for (String name : showBranch) {
     561           0 :       String ref = info.branches != null ? info.branches.get(name) : null;
     562           0 :       if (ref == null) {
     563             :         // Print stub (forty '-' symbols)
     564           0 :         ref = "----------------------------------------";
     565             :       }
     566           0 :       stdout.print(ref);
     567           0 :       stdout.print(' ');
     568           0 :     }
     569           0 :   }
     570             : 
     571             :   private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
     572           1 :     for (int i = 0; i < showBranch.size(); i++) {
     573           1 :       Ref ref = refs.get(i);
     574           1 :       if (ref != null && ref.getObjectId() != null) {
     575           1 :         if (info.branches == null) {
     576           1 :           info.branches = new LinkedHashMap<>();
     577             :         }
     578           1 :         info.branches.put(showBranch.get(i), ref.getObjectId().name());
     579             :       }
     580             :     }
     581           1 :   }
     582             : 
     583             :   private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
     584           1 :     if (!e.statePermitsRead()) {
     585           0 :       return ImmutableList.of();
     586             :     }
     587             : 
     588           1 :     return getBranchRefs(e.getNameKey(), git);
     589             :   }
     590             : 
     591             :   private void addParentProjectInfo(
     592             :       Map<String, String> hiddenNames,
     593             :       Map<Project.NameKey, Boolean> accessibleParents,
     594             :       PermissionBackend.WithUser perm,
     595             :       ProjectState e,
     596             :       ProjectInfo info)
     597             :       throws PermissionBackendException {
     598           1 :     ProjectState parent = Iterables.getFirst(e.parents(), null);
     599           1 :     if (parent != null) {
     600           1 :       if (isParentAccessible(accessibleParents, perm, parent)) {
     601           1 :         info.parent = parent.getName();
     602             :       } else {
     603           0 :         info.parent = hiddenNames.get(parent.getName());
     604           0 :         if (info.parent == null) {
     605           0 :           info.parent = "?-" + (hiddenNames.size() + 1);
     606           0 :           hiddenNames.put(parent.getName(), info.parent);
     607             :         }
     608             :       }
     609             :     }
     610           1 :   }
     611             : 
     612             :   private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
     613           7 :     return StreamSupport.stream(scan().spliterator(), false)
     614           7 :         .map(projectCache::get)
     615           7 :         .filter(Optional::isPresent)
     616           7 :         .map(Optional::get)
     617           7 :         .filter(p -> permissionCheck(p, perm));
     618             :   }
     619             : 
     620             :   private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
     621             :     // Hidden projects(permitsRead = false) should only be accessible by the project owners.
     622             :     // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
     623             :     // be allowed for other users). Allowing project owners to access here will help them to view
     624             :     // and update the config of hidden projects easily.
     625           7 :     return perm.project(state.getNameKey())
     626           7 :         .testOrFalse(
     627           7 :             state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
     628             :   }
     629             : 
     630             :   private boolean isParentAccessible(
     631             :       Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
     632             :       throws PermissionBackendException {
     633           1 :     Project.NameKey name = state.getNameKey();
     634           1 :     Boolean b = checked.get(name);
     635           1 :     if (b == null) {
     636             :       try {
     637             :         // Hidden projects(permitsRead = false) should only be accessible by the project owners.
     638             :         // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
     639             :         // be allowed for other users). Allowing project owners to access here will help them to
     640             :         // view
     641             :         // and update the config of hidden projects easily.
     642             :         ProjectPermission permissionToCheck =
     643           1 :             state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
     644           1 :         perm.project(name).check(permissionToCheck);
     645           1 :         b = true;
     646           0 :       } catch (AuthException denied) {
     647           0 :         b = false;
     648           1 :       }
     649           1 :       checked.put(name, b);
     650             :     }
     651           1 :     return b;
     652             :   }
     653             : 
     654             :   private Stream<Project.NameKey> scan() throws BadRequestException {
     655           7 :     if (matchPrefix != null) {
     656           1 :       checkMatchOptions(matchSubstring == null && matchRegex == null);
     657           1 :       return projectCache.byName(matchPrefix).stream();
     658           7 :     } else if (matchSubstring != null) {
     659           1 :       checkMatchOptions(matchPrefix == null && matchRegex == null);
     660           1 :       return projectCache.all().stream()
     661           1 :           .filter(
     662           1 :               p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
     663           7 :     } else if (matchRegex != null) {
     664           1 :       checkMatchOptions(matchPrefix == null && matchSubstring == null);
     665             :       RegexListSearcher<Project.NameKey> searcher;
     666             :       try {
     667           1 :         searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
     668           1 :       } catch (IllegalArgumentException e) {
     669           1 :         throw new BadRequestException(e.getMessage());
     670           1 :       }
     671           1 :       return searcher.search(projectCache.all().asList());
     672             :     } else {
     673           7 :       return projectCache.all().stream();
     674             :     }
     675             :   }
     676             : 
     677             :   private static void checkMatchOptions(boolean cond) throws BadRequestException {
     678           1 :     if (!cond) {
     679           1 :       throw new BadRequestException("specify exactly one of p/m/r");
     680             :     }
     681           1 :   }
     682             : 
     683             :   private void printProjectTree(
     684             :       final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
     685           0 :     final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
     686             : 
     687             :     // Builds the inheritance tree using a list.
     688             :     //
     689           0 :     for (ProjectNode key : treeMap.values()) {
     690           0 :       if (key.isAllProjects()) {
     691           0 :         sortedNodes.add(key);
     692           0 :         continue;
     693             :       }
     694             : 
     695           0 :       ProjectNode node = treeMap.get(key.getParentName());
     696           0 :       if (node != null) {
     697           0 :         node.addChild(key);
     698             :       } else {
     699           0 :         sortedNodes.add(key);
     700             :       }
     701           0 :     }
     702             : 
     703           0 :     final TreeFormatter treeFormatter = new TreeFormatter(stdout);
     704           0 :     treeFormatter.printTree(sortedNodes);
     705           0 :     stdout.flush();
     706           0 :   }
     707             : 
     708             :   private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
     709           1 :     Ref[] result = new Ref[showBranch.size()];
     710             :     try {
     711           1 :       PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
     712           1 :       for (int i = 0; i < showBranch.size(); i++) {
     713           1 :         Ref ref = git.findRef(showBranch.get(i));
     714           1 :         if (ref != null && ref.getObjectId() != null) {
     715             :           try {
     716           1 :             perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
     717           1 :             result[i] = ref;
     718           0 :           } catch (AuthException e) {
     719           0 :             continue;
     720           1 :           }
     721             :         }
     722             :       }
     723           0 :     } catch (IOException | PermissionBackendException e) {
     724             :       // Fall through and return what is available.
     725           1 :     }
     726           1 :     return Arrays.asList(result);
     727             :   }
     728             : 
     729             :   private static boolean hasValidRef(List<Ref> refs) {
     730           1 :     for (Ref ref : refs) {
     731           1 :       if (ref != null) {
     732           1 :         return true;
     733             :       }
     734           1 :     }
     735           1 :     return false;
     736             :   }
     737             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750