Line data Source code
1 : // Copyright (C) 2014 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.collect.ImmutableList.toImmutableList; 18 : 19 : import com.google.common.collect.ImmutableList; 20 : import com.google.common.collect.ImmutableSet; 21 : import com.google.gerrit.entities.Project; 22 : import com.google.gerrit.entities.RefNames; 23 : import com.google.gerrit.extensions.registration.DynamicMap; 24 : import com.google.gerrit.extensions.restapi.ChildCollection; 25 : import com.google.gerrit.extensions.restapi.IdString; 26 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 27 : import com.google.gerrit.extensions.restapi.RestApiException; 28 : import com.google.gerrit.extensions.restapi.RestView; 29 : import com.google.gerrit.index.query.Predicate; 30 : import com.google.gerrit.server.git.GitRepositoryManager; 31 : import com.google.gerrit.server.index.change.ChangeIndexCollection; 32 : import com.google.gerrit.server.project.CommitResource; 33 : import com.google.gerrit.server.project.ProjectResource; 34 : import com.google.gerrit.server.project.ProjectState; 35 : import com.google.gerrit.server.project.Reachable; 36 : import com.google.gerrit.server.query.change.ChangeData; 37 : import com.google.gerrit.server.query.change.ChangePredicates; 38 : import com.google.gerrit.server.update.RetryHelper; 39 : import com.google.inject.Inject; 40 : import com.google.inject.Singleton; 41 : import java.io.IOException; 42 : import java.util.Arrays; 43 : import java.util.HashSet; 44 : import java.util.List; 45 : import java.util.Set; 46 : import java.util.stream.Collectors; 47 : import org.eclipse.jgit.errors.IncorrectObjectTypeException; 48 : import org.eclipse.jgit.errors.MissingObjectException; 49 : import org.eclipse.jgit.lib.ObjectId; 50 : import org.eclipse.jgit.lib.Ref; 51 : import org.eclipse.jgit.lib.RefDatabase; 52 : import org.eclipse.jgit.lib.Repository; 53 : import org.eclipse.jgit.revwalk.RevCommit; 54 : import org.eclipse.jgit.revwalk.RevWalk; 55 : 56 : /** The collection of commit IDs (ie. 40 char hex IDs) */ 57 : @Singleton 58 : public class CommitsCollection implements ChildCollection<ProjectResource, CommitResource> { 59 : private final DynamicMap<RestView<CommitResource>> views; 60 : private final GitRepositoryManager repoManager; 61 : private final RetryHelper retryHelper; 62 : private final ChangeIndexCollection indexes; 63 : private final Reachable reachable; 64 : 65 : @Inject 66 : public CommitsCollection( 67 : DynamicMap<RestView<CommitResource>> views, 68 : GitRepositoryManager repoManager, 69 : RetryHelper retryHelper, 70 : ChangeIndexCollection indexes, 71 149 : Reachable reachable) { 72 149 : this.views = views; 73 149 : this.repoManager = repoManager; 74 149 : this.retryHelper = retryHelper; 75 149 : this.indexes = indexes; 76 149 : this.reachable = reachable; 77 149 : } 78 : 79 : @Override 80 : public RestView<ProjectResource> list() throws ResourceNotFoundException { 81 1 : throw new ResourceNotFoundException(); 82 : } 83 : 84 : @Override 85 : public CommitResource parse(ProjectResource parent, IdString id) 86 : throws RestApiException, IOException { 87 6 : parent.getProjectState().checkStatePermitsRead(); 88 : ObjectId objectId; 89 : try { 90 6 : objectId = ObjectId.fromString(id.get()); 91 0 : } catch (IllegalArgumentException e) { 92 0 : throw new ResourceNotFoundException(id, e); 93 6 : } 94 : 95 6 : try (Repository repo = repoManager.openRepository(parent.getNameKey()); 96 6 : RevWalk rw = new RevWalk(repo)) { 97 6 : RevCommit commit = rw.parseCommit(objectId); 98 6 : if (!canRead(parent.getProjectState(), repo, commit)) { 99 0 : throw new ResourceNotFoundException(id); 100 : } 101 : // GetCommit depends on the body of both the commit and parent being parsed, to get the 102 : // subject. 103 6 : rw.parseBody(commit); 104 6 : for (int i = 0; i < commit.getParentCount(); i++) { 105 6 : rw.parseBody(rw.parseCommit(commit.getParent(i))); 106 : } 107 6 : return new CommitResource(parent, commit); 108 0 : } catch (MissingObjectException | IncorrectObjectTypeException e) { 109 0 : throw new ResourceNotFoundException(id, e); 110 : } 111 : } 112 : 113 : @Override 114 : public DynamicMap<RestView<CommitResource>> views() { 115 3 : return views; 116 : } 117 : 118 : /** 119 : * Returns true if {@code commit} is visible to the caller and {@code commit} is reachable from 120 : * the given branch. 121 : */ 122 : public boolean canRead(ProjectState state, Repository repo, RevCommit commit, Ref ref) { 123 1 : return reachable.fromRefs(state.getNameKey(), repo, commit, ImmutableList.of(ref)); 124 : } 125 : 126 : /** Returns true if {@code commit} is visible to the caller. */ 127 : public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException { 128 11 : Project.NameKey project = state.getNameKey(); 129 11 : if (indexes.getSearchIndex() == null) { 130 : // No index in slaves, fall back to scanning refs. We must inspect change refs too 131 : // as the commit might be a patchset of a not yet submitted change. 132 0 : return reachable.fromRefs(project, repo, commit, repo.getRefDatabase().getRefs()); 133 : } 134 : 135 : // Check first if any patchset of any change references the commit in question. This is much 136 : // cheaper than ref visibility filtering and reachability computation. 137 11 : List<ChangeData> changes = 138 : retryHelper 139 11 : .changeIndexQuery( 140 : "queryChangesByProjectCommitWithLimit1", 141 11 : q -> q.enforceVisibility(true).setLimit(1).byProjectCommit(project, commit)) 142 11 : .call(); 143 11 : if (!changes.isEmpty()) { 144 9 : return true; 145 : } 146 : 147 : // Maybe the commit was a merge commit of a change. Try to find promising candidates for 148 : // branches to check, by seeing if its parents were associated to changes. 149 7 : Predicate<ChangeData> pred = 150 7 : Predicate.and( 151 7 : ChangePredicates.project(project), 152 7 : Predicate.or( 153 7 : Arrays.stream(commit.getParents()) 154 7 : .map(parent -> ChangePredicates.commitPrefix(parent.getId().getName())) 155 7 : .collect(toImmutableList()))); 156 7 : changes = 157 : retryHelper 158 7 : .changeIndexQuery( 159 7 : "queryChangesByProjectCommit", q -> q.enforceVisibility(true).query(pred)) 160 7 : .call(); 161 : 162 7 : Set<Ref> branchesForCommitParents = new HashSet<>(changes.size()); 163 7 : for (ChangeData cd : changes) { 164 1 : Ref ref = repo.exactRef(cd.change().getDest().branch()); 165 1 : if (ref != null) { 166 1 : branchesForCommitParents.add(ref); 167 : } 168 1 : } 169 : 170 7 : if (reachable.fromRefs( 171 7 : project, repo, commit, branchesForCommitParents.stream().collect(Collectors.toList()))) { 172 1 : return true; 173 : } 174 : 175 : // If we have already checked change refs using the change index, spare any further checks for 176 : // changes. 177 7 : List<Ref> refs = 178 7 : repo.getRefDatabase() 179 7 : .getRefsByPrefixWithExclusions(RefDatabase.ALL, ImmutableSet.of(RefNames.REFS_CHANGES)); 180 7 : return reachable.fromRefs(project, repo, commit, refs); 181 : } 182 : }