Line data Source code
1 : // Copyright (C) 2015 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.change; 16 : 17 : import com.google.common.collect.ListMultimap; 18 : import com.google.common.collect.MultimapBuilder; 19 : import com.google.gerrit.entities.Project; 20 : import com.google.gerrit.extensions.common.BlameInfo; 21 : import com.google.gerrit.extensions.common.RangeInfo; 22 : import com.google.gerrit.extensions.restapi.CacheControl; 23 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 24 : import com.google.gerrit.extensions.restapi.Response; 25 : import com.google.gerrit.extensions.restapi.RestApiException; 26 : import com.google.gerrit.extensions.restapi.RestReadView; 27 : import com.google.gerrit.server.change.FileResource; 28 : import com.google.gerrit.server.config.GerritServerConfig; 29 : import com.google.gerrit.server.git.GitRepositoryManager; 30 : import com.google.gerrit.server.git.InMemoryInserter; 31 : import com.google.gerrit.server.git.MergeUtil; 32 : import com.google.gerrit.server.patch.AutoMerger; 33 : import com.google.gerrit.server.project.InvalidChangeOperationException; 34 : import com.google.gitiles.blame.cache.BlameCache; 35 : import com.google.gitiles.blame.cache.Region; 36 : import com.google.inject.Inject; 37 : import java.io.IOException; 38 : import java.util.ArrayList; 39 : import java.util.List; 40 : import java.util.concurrent.TimeUnit; 41 : import org.eclipse.jgit.lib.Config; 42 : import org.eclipse.jgit.lib.ObjectId; 43 : import org.eclipse.jgit.lib.ObjectReader; 44 : import org.eclipse.jgit.lib.PersonIdent; 45 : import org.eclipse.jgit.lib.Ref; 46 : import org.eclipse.jgit.lib.Repository; 47 : import org.eclipse.jgit.merge.ThreeWayMergeStrategy; 48 : import org.eclipse.jgit.revwalk.RevCommit; 49 : import org.eclipse.jgit.revwalk.RevWalk; 50 : import org.kohsuke.args4j.Option; 51 : 52 : public class GetBlame implements RestReadView<FileResource> { 53 : 54 : private final GitRepositoryManager repoManager; 55 : private final BlameCache blameCache; 56 : private final ThreeWayMergeStrategy mergeStrategy; 57 : private final AutoMerger autoMerger; 58 : 59 : @Option( 60 : name = "--base", 61 : aliases = {"-b"}, 62 : usage = 63 : "whether to load the blame of the base revision (the direct" 64 : + " parent of the change) instead of the change") 65 : private boolean base; 66 : 67 : @Inject 68 : GetBlame( 69 : GitRepositoryManager repoManager, 70 : BlameCache blameCache, 71 : @GerritServerConfig Config cfg, 72 3 : AutoMerger autoMerger) { 73 3 : this.repoManager = repoManager; 74 3 : this.blameCache = blameCache; 75 3 : this.mergeStrategy = MergeUtil.getMergeStrategy(cfg); 76 3 : this.autoMerger = autoMerger; 77 3 : } 78 : 79 : public GetBlame setBase(boolean base) { 80 2 : this.base = base; 81 2 : return this; 82 : } 83 : 84 : @Override 85 : public Response<List<BlameInfo>> apply(FileResource resource) 86 : throws RestApiException, IOException, InvalidChangeOperationException { 87 3 : Project.NameKey project = resource.getRevision().getChange().getProject(); 88 3 : try (Repository repository = repoManager.openRepository(project); 89 3 : InMemoryInserter ins = new InMemoryInserter(repository); 90 3 : ObjectReader reader = ins.newReader(); 91 3 : RevWalk revWalk = new RevWalk(reader)) { 92 : String refName = 93 3 : resource.getRevision().getEdit().isPresent() 94 0 : ? resource.getRevision().getEdit().get().getRefName() 95 3 : : resource.getRevision().getPatchSet().refName(); 96 : 97 3 : Ref ref = repository.findRef(refName); 98 3 : if (ref == null) { 99 0 : throw new ResourceNotFoundException("unknown ref " + refName); 100 : } 101 3 : ObjectId objectId = ref.getObjectId(); 102 3 : RevCommit revCommit = revWalk.parseCommit(objectId); 103 3 : RevCommit[] parents = revCommit.getParents(); 104 : 105 3 : String path = resource.getPatchKey().fileName(); 106 : 107 : List<BlameInfo> result; 108 3 : if (!base) { 109 3 : result = blame(revCommit, path, repository, revWalk); 110 : 111 1 : } else if (parents.length == 0) { 112 0 : throw new ResourceNotFoundException("Initial commit doesn't have base"); 113 : 114 1 : } else if (parents.length == 1) { 115 1 : result = blame(parents[0], path, repository, revWalk); 116 : 117 0 : } else if (parents.length == 2) { 118 0 : ObjectId automerge = 119 0 : autoMerger.lookupFromGitOrMergeInMemory( 120 : repository, revWalk, ins, revCommit, mergeStrategy); 121 0 : result = blame(automerge, path, repository, revWalk); 122 : 123 0 : } else { 124 0 : throw new ResourceNotFoundException( 125 : "Cannot generate blame for merge commit with more than 2 parents"); 126 : } 127 : 128 3 : Response<List<BlameInfo>> r = Response.ok(result); 129 3 : if (resource.isCacheable()) { 130 1 : r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS)); 131 : } 132 3 : return r; 133 : } 134 : } 135 : 136 : private List<BlameInfo> blame(ObjectId id, String path, Repository repository, RevWalk revWalk) 137 : throws IOException { 138 : ListMultimap<BlameInfo, RangeInfo> ranges = 139 3 : MultimapBuilder.hashKeys().arrayListValues().build(); 140 3 : List<BlameInfo> result = new ArrayList<>(); 141 3 : if (blameCache.findLastCommit(repository, id, path) == null) { 142 1 : return result; 143 : } 144 : 145 3 : List<Region> blameRegions = blameCache.get(repository, id, path); 146 3 : int from = 1; 147 3 : for (Region region : blameRegions) { 148 3 : RevCommit commit = revWalk.parseCommit(region.getSourceCommit()); 149 3 : BlameInfo blameInfo = toBlameInfo(commit, region.getSourceAuthor()); 150 3 : ranges.put(blameInfo, new RangeInfo(from, from + region.getCount() - 1)); 151 3 : from += region.getCount(); 152 3 : } 153 : 154 3 : for (BlameInfo key : ranges.keySet()) { 155 3 : key.ranges = ranges.get(key); 156 3 : result.add(key); 157 3 : } 158 3 : return result; 159 : } 160 : 161 : private static BlameInfo toBlameInfo(RevCommit commit, PersonIdent sourceAuthor) { 162 3 : BlameInfo blameInfo = new BlameInfo(); 163 3 : blameInfo.author = sourceAuthor.getName(); 164 3 : blameInfo.id = commit.getName(); 165 3 : blameInfo.commitMsg = commit.getFullMessage(); 166 3 : blameInfo.time = commit.getCommitTime(); 167 3 : return blameInfo; 168 : } 169 : }