// 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 com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.CacheControl; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; 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.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountPatchReview; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.TimeUnit; public class Files implements ChildCollection<RevisionResource, FileResource> { private final DynamicMap<RestView<FileResource>> views; private final Provider<ListFiles> list; @Inject Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) { this.views = views; this.list = list; } @Override public DynamicMap<RestView<FileResource>> views() { return views; } @Override public RestView<RevisionResource> list() throws AuthException { return list.get(); } @Override public FileResource parse(RevisionResource rev, IdString id) throws ResourceNotFoundException, OrmException, AuthException { return new FileResource(rev, id.get()); } public static final class ListFiles implements RestReadView<RevisionResource> { private static final Logger log = LoggerFactory.getLogger(ListFiles.class); @Option(name = "--base", metaVar = "revision-id") String base; @Option(name = "--reviewed") boolean reviewed; private final Provider<ReviewDb> db; private final Provider<CurrentUser> self; private final FileInfoJson fileInfoJson; private final Revisions revisions; private final GitRepositoryManager gitManager; private final PatchListCache patchListCache; @Inject ListFiles(Provider<ReviewDb> db, Provider<CurrentUser> self, FileInfoJson fileInfoJson, Revisions revisions, GitRepositoryManager gitManager, PatchListCache patchListCache) { this.db = db; this.self = self; this.fileInfoJson = fileInfoJson; this.revisions = revisions; this.gitManager = gitManager; this.patchListCache = patchListCache; } public ListFiles setReviewed(boolean r) { this.reviewed = r; return this; } @Override public Response<?> apply(RevisionResource resource) throws AuthException, BadRequestException, ResourceNotFoundException, OrmException { if (base != null && reviewed) { throw new BadRequestException("cannot combine base and reviewed"); } else if (reviewed) { return Response.ok(reviewed(resource)); } PatchSet basePatchSet = null; if (base != null) { RevisionResource baseResource = revisions.parse( resource.getChangeResource(), IdString.fromDecoded(base)); basePatchSet = baseResource.getPatchSet(); } try { Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap( resource.getChange(), resource.getPatchSet(), basePatchSet)); if (resource.isCacheable()) { r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS)); } return r; } catch (PatchListNotAvailableException e) { throw new ResourceNotFoundException(e.getMessage()); } } private List<String> reviewed(RevisionResource resource) throws AuthException, OrmException { CurrentUser user = self.get(); if (!(user.isIdentifiedUser())) { throw new AuthException("Authentication required"); } Account.Id userId = ((IdentifiedUser) user).getAccountId(); List<String> r = scan(userId, resource.getPatchSet().getId()); if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) { for (Integer id : reverseSortPatchSets(resource)) { PatchSet.Id old = new PatchSet.Id(resource.getChange().getId(), id); List<String> o = scan(userId, old); if (!o.isEmpty()) { try { r = copy(Sets.newHashSet(o), old, resource, userId); } catch (IOException | PatchListNotAvailableException e) { log.warn("Cannot copy patch review flags", e); } break; } } } return r; } private List<String> scan(Account.Id userId, PatchSet.Id psId) throws OrmException { List<String> r = Lists.newArrayList(); for (AccountPatchReview w : db.get().accountPatchReviews() .byReviewer(userId, psId)) { r.add(w.getKey().getPatchKey().getFileName()); } return r; } private List<Integer> reverseSortPatchSets( RevisionResource resource) throws OrmException { SortedSet<Integer> ids = Sets.newTreeSet(); for (PatchSet p : db.get().patchSets() .byChange(resource.getChange().getId())) { if (p.getPatchSetId() < resource.getPatchSet().getPatchSetId()) { ids.add(p.getPatchSetId()); } } List<Integer> r = Lists.newArrayList(ids); Collections.reverse(r); return r; } private List<String> copy(Set<String> paths, PatchSet.Id old, RevisionResource resource, Account.Id userId) throws IOException, PatchListNotAvailableException, OrmException { Repository git = gitManager.openRepository(resource.getChange().getProject()); try { ObjectReader reader = git.newObjectReader(); try { PatchList oldList = patchListCache.get( resource.getChange(), db.get().patchSets().get(old)); PatchList curList = patchListCache.get( resource.getChange(), resource.getPatchSet()); int sz = paths.size(); List<AccountPatchReview> inserts = Lists.newArrayListWithCapacity(sz); List<String> pathList = Lists.newArrayListWithCapacity(sz); RevWalk rw = new RevWalk(reader); TreeWalk tw = new TreeWalk(reader); tw.setFilter(PathFilterGroup.createFromStrings(paths)); tw.setRecursive(true); int o = tw.addTree(rw.parseCommit(oldList.getNewId()).getTree()); int c = tw.addTree(rw.parseCommit(curList.getNewId()).getTree()); int op = -1; if (oldList.getOldId() != null) { op = tw.addTree(rw.parseTree(oldList.getOldId())); } int cp = -1; if (curList.getOldId() != null) { cp = tw.addTree(rw.parseTree(curList.getOldId())); } while (tw.next()) { String path = tw.getPathString(); if (tw.getRawMode(o) != 0 && tw.getRawMode(c) != 0 && tw.idEqual(o, c) && paths.contains(path)) { // File exists in previously reviewed oldList and in curList. // File content is identical. inserts.add(new AccountPatchReview( new Patch.Key( resource.getPatchSet().getId(), path), userId)); pathList.add(path); } else if (op >= 0 && cp >= 0 && tw.getRawMode(o) == 0 && tw.getRawMode(c) == 0 && tw.getRawMode(op) != 0 && tw.getRawMode(cp) != 0 && tw.idEqual(op, cp) && paths.contains(path)) { // File was deleted in previously reviewed oldList and curList. // File exists in ancestor of oldList and curList. // File content is identical in ancestors. inserts.add(new AccountPatchReview( new Patch.Key( resource.getPatchSet().getId(), path), userId)); pathList.add(path); } } db.get().accountPatchReviews().insert(inserts); return pathList; } finally { reader.release(); } } finally { git.close(); } } } }