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 : }
|