diff options
author | Martin Fick <mfick@codeaurora.org> | 2021-10-28 20:35:21 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-10-28 20:35:21 +0000 |
commit | 5c9ed3b9f15f378739a834bca87043c037134979 (patch) | |
tree | 1a56dab597cba219fa4d0ec12832ce5fc5ad26aa | |
parent | 4327dafac1823926298fe8e263ac98d1a30d5e3e (diff) | |
parent | 559132d8fd699f2a52c8f5d744cf6b29f1fc47d0 (diff) |
Merge changes I756c55f8,Ic9f0dd6d,I05a330de into stable-3.5
* changes:
Add REST endpoint for commits included in refs
Use JGit's RevWalk.getMergedInto instead of IncludedInResolver
Update JGit to 60b81c5a9280e44fa48d533a61f915382b2b9ce2
12 files changed, 569 insertions, 339 deletions
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 92c6dbf13c..2d5451872c 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -1541,6 +1541,47 @@ entity is returned that results for the consistency checks. } ---- +[[commits-included-in]] +=== Get Commits Included In Refs +-- +'GET /projects/link:#project-name[\{project-name\}]/commits:in' +-- + +Gets refs in which the specified commits were merged into. Returns a map of commits +to sets of full ref names. + +One or more `commit` query parameters are required and each specifies a +commit-id (SHA-1 in 40 digit hex representation). Commits will not be contained +in the map if they do not exist or are not reachable from visible, specified refs. + +One or more `ref` query parameters are required and each specifies a full ref name. +Refs which are not visible to the calling user according to the project's read +permissions and refs which do not exist will be filtered out from the result. + +.Request +---- + GET /projects/work%2Fmy-project/commits:in?commit=a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96&commit=6d2a3adb10e844c33617fc948dbeb88e868d396e&ref=refs/heads/master&ref=refs/heads/branch1 HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + { + "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96": [ + "refs/heads/master" + ], + "6d2a3adb10e844c33617fc948dbeb88e868d396e": [ + "refs/heads/branch1", + "refs/heads/master" + ] + } + +---- + [[branch-endpoints]] == Branch Endpoints diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index 987399501c..59475a46dd 100644 --- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java @@ -24,7 +24,10 @@ import com.google.gerrit.extensions.common.LabelDefinitionInfo; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; public interface ProjectApi { ProjectApi create() throws RestApiException; @@ -51,6 +54,9 @@ public interface ProjectApi { ConfigInfo config(ConfigInput in) throws RestApiException; + Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs) + throws RestApiException; + ListRefsRequest<BranchInfo> branches(); ListRefsRequest<TagInfo> tags(); @@ -289,6 +295,12 @@ public interface ProjectApi { } @Override + public Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs) + throws RestApiException { + throw new NotImplementedException(); + } + + @Override public void description(DescriptionInput in) throws RestApiException { throw new NotImplementedException(); } diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index 6d7fc15d40..9521759cce 100644 --- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java @@ -63,6 +63,7 @@ import com.google.gerrit.server.restapi.project.Check; import com.google.gerrit.server.restapi.project.CheckAccess; import com.google.gerrit.server.restapi.project.ChildProjectsCollection; import com.google.gerrit.server.restapi.project.CommitsCollection; +import com.google.gerrit.server.restapi.project.CommitsIncludedInRefs; import com.google.gerrit.server.restapi.project.CreateAccessChange; import com.google.gerrit.server.restapi.project.CreateProject; import com.google.gerrit.server.restapi.project.DeleteBranches; @@ -88,8 +89,11 @@ import com.google.gerrit.server.restapi.project.SetParent; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; public class ProjectApiImpl implements ProjectApi { interface Factory { @@ -116,6 +120,7 @@ public class ProjectApiImpl implements ProjectApi { private final CreateAccessChange createAccessChange; private final GetConfig getConfig; private final PutConfig putConfig; + private final CommitsIncludedInRefs commitsIncludedInRefs; private final Provider<ListBranches> listBranches; private final Provider<ListTags> listTags; private final DeleteBranches deleteBranches; @@ -154,6 +159,7 @@ public class ProjectApiImpl implements ProjectApi { CreateAccessChange createAccessChange, GetConfig getConfig, PutConfig putConfig, + CommitsIncludedInRefs commitsIncludedInRefs, Provider<ListBranches> listBranches, Provider<ListTags> listTags, DeleteBranches deleteBranches, @@ -191,6 +197,7 @@ public class ProjectApiImpl implements ProjectApi { createAccessChange, getConfig, putConfig, + commitsIncludedInRefs, listBranches, listTags, deleteBranches, @@ -232,6 +239,7 @@ public class ProjectApiImpl implements ProjectApi { CreateAccessChange createAccessChange, GetConfig getConfig, PutConfig putConfig, + CommitsIncludedInRefs commitsIncludedInRefs, Provider<ListBranches> listBranches, Provider<ListTags> listTags, DeleteBranches deleteBranches, @@ -269,6 +277,7 @@ public class ProjectApiImpl implements ProjectApi { createAccessChange, getConfig, putConfig, + commitsIncludedInRefs, listBranches, listTags, deleteBranches, @@ -309,6 +318,7 @@ public class ProjectApiImpl implements ProjectApi { CreateAccessChange createAccessChange, GetConfig getConfig, PutConfig putConfig, + CommitsIncludedInRefs commitsIncludedInRefs, Provider<ListBranches> listBranches, Provider<ListTags> listTags, DeleteBranches deleteBranches, @@ -346,6 +356,7 @@ public class ProjectApiImpl implements ProjectApi { this.setAccess = setAccess; this.getConfig = getConfig; this.putConfig = putConfig; + this.commitsIncludedInRefs = commitsIncludedInRefs; this.listBranches = listBranches; this.listTags = listTags; this.deleteBranches = deleteBranches; @@ -483,6 +494,18 @@ public class ProjectApiImpl implements ProjectApi { } @Override + public Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs) + throws RestApiException { + try { + commitsIncludedInRefs.addCommits(commits); + commitsIncludedInRefs.addRefs(refs); + return commitsIncludedInRefs.apply(project).value(); + } catch (Exception e) { + throw asRestApiException("Cannot list commits included in refs", e); + } + } + + @Override public ListRefsRequest<BranchInfo> branches() { return new ListRefsRequest<BranchInfo>() { @Override diff --git a/java/com/google/gerrit/server/change/IncludedIn.java b/java/com/google/gerrit/server/change/IncludedIn.java index c06ce82c05..94498d7f49 100644 --- a/java/com/google/gerrit/server/change/IncludedIn.java +++ b/java/com/google/gerrit/server/change/IncludedIn.java @@ -21,6 +21,7 @@ import static java.util.Comparator.naturalOrder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; import com.google.common.collect.MultimapBuilder; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.changes.IncludedInInfo; @@ -37,10 +38,15 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +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.RevWalk; @@ -75,13 +81,26 @@ public class IncludedIn { throw new ResourceConflictException(err.getMessage()); } - IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev); + RefDatabase refDb = r.getRefDatabase(); + Collection<Ref> tags = refDb.getRefsByPrefix(Constants.R_TAGS); + Collection<Ref> branches = refDb.getRefsByPrefix(Constants.R_HEADS); + List<Ref> allTagsAndBranches = Lists.newArrayListWithCapacity(tags.size() + branches.size()); + allTagsAndBranches.addAll(tags); + allTagsAndBranches.addAll(branches); + + Set<String> allMatchingTagsAndBranches = + rw.getMergedInto(rev, IncludedInUtil.getSortedRefs(allTagsAndBranches, rw)).stream() + .map(Ref::getName) + .collect(Collectors.toSet()); // Filter branches and tags according to their visbility by the user ImmutableSortedSet<String> filteredBranches = - sortedShortNames(filterReadableRefs(project, d.branches())); + sortedShortNames( + filterReadableRefs( + project, getMatchingRefNames(allMatchingTagsAndBranches, branches))); ImmutableSortedSet<String> filteredTags = - sortedShortNames(filterReadableRefs(project, d.tags())); + sortedShortNames( + filterReadableRefs(project, getMatchingRefNames(allMatchingTagsAndBranches, tags))); ListMultimap<String, String> external = MultimapBuilder.hashKeys().arrayListValues().build(); externalIncludedIn.runEach( @@ -115,6 +134,18 @@ public class IncludedIn { } } + /** + * Returns the short names of refs which are as well in the matchingRefs list as well as in the + * allRef list. + */ + private static ImmutableList<Ref> getMatchingRefNames( + Set<String> matchingRefs, Collection<Ref> allRefs) { + return allRefs.stream() + .filter(r -> matchingRefs.contains(r.getName())) + .distinct() + .collect(toImmutableList()); + } + private ImmutableSortedSet<String> sortedShortNames(Collection<String> refs) { return refs.stream() .map(Repository::shortenRefName) diff --git a/java/com/google/gerrit/server/change/IncludedInRefs.java b/java/com/google/gerrit/server/change/IncludedInRefs.java new file mode 100644 index 0000000000..f06925129c --- /dev/null +++ b/java/com/google/gerrit/server/change/IncludedInRefs.java @@ -0,0 +1,139 @@ +// Copyright (C) 2021 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.change; + +import static java.util.stream.Collectors.toSet; + +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.inject.Inject; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +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.RevWalk; + +public class IncludedInRefs { + protected final GitRepositoryManager repoManager; + protected final PermissionBackend permissionBackend; + + @Inject + IncludedInRefs(GitRepositoryManager repoManager, PermissionBackend permissionBackend) { + this.repoManager = repoManager; + this.permissionBackend = permissionBackend; + } + + public Map<String, Set<String>> apply( + Project.NameKey project, Set<String> commits, Set<String> refNames) + throws ResourceConflictException, BadRequestException, IOException, + PermissionBackendException, ResourceNotFoundException, AuthException { + try (Repository repo = repoManager.openRepository(project)) { + Set<Ref> visibleRefs = getVisibleRefs(repo, refNames, project); + + if (!visibleRefs.isEmpty()) { + try (RevWalk revWalk = new RevWalk(repo)) { + revWalk.setRetainBody(false); + Set<RevCommit> revCommits = getRevCommits(commits, revWalk); + + if (!revCommits.isEmpty()) { + return commitsIncludedIn( + revCommits, IncludedInUtil.getSortedRefs(visibleRefs, revWalk), revWalk); + } + } + } + } + return Collections.EMPTY_MAP; + } + + private Set<Ref> getVisibleRefs(Repository repo, Set<String> refNames, Project.NameKey project) + throws PermissionBackendException { + RefDatabase refDb = repo.getRefDatabase(); + Set<Ref> refs = new HashSet<>(); + for (String refName : refNames) { + try { + Ref ref = refDb.exactRef(refName); + if (ref != null) { + refs.add(ref); + } + } catch (IOException e) { + // Ignore and continue to process rest of the refs so as to keep + // the behavior similar to the ref not being visible to the user. + // This will ensure that there is no information leak about the + // ref when the ref is corrupted and is not visible to the user. + } + } + return filterReadableRefs(project, refs, repo); + } + + private Set<RevCommit> getRevCommits(Set<String> commits, RevWalk revWalk) throws IOException { + Set<RevCommit> revCommits = new HashSet<>(); + for (String commit : commits) { + try { + revCommits.add(revWalk.parseCommit(ObjectId.fromString(commit))); + } catch (MissingObjectException | IncorrectObjectTypeException | IllegalArgumentException e) { + // Ignore and continue to process the rest of the commits so as to keep + // the behavior similar to the commit not being included in any of the + // visible specified refs. This will ensure that there is no information + // leak about the commit when the commit is not visible to the user. + } + } + return revCommits; + } + + private Map<String, Set<String>> commitsIncludedIn( + Collection<RevCommit> commits, Collection<Ref> refs, RevWalk revWalk) throws IOException { + Map<String, Set<String>> refsByCommit = new HashMap<>(); + for (RevCommit commit : commits) { + List<Ref> matchingRefs = revWalk.getMergedInto(commit, refs); + if (matchingRefs.size() > 0) { + refsByCommit.put( + commit.getName(), matchingRefs.stream().map(Ref::getName).collect(toSet())); + } + } + return refsByCommit; + } + + /** + * Filter readable refs according to the caller's refs visibility. + * + * @param project specific Gerrit project. + * @param inputRefs a list of refs + * @param repo repository opened for the Gerrit project. + * @return set of visible refs to the caller + */ + private Set<Ref> filterReadableRefs(Project.NameKey project, Set<Ref> inputRefs, Repository repo) + throws PermissionBackendException { + PermissionBackend.ForProject perm = permissionBackend.currentUser().project(project); + return perm.filter(inputRefs, repo, RefFilterOptions.defaults()).stream().collect(toSet()); + } +} diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java deleted file mode 100644 index b2b0a64fa3..0000000000 --- a/java/com/google/gerrit/server/change/IncludedInResolver.java +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (C) 2013 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.change; - -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toList; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Lists; -import com.google.common.flogger.FluentLogger; -import java.io.IOException; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.lib.Constants; -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.RevFlag; -import org.eclipse.jgit.revwalk.RevWalk; - -/** Resolve in which tags and branches a commit is included. */ -public class IncludedInResolver { - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - public static Result resolve(Repository repo, RevWalk rw, RevCommit commit) throws IOException { - RevFlag flag = newFlag(rw); - try { - return new IncludedInResolver(repo, rw, commit, flag).resolve(); - } finally { - rw.disposeFlag(flag); - } - } - - private static RevFlag newFlag(RevWalk rw) { - return rw.newFlag("CONTAINS_TARGET"); - } - - private final Repository repo; - private final RevWalk rw; - private final RevCommit target; - - private final RevFlag containsTarget; - private ListMultimap<RevCommit, String> commitToRef; - private List<RevCommit> tipsByCommitTime; - - private IncludedInResolver( - Repository repo, RevWalk rw, RevCommit target, RevFlag containsTarget) { - this.repo = repo; - this.rw = rw; - this.target = target; - this.containsTarget = containsTarget; - } - - private Result resolve() throws IOException { - RefDatabase refDb = repo.getRefDatabase(); - Collection<Ref> tags = refDb.getRefsByPrefix(Constants.R_TAGS); - Collection<Ref> branches = refDb.getRefsByPrefix(Constants.R_HEADS); - List<Ref> allTagsAndBranches = Lists.newArrayListWithCapacity(tags.size() + branches.size()); - allTagsAndBranches.addAll(tags); - allTagsAndBranches.addAll(branches); - parseCommits(allTagsAndBranches); - Set<String> allMatchingTagsAndBranches = includedIn(tipsByCommitTime, 0); - - return new AutoValue_IncludedInResolver_Result( - getMatchingRefNames(allMatchingTagsAndBranches, branches), - getMatchingRefNames(allMatchingTagsAndBranches, tags)); - } - - /** Resolves which tip refs include the target commit. */ - private Set<String> includedIn(Collection<RevCommit> tips, int limit) - throws IOException, MissingObjectException, IncorrectObjectTypeException { - Set<String> result = new HashSet<>(); - for (RevCommit tip : tips) { - boolean commitFound = false; - rw.resetRetain(RevFlag.UNINTERESTING, containsTarget); - rw.markStart(tip); - for (RevCommit commit : rw) { - if (commit.equals(target) || commit.has(containsTarget)) { - commitFound = true; - tip.add(containsTarget); - result.addAll(commitToRef.get(tip)); - break; - } - } - if (!commitFound) { - rw.markUninteresting(tip); - } else if (0 < limit && limit < result.size()) { - break; - } - } - return result; - } - - /** - * Returns the short names of refs which are as well in the matchingRefs list as well as in the - * allRef list. - */ - private static ImmutableList<Ref> getMatchingRefNames( - Set<String> matchingRefs, Collection<Ref> allRefs) { - return allRefs.stream() - .filter(r -> matchingRefs.contains(r.getName())) - .distinct() - .collect(ImmutableList.toImmutableList()); - } - - /** Parse commit of ref and store the relation between ref and commit. */ - private void parseCommits(Collection<Ref> refs) throws IOException { - if (commitToRef != null) { - return; - } - commitToRef = LinkedListMultimap.create(); - for (Ref ref : refs) { - final RevCommit commit; - try { - commit = rw.parseCommit(ref.getObjectId()); - } catch (IncorrectObjectTypeException notCommit) { - // Its OK for a tag reference to point to a blob or a tree, this - // is common in the Linux kernel or git.git repository. - // - continue; - } catch (MissingObjectException notHere) { - // Log the problem with this branch, but keep processing. - // - logger.atWarning().log( - "Reference %s in %s points to dangling object %s", - ref.getName(), repo.getDirectory(), ref.getObjectId()); - continue; - } - commitToRef.put(commit, ref.getName()); - } - tipsByCommitTime = - commitToRef.keySet().stream().sorted(comparing(RevCommit::getCommitTime)).collect(toList()); - } - - @AutoValue - public abstract static class Result { - public abstract ImmutableList<Ref> branches(); - - public abstract ImmutableList<Ref> tags(); - } -} diff --git a/java/com/google/gerrit/server/change/IncludedInUtil.java b/java/com/google/gerrit/server/change/IncludedInUtil.java new file mode 100644 index 0000000000..6f75e0f402 --- /dev/null +++ b/java/com/google/gerrit/server/change/IncludedInUtil.java @@ -0,0 +1,49 @@ +// Copyright (C) 2021 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.change; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevWalk; + +public class IncludedInUtil { + + /** + * Sorts the collection of {@code Ref} instances by its tip commit time. + * + * @param refs collection to be sorted + * @param revWalk {@code RevWalk} instance for parsing ref's tip commit + * @return sorted list of refs + */ + public static List<Ref> getSortedRefs(Collection<Ref> refs, RevWalk revWalk) { + return refs.stream() + .sorted( + comparing( + ref -> { + try { + return revWalk.parseCommit(ref.getObjectId()).getCommitTime(); + } catch (IOException e) { + // Ignore and continue to sort + } + return 0; + })) + .collect(toList()); + } +} diff --git a/java/com/google/gerrit/server/restapi/project/CommitsIncludedInRefs.java b/java/com/google/gerrit/server/restapi/project/CommitsIncludedInRefs.java new file mode 100644 index 0000000000..05ec28ee14 --- /dev/null +++ b/java/com/google/gerrit/server/restapi/project/CommitsIncludedInRefs.java @@ -0,0 +1,85 @@ +// Copyright (C) 2021 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.restapi.project; + +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.change.IncludedInRefs; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.kohsuke.args4j.Option; + +public class CommitsIncludedInRefs implements RestReadView<ProjectResource> { + protected final IncludedInRefs includedInRefs; + + protected Set<String> commits = new HashSet<>(); + protected Set<String> refs = new HashSet<>(); + + @Option( + name = "--commit", + aliases = {"-c"}, + required = true, + metaVar = "COMMIT", + usage = "commit sha1") + protected void addCommit(String commit) { + commits.add(commit); + } + + @Option( + name = "--ref", + aliases = {"-r"}, + required = true, + metaVar = "REF", + usage = "full ref name") + protected void addRef(String ref) { + refs.add(ref); + } + + public void addCommits(Collection<String> commits) { + this.commits.addAll(commits); + } + + public void addRefs(Collection<String> refs) { + this.refs.addAll(refs); + } + + @Inject + public CommitsIncludedInRefs(IncludedInRefs includedInRefs) { + this.includedInRefs = includedInRefs; + } + + @Override + public Response<Map<String, Set<String>>> apply(ProjectResource resource) + throws ResourceConflictException, BadRequestException, IOException, + PermissionBackendException, ResourceNotFoundException, AuthException { + if (commits.isEmpty()) { + throw new BadRequestException("commit is required"); + } + if (refs.isEmpty()) { + throw new BadRequestException("ref is required"); + } + return Response.ok(includedInRefs.apply(resource.getNameKey(), commits, refs)); + } +} diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java index 410a88c8e7..e50a494922 100644 --- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java +++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java @@ -99,6 +99,7 @@ public class ProjectRestApiModule extends RestApiModule { get(FILE_KIND, "content").to(GetContent.class); child(PROJECT_KIND, "commits").to(CommitsCollection.class); + get(PROJECT_KIND, "commits:in").to(CommitsIncludedInRefs.class); get(COMMIT_KIND).to(GetCommit.class); get(COMMIT_KIND, "in").to(CommitIncludedIn.class); child(COMMIT_KIND, "files").to(FilesInCommitCollection.class); diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java index c42628c9db..6b2f4f14e4 100644 --- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java +++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java @@ -16,20 +16,25 @@ package com.google.gerrit.acceptance.api.project; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static com.google.gerrit.acceptance.GitUtil.pushHead; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_GLOBAL; import static com.google.gerrit.server.project.ProjectState.INHERITED_FROM_PARENT; import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_GLOBAL; import static com.google.gerrit.server.project.ProjectState.OVERRIDDEN_BY_PARENT; import static com.google.gerrit.testing.GerritJUnit.assertThrows; import static java.util.stream.Collectors.toSet; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.AtomicLongMap; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.ExtensionRegistry; @@ -41,6 +46,7 @@ import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.entities.AccountGroup; +import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.GroupReference; import com.google.gerrit.entities.LabelId; @@ -74,9 +80,13 @@ import com.google.gerrit.server.project.ProjectConfig; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Module; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; @@ -92,6 +102,7 @@ public class ProjectIT extends AbstractDaemonTest { private static final String JIRA = "jira"; private static final String JIRA_LINK = "http://jira.example.com/?id=$2"; private static final String JIRA_MATCH = "(jira\\\\s+#?)(\\\\d+)"; + private static final String R_HEADS_MASTER = R_HEADS + "master"; @Inject private ProjectOperations projectOperations; @Inject private RequestScopeOperations requestScopeOperations; @@ -1000,6 +1011,156 @@ public class ProjectIT extends AbstractDaemonTest { assertThat(afterRename).hasValue(newName); } + @Test + public void commitsIncludedInRefsEmptyCommitAndEmptyRefs() throws Exception { + BadRequestException thrown = + assertThrows( + BadRequestException.class, + () -> getCommitsIncludedInRefs(Collections.emptyList(), Arrays.asList(R_HEADS_MASTER))); + assertThat(thrown).hasMessageThat().contains("commit is required"); + PushOneCommit.Result result = createChange(); + thrown = + assertThrows( + BadRequestException.class, + () -> getCommitsIncludedInRefs(result.getCommit().getName(), Collections.emptyList())); + assertThat(thrown).hasMessageThat().contains("ref is required"); + } + + @Test + public void commitsIncludedInRefsNonExistentCommits() throws Exception { + assertThat( + getCommitsIncludedInRefs( + Arrays.asList("foo", "4fa12ab8f257034ec793dacb2ae2752ae2e9f5f3"), + Arrays.asList(R_HEADS_MASTER))) + .isEmpty(); + } + + @Test + public void commitsIncludedInRefsNonExistentRefs() throws Exception { + PushOneCommit.Result change = createChange(); + assertThat( + getCommitsIncludedInRefs( + Arrays.asList(change.getCommit().getName()), Arrays.asList(R_HEADS + "foo"))) + .isEmpty(); + } + + @Test + public void commitsIncludedInRefsOpenChange() throws Exception { + PushOneCommit.Result change1 = createChange(); + testRepo.reset(change1.getCommit().getParent(0)); + PushOneCommit.Result change2 = createChange(); + + Map<String, Set<String>> refsByCommit = + getCommitsIncludedInRefs( + Arrays.asList(change1.getCommit().getName(), change2.getCommit().getName()), + Arrays.asList( + R_HEADS_MASTER, change1.getPatchSet().refName(), change2.getPatchSet().refName())); + assertThat(refsByCommit.get(change2.getCommit().getName())) + .containsExactly(change2.getPatchSet().refName()); + assertThat(refsByCommit.get(change1.getCommit().getName())) + .containsExactly(change1.getPatchSet().refName()); + } + + @Test + public void commitsIncludedInRefsMergedChange() throws Exception { + String branchWithoutChanges = R_HEADS + "branch-without-changes"; + String tagWithChange1 = R_TAGS + "tag-with-change1"; + String branchWithChange1 = R_HEADS + "branch-with-change1"; + + createBranch(BranchNameKey.create(project, "branch-without-changes")); + PushOneCommit.Result change1 = createAndSubmitChange("refs/for/master"); + + assertThat( + getCommitsIncludedInRefs(change1.getCommit().getName(), Arrays.asList(R_HEADS_MASTER))) + .containsExactly(R_HEADS_MASTER); + assertThat( + getCommitsIncludedInRefs( + change1.getCommit().getName(), Arrays.asList(R_HEADS_MASTER, branchWithoutChanges))) + .containsExactly(R_HEADS_MASTER); + + pushHead(testRepo, tagWithChange1, false, false); + createBranch(BranchNameKey.create(project, "branch-with-change1")); + + PushOneCommit.Result change2 = createAndSubmitChange("refs/for/master"); + Map<String, Set<String>> refsByCommit = + getCommitsIncludedInRefs( + Arrays.asList(change1.getCommit().getName(), change2.getCommit().getName()), + Arrays.asList(R_HEADS_MASTER, tagWithChange1, branchWithoutChanges, branchWithChange1)); + assertThat(refsByCommit.get(change1.getCommit().getName())) + .containsExactly(R_HEADS_MASTER, tagWithChange1, branchWithChange1); + assertThat(refsByCommit.get(change2.getCommit().getName())).containsExactly(R_HEADS_MASTER); + } + + @Test + public void commitsIncludedInRefsMergedChangeNonTipCommit() throws Exception { + String branchWithChange1 = R_HEADS + "branch-with-change1"; + String tagWithChange1 = R_TAGS + "tag-with-change1"; + String branchWithChange1Change2 = R_HEADS + "branch-with-change1-change2"; + String tagWithChange1Change2 = R_TAGS + "tag-with-change1-change2"; + + createBranch(BranchNameKey.create(project, "branch-with-change1")); + PushOneCommit.Result change1 = createAndSubmitChange("refs/for/branch-with-change1"); + pushHead(testRepo, tagWithChange1, false, false); + createBranch(BranchNameKey.create(project, "branch-with-change1-change2")); + PushOneCommit.Result change2 = createAndSubmitChange("refs/for/branch-with-change1-change2"); + pushHead(testRepo, tagWithChange1Change2, false, false); + + Map<String, Set<String>> refsByCommit = + getCommitsIncludedInRefs( + Arrays.asList(change1.getCommit().getName(), change2.getCommit().getName()), + Arrays.asList( + branchWithChange1, + branchWithChange1Change2, + tagWithChange1, + tagWithChange1Change2)); + assertThat(refsByCommit.get(change1.getCommit().getName())) + .containsExactly( + branchWithChange1, branchWithChange1Change2, tagWithChange1, tagWithChange1Change2); + assertThat(refsByCommit.get(change2.getCommit().getName())) + .containsExactly(branchWithChange1Change2, tagWithChange1Change2); + } + + @Test + public void commitsIncludedInRefsMergedChangeFilterNonVisibleBranchRef() throws Exception { + String nonVisibleBranch = R_HEADS + "non-visible-branch"; + PushOneCommit.Result change = createAndSubmitChange("refs/for/master"); + createBranch(BranchNameKey.create(project, "non-visible-branch")); + blockReadPermission(nonVisibleBranch); + + assertThat( + getCommitsIncludedInRefs( + change.getCommit().getName(), Arrays.asList(R_HEADS_MASTER, nonVisibleBranch))) + .containsExactly(R_HEADS_MASTER); + } + + @Test + public void commitsIncludedInRefsMergedChangeFilterNonVisibleTagRef() throws Exception { + String nonVisibleTag = R_TAGS + "non-visible-tag"; + PushOneCommit.Result change = createAndSubmitChange("refs/for/master"); + pushHead(testRepo, nonVisibleTag, false, false); + // Tag permissions are controlled by read permissions on branches. Blocking read permission + // on master so that tag-with-change becomes non-visible + blockReadPermission(R_HEADS_MASTER); + + assertThat( + getCommitsIncludedInRefs( + change.getCommit().getName(), Arrays.asList(R_HEADS_MASTER, nonVisibleTag))) + .isNull(); + } + + @Test + public void commitsIncludedInRefsFilterNonVisibleChangeRef() throws Exception { + PushOneCommit.Result change = createChange("refs/for/master"); + // change ref permissions are controlled by read permissions on destination branch. + // Blocking read permission on master so that refs/changes/01/1/1 becomes non-visible + blockReadPermission(R_HEADS_MASTER); + + assertThat( + getCommitsIncludedInRefs( + change.getCommit().getName(), Arrays.asList(change.getPatchSet().refName()))) + .isNull(); + } + private CommentLinkInfo commentLinkInfo(String name, String match, String link) { CommentLinkInfo info = new CommentLinkInfo(); info.name = name; @@ -1039,6 +1200,30 @@ public class ProjectIT extends AbstractDaemonTest { return getConfig(project); } + private Set<String> getCommitsIncludedInRefs(String commit, List<String> refs) throws Exception { + return getCommitsIncludedInRefs(Lists.newArrayList(commit), refs).get(commit); + } + + private Map<String, Set<String>> getCommitsIncludedInRefs(List<String> commits, List<String> refs) + throws Exception { + return gApi.projects().name(project.get()).commitsIn(commits, refs); + } + + private PushOneCommit.Result createAndSubmitChange(String branch) throws Exception { + PushOneCommit.Result r = createChange(branch); + approve(r.getChangeId()); + gApi.changes().id(r.getChangeId()).current().submit(); + return r; + } + + private void blockReadPermission(String ref) { + projectOperations + .project(project) + .forUpdate() + .add(block(Permission.READ).ref(ref).group(REGISTERED_USERS)) + .update(); + } + private ConfigInput createTestConfigInput() { ConfigInput input = new ConfigInput(); input.description = "some description"; diff --git a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java deleted file mode 100644 index b69a894b14..0000000000 --- a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (C) 2013 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.change; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.gerrit.entities.RefNames.REFS_TAGS; - -import com.google.common.truth.Correspondence; -import com.google.gerrit.truth.NullAwareCorrespondence; -import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; -import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; -import org.eclipse.jgit.junit.TestRepository; -import org.eclipse.jgit.lib.Ref; -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.junit.Before; -import org.junit.Test; - -public class IncludedInResolverTest { - // Branch names - private static final String BRANCH_MASTER = "master"; - private static final String BRANCH_1_0 = "rel-1.0"; - private static final String BRANCH_1_3 = "rel-1.3"; - private static final String BRANCH_2_0 = "rel-2.0"; - private static final String BRANCH_2_5 = "rel-2.5"; - - // Tag names - private static final String TAG_1_0 = "1.0"; - private static final String TAG_1_0_1 = "1.0.1"; - private static final String TAG_1_3 = "1.3"; - private static final String TAG_2_0_1 = "2.0.1"; - private static final String TAG_2_0 = "2.0"; - private static final String TAG_2_5 = "2.5"; - private static final String TAG_2_5_ANNOTATED = "2.5-annotated"; - private static final String TAG_2_5_ANNOTATED_TWICE = "2.5-annotated_twice"; - - // Commits - private RevCommit commit_initial; - private RevCommit commit_v1_3; - private RevCommit commit_v2_5; - - private TestRepository<?> tr; - - @Before - public void setUp() throws Exception { - tr = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("repo"))); - - /*- The following graph will be created. - - o tag 2.5, 2.5_annotated, 2.5_annotated_twice - |\ - | o tag 2.0.1 - | o tag 2.0 - o | tag 1.3 - |/ - o c3 - - | o tag 1.0.1 - |/ - o tag 1.0 - o c2 - o c1 - - */ - - // Version 1.0 - commit_initial = tr.branch(BRANCH_MASTER).commit().message("c1").create(); - tr.branch(BRANCH_MASTER).commit().message("c2").create(); - RevCommit commit_v1_0 = tr.branch(BRANCH_MASTER).commit().message("version 1.0").create(); - tag(TAG_1_0, commit_v1_0); - RevCommit c3 = tr.branch(BRANCH_MASTER).commit().message("c3").create(); - - // Version 1.01 - tr.branch(BRANCH_1_0).update(commit_v1_0); - RevCommit commit_v1_0_1 = tr.branch(BRANCH_1_0).commit().message("version 1.0.1").create(); - tag(TAG_1_0_1, commit_v1_0_1); - - // Version 1.3 - tr.branch(BRANCH_1_3).update(c3); - commit_v1_3 = tr.branch(BRANCH_1_3).commit().message("version 1.3").create(); - tag(TAG_1_3, commit_v1_3); - - // Version 2.0 - tr.branch(BRANCH_2_0).update(c3); - RevCommit commit_v2_0 = tr.branch(BRANCH_2_0).commit().message("version 2.0").create(); - tag(TAG_2_0, commit_v2_0); - RevCommit commit_v2_0_1 = tr.branch(BRANCH_2_0).commit().message("version 2.0.1").create(); - tag(TAG_2_0_1, commit_v2_0_1); - - // Version 2.5 - tr.branch(BRANCH_2_5).update(commit_v1_3); - tr.branch(BRANCH_2_5).commit().parent(commit_v2_0_1).create(); // Merge v2.0.1 - commit_v2_5 = tr.branch(BRANCH_2_5).commit().message("version 2.5").create(); - tr.update(REFS_TAGS + TAG_2_5, commit_v2_5); - RevTag tag_2_5_annotated = tag(TAG_2_5_ANNOTATED, commit_v2_5); - tag(TAG_2_5_ANNOTATED_TWICE, tag_2_5_annotated); - } - - @Test - public void resolveLatestCommit() throws Exception { - // Check tip commit - IncludedInResolver.Result detail = resolve(commit_v2_5); - - // Check that only tags and branches which refer the tip are returned - assertThat(detail.tags()) - .comparingElementsUsing(hasShortName()) - .containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE); - assertThat(detail.branches()) - .comparingElementsUsing(hasShortName()) - .containsExactly(BRANCH_2_5); - } - - @Test - public void resolveFirstCommit() throws Exception { - // Check first commit - IncludedInResolver.Result detail = resolve(commit_initial); - - // Check whether all tags and branches are returned - assertThat(detail.tags()) - .comparingElementsUsing(hasShortName()) - .containsExactly( - TAG_1_0, - TAG_1_0_1, - TAG_1_3, - TAG_2_0, - TAG_2_0_1, - TAG_2_5, - TAG_2_5_ANNOTATED, - TAG_2_5_ANNOTATED_TWICE); - assertThat(detail.branches()) - .comparingElementsUsing(hasShortName()) - .containsExactly(BRANCH_MASTER, BRANCH_1_0, BRANCH_1_3, BRANCH_2_0, BRANCH_2_5); - } - - @Test - public void resolveBetwixtCommit() throws Exception { - // Check a commit somewhere in the middle - IncludedInResolver.Result detail = resolve(commit_v1_3); - - // Check whether all succeeding tags and branches are returned - assertThat(detail.tags()) - .comparingElementsUsing(hasShortName()) - .containsExactly(TAG_1_3, TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE); - assertThat(detail.branches()) - .comparingElementsUsing(hasShortName()) - .containsExactly(BRANCH_1_3, BRANCH_2_5); - } - - private IncludedInResolver.Result resolve(RevCommit commit) throws Exception { - return IncludedInResolver.resolve(tr.getRepository(), tr.getRevWalk(), commit); - } - - private RevTag tag(String name, RevObject dest) throws Exception { - return tr.update(REFS_TAGS + name, tr.tag(name, dest)); - } - - private static Correspondence<Ref, String> hasShortName() { - return NullAwareCorrespondence.transforming( - ref -> Repository.shortenRefName(ref.getName()), "has short name"); - } -} diff --git a/modules/jgit b/modules/jgit -Subproject 1cbfea9ece03b40669377a7f858218f6994562e +Subproject 60b81c5a9280e44fa48d533a61f915382b2b9ce |