LCOV - code coverage report
Current view: top level - server/restapi/change - Files.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 140 157 89.2 %
Date: 2022-11-19 15:00:39 Functions: 20 20 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2013 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.Iterables;
      18             : import com.google.common.collect.Lists;
      19             : import com.google.common.flogger.FluentLogger;
      20             : import com.google.common.hash.Hasher;
      21             : import com.google.common.hash.Hashing;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.Account;
      24             : import com.google.gerrit.entities.Change;
      25             : import com.google.gerrit.entities.PatchSet;
      26             : import com.google.gerrit.entities.Project;
      27             : import com.google.gerrit.extensions.api.GerritApi;
      28             : import com.google.gerrit.extensions.common.FileInfo;
      29             : import com.google.gerrit.extensions.registration.DynamicMap;
      30             : import com.google.gerrit.extensions.restapi.AuthException;
      31             : import com.google.gerrit.extensions.restapi.BadRequestException;
      32             : import com.google.gerrit.extensions.restapi.CacheControl;
      33             : import com.google.gerrit.extensions.restapi.ChildCollection;
      34             : import com.google.gerrit.extensions.restapi.ETagView;
      35             : import com.google.gerrit.extensions.restapi.IdString;
      36             : import com.google.gerrit.extensions.restapi.Response;
      37             : import com.google.gerrit.extensions.restapi.RestApiException;
      38             : import com.google.gerrit.extensions.restapi.RestView;
      39             : import com.google.gerrit.server.CurrentUser;
      40             : import com.google.gerrit.server.PatchSetUtil;
      41             : import com.google.gerrit.server.change.AccountPatchReviewStore;
      42             : import com.google.gerrit.server.change.AccountPatchReviewStore.PatchSetWithReviewedFiles;
      43             : import com.google.gerrit.server.change.FileInfoJson;
      44             : import com.google.gerrit.server.change.FileResource;
      45             : import com.google.gerrit.server.change.RevisionResource;
      46             : import com.google.gerrit.server.git.GitRepositoryManager;
      47             : import com.google.gerrit.server.patch.DiffNotAvailableException;
      48             : import com.google.gerrit.server.patch.DiffOperations;
      49             : import com.google.gerrit.server.patch.DiffOptions;
      50             : import com.google.gerrit.server.patch.PatchListKey;
      51             : import com.google.gerrit.server.patch.PatchListNotAvailableException;
      52             : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
      53             : import com.google.gerrit.server.permissions.PermissionBackendException;
      54             : import com.google.gerrit.server.plugincontext.PluginItemContext;
      55             : import com.google.inject.Inject;
      56             : import com.google.inject.Provider;
      57             : import com.google.inject.Singleton;
      58             : import java.io.IOException;
      59             : import java.util.ArrayList;
      60             : import java.util.Collection;
      61             : import java.util.Collections;
      62             : import java.util.List;
      63             : import java.util.Map;
      64             : import java.util.Optional;
      65             : import java.util.Set;
      66             : import java.util.concurrent.TimeUnit;
      67             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      68             : import org.eclipse.jgit.lib.ObjectId;
      69             : import org.eclipse.jgit.lib.ObjectReader;
      70             : import org.eclipse.jgit.lib.Repository;
      71             : import org.eclipse.jgit.revwalk.RevCommit;
      72             : import org.eclipse.jgit.revwalk.RevWalk;
      73             : import org.eclipse.jgit.treewalk.TreeWalk;
      74             : import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
      75             : import org.kohsuke.args4j.Option;
      76             : 
      77             : @Singleton
      78             : public class Files implements ChildCollection<RevisionResource, FileResource> {
      79             :   private final DynamicMap<RestView<FileResource>> views;
      80             :   private final Provider<ListFiles> list;
      81             : 
      82             :   @Inject
      83         145 :   Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
      84         145 :     this.views = views;
      85         145 :     this.list = list;
      86         145 :   }
      87             : 
      88             :   @Override
      89             :   public DynamicMap<RestView<FileResource>> views() {
      90           4 :     return views;
      91             :   }
      92             : 
      93             :   @Override
      94             :   public RestView<RevisionResource> list() throws AuthException {
      95           3 :     return list.get();
      96             :   }
      97             : 
      98             :   @Override
      99             :   public FileResource parse(RevisionResource rev, IdString id) {
     100          16 :     return new FileResource(rev, id.get());
     101             :   }
     102             : 
     103             :   public static final class ListFiles implements ETagView<RevisionResource> {
     104          79 :     private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     105             : 
     106             :     @Option(name = "--base", metaVar = "revision-id")
     107             :     String base;
     108             : 
     109             :     @Option(name = "--parent", metaVar = "parent-number")
     110             :     int parentNum;
     111             : 
     112             :     @Option(name = "--reviewed")
     113             :     boolean reviewed;
     114             : 
     115             :     @Option(name = "-q")
     116             :     String query;
     117             : 
     118             :     private final DiffOperations diffOperations;
     119             :     private final Provider<CurrentUser> self;
     120             :     private final FileInfoJson fileInfoJson;
     121             :     private final Revisions revisions;
     122             :     private final GitRepositoryManager gitManager;
     123             :     private final PatchSetUtil psUtil;
     124             :     private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
     125             :     private final GerritApi gApi;
     126             : 
     127             :     @Inject
     128             :     ListFiles(
     129             :         DiffOperations diffOperations,
     130             :         Provider<CurrentUser> self,
     131             :         FileInfoJson fileInfoJson,
     132             :         Revisions revisions,
     133             :         GitRepositoryManager gitManager,
     134             :         PatchSetUtil psUtil,
     135             :         PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
     136          79 :         GerritApi gApi) {
     137          79 :       this.diffOperations = diffOperations;
     138          79 :       this.self = self;
     139          79 :       this.fileInfoJson = fileInfoJson;
     140          79 :       this.revisions = revisions;
     141          79 :       this.gitManager = gitManager;
     142          79 :       this.psUtil = psUtil;
     143          79 :       this.accountPatchReviewStore = accountPatchReviewStore;
     144          79 :       this.gApi = gApi;
     145          79 :     }
     146             : 
     147             :     public ListFiles setReviewed(boolean r) {
     148           1 :       this.reviewed = r;
     149           1 :       return this;
     150             :     }
     151             : 
     152             :     @Override
     153             :     public Response<?> apply(RevisionResource resource)
     154             :         throws RestApiException, RepositoryNotFoundException, IOException,
     155             :             PatchListNotAvailableException, PermissionBackendException {
     156          10 :       checkOptions();
     157          10 :       if (reviewed) {
     158           1 :         return Response.ok(reviewed(resource));
     159          10 :       } else if (query != null) {
     160           1 :         return Response.ok(query(resource));
     161             :       }
     162             : 
     163             :       Response<Map<String, FileInfo>> r;
     164          10 :       if (base != null) {
     165           3 :         RevisionResource baseResource =
     166           3 :             revisions.parse(resource.getChangeResource(), IdString.fromDecoded(base));
     167           3 :         r =
     168           3 :             Response.ok(
     169           3 :                 fileInfoJson.getFileInfoMap(
     170           3 :                     resource.getChange(),
     171           3 :                     resource.getPatchSet().commitId(),
     172           3 :                     baseResource.getPatchSet()));
     173          10 :       } else if (parentNum != 0) {
     174           5 :         int parents =
     175           5 :             gApi.changes()
     176           5 :                 .id(resource.getChange().getChangeId())
     177           5 :                 .revision(resource.getPatchSet().id().get())
     178           5 :                 .commit(false)
     179             :                 .parents
     180           5 :                 .size();
     181           5 :         if (parentNum < 0 || parentNum > parents) {
     182           1 :           throw new BadRequestException(String.format("invalid parent number: %d", parentNum));
     183             :         }
     184           5 :         r =
     185           5 :             Response.ok(
     186           5 :                 fileInfoJson.getFileInfoMap(
     187           5 :                     resource.getChange(), resource.getPatchSet().commitId(), parentNum));
     188           5 :       } else {
     189          10 :         r = Response.ok(fileInfoJson.getFileInfoMap(resource.getChange(), resource.getPatchSet()));
     190             :       }
     191             : 
     192          10 :       if (resource.isCacheable()) {
     193           5 :         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
     194             :       }
     195          10 :       return r;
     196             :     }
     197             : 
     198             :     private void checkOptions() throws BadRequestException {
     199          10 :       int supplied = 0;
     200          10 :       if (base != null) {
     201           3 :         supplied++;
     202             :       }
     203          10 :       if (parentNum > 0) {
     204           5 :         supplied++;
     205             :       }
     206          10 :       if (reviewed) {
     207           1 :         supplied++;
     208             :       }
     209          10 :       if (query != null) {
     210           1 :         supplied++;
     211             :       }
     212          10 :       if (supplied > 1) {
     213           0 :         throw new BadRequestException("cannot combine base, parent, reviewed, query");
     214             :       }
     215          10 :     }
     216             : 
     217             :     private List<String> query(RevisionResource resource)
     218             :         throws RepositoryNotFoundException, IOException {
     219           1 :       Project.NameKey project = resource.getChange().getProject();
     220           1 :       try (Repository git = gitManager.openRepository(project);
     221           1 :           ObjectReader or = git.newObjectReader();
     222           1 :           RevWalk rw = new RevWalk(or);
     223           1 :           TreeWalk tw = new TreeWalk(or)) {
     224           1 :         RevCommit c = rw.parseCommit(resource.getPatchSet().commitId());
     225             : 
     226           1 :         tw.addTree(c.getTree());
     227           1 :         tw.setRecursive(true);
     228           1 :         List<String> paths = new ArrayList<>();
     229           1 :         while (tw.next() && paths.size() < 20) {
     230           1 :           String s = tw.getPathString();
     231           1 :           if (s.contains(query)) {
     232           1 :             paths.add(s);
     233             :           }
     234           1 :         }
     235           1 :         return paths;
     236             :       }
     237             :     }
     238             : 
     239             :     private Collection<String> reviewed(RevisionResource resource) throws AuthException {
     240           1 :       CurrentUser user = self.get();
     241           1 :       if (!user.isIdentifiedUser()) {
     242           0 :         throw new AuthException("Authentication required");
     243             :       }
     244             : 
     245           1 :       Account.Id userId = user.getAccountId();
     246           1 :       PatchSet patchSetId = resource.getPatchSet();
     247             :       Optional<PatchSetWithReviewedFiles> o;
     248           1 :       o = accountPatchReviewStore.call(s -> s.findReviewed(patchSetId.id(), userId));
     249             : 
     250           1 :       if (o.isPresent()) {
     251           1 :         PatchSetWithReviewedFiles res = o.get();
     252           1 :         if (res.patchSetId().equals(patchSetId.id())) {
     253           1 :           return res.files();
     254             :         }
     255             : 
     256             :         try {
     257           1 :           return copy(res.files(), res.patchSetId(), resource, userId);
     258           0 :         } catch (IOException | DiffNotAvailableException e) {
     259           0 :           logger.atWarning().withCause(e).log("Cannot copy patch review flags");
     260             :         }
     261             :       }
     262             : 
     263           1 :       return Collections.emptyList();
     264             :     }
     265             : 
     266             :     private List<String> copy(
     267             :         Set<String> paths, PatchSet.Id old, RevisionResource resource, Account.Id userId)
     268             :         throws IOException, DiffNotAvailableException {
     269           1 :       Project.NameKey project = resource.getChange().getProject();
     270           1 :       try (Repository git = gitManager.openRepository(project);
     271           1 :           ObjectReader reader = git.newObjectReader();
     272           1 :           RevWalk rw = new RevWalk(reader);
     273           1 :           TreeWalk tw = new TreeWalk(reader)) {
     274           1 :         Change change = resource.getChange();
     275           1 :         PatchSet patchSet = psUtil.get(resource.getNotes(), old);
     276           1 :         if (patchSet == null) {
     277           0 :           throw new DiffNotAvailableException(
     278           0 :               String.format(
     279           0 :                   "patch set %s of change %s not found", old.get(), change.getId().get()));
     280             :         }
     281             : 
     282           1 :         Map<String, FileDiffOutput> oldList =
     283           1 :             diffOperations.listModifiedFilesAgainstParent(
     284           1 :                 project, patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
     285             : 
     286           1 :         Map<String, FileDiffOutput> curList =
     287           1 :             diffOperations.listModifiedFilesAgainstParent(
     288             :                 project,
     289           1 :                 resource.getPatchSet().commitId(),
     290             :                 /* parentNum= */ 0,
     291             :                 DiffOptions.DEFAULTS);
     292             : 
     293           1 :         int sz = paths.size();
     294           1 :         List<String> pathList = Lists.newArrayListWithCapacity(sz);
     295             : 
     296           1 :         tw.setFilter(PathFilterGroup.createFromStrings(paths));
     297           1 :         tw.setRecursive(true);
     298           1 :         int o = tw.addTree(rw.parseCommit(getNewId(oldList)).getTree());
     299           1 :         int c = tw.addTree(rw.parseCommit(getNewId(curList)).getTree());
     300             : 
     301           1 :         int op = -1;
     302           1 :         if (getOldId(oldList) != null) {
     303           1 :           op = tw.addTree(rw.parseTree(getOldId(oldList)));
     304             :         }
     305             : 
     306           1 :         int cp = -1;
     307           1 :         if (getOldId(curList) != null) {
     308           1 :           cp = tw.addTree(rw.parseTree(getOldId(curList)));
     309             :         }
     310             : 
     311           1 :         while (tw.next()) {
     312           1 :           String path = tw.getPathString();
     313           1 :           if (tw.getRawMode(o) != 0
     314           1 :               && tw.getRawMode(c) != 0
     315           1 :               && tw.idEqual(o, c)
     316           0 :               && paths.contains(path)) {
     317             :             // File exists in previously reviewed oldList and in curList.
     318             :             // File content is identical.
     319           0 :             pathList.add(path);
     320           1 :           } else if (op >= 0
     321             :               && cp >= 0
     322           1 :               && tw.getRawMode(o) == 0
     323           0 :               && tw.getRawMode(c) == 0
     324           0 :               && tw.getRawMode(op) != 0
     325           0 :               && tw.getRawMode(cp) != 0
     326           0 :               && tw.idEqual(op, cp)
     327           0 :               && paths.contains(path)) {
     328             :             // File was deleted in previously reviewed oldList and curList.
     329             :             // File exists in ancestor of oldList and curList.
     330             :             // File content is identical in ancestors.
     331           0 :             pathList.add(path);
     332             :           }
     333           1 :         }
     334             : 
     335           1 :         accountPatchReviewStore.run(
     336           1 :             s -> s.markReviewed(resource.getPatchSet().id(), userId, pathList));
     337           1 :         return pathList;
     338             :       }
     339             :     }
     340             : 
     341             :     public ListFiles setQuery(String query) {
     342           1 :       this.query = query;
     343           1 :       return this;
     344             :     }
     345             : 
     346             :     public ListFiles setBase(@Nullable String base) {
     347           7 :       this.base = base;
     348           7 :       return this;
     349             :     }
     350             : 
     351             :     public ListFiles setParent(int parentNum) {
     352           4 :       this.parentNum = parentNum;
     353           4 :       return this;
     354             :     }
     355             : 
     356             :     @Override
     357             :     public String getETag(RevisionResource resource) {
     358           2 :       Hasher h = Hashing.murmur3_128().newHasher();
     359           2 :       resource.prepareETag(h, resource.getUser());
     360             :       // File list comes from the PatchListCache, so any change to the key or value should
     361             :       // invalidate ETag.
     362           2 :       h.putLong(PatchListKey.serialVersionUID);
     363           2 :       return h.hash().toString();
     364             :     }
     365             : 
     366             :     @Nullable
     367             :     private ObjectId getOldId(Map<String, FileDiffOutput> fileDiffList) {
     368           1 :       return fileDiffList.isEmpty()
     369           0 :           ? null
     370           1 :           : Iterables.getFirst(fileDiffList.values(), null).oldCommitId();
     371             :     }
     372             : 
     373             :     @Nullable
     374             :     private ObjectId getNewId(Map<String, FileDiffOutput> fileDiffList) {
     375           1 :       return fileDiffList.isEmpty()
     376           0 :           ? null
     377           1 :           : Iterables.getFirst(fileDiffList.values(), null).newCommitId();
     378             :     }
     379             :   }
     380             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750