LCOV - code coverage report
Current view: top level - server/change - RevisionJson.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 124 149 83.2 %
Date: 2022-11-19 15:00:39 Functions: 10 11 90.9 %

          Line data    Source code
       1             : // Copyright (C) 2018 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.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
      19             : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
      20             : import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
      21             : import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
      22             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
      23             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
      24             : import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
      25             : import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
      26             : import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
      27             : import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
      28             : import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
      29             : import static com.google.gerrit.server.CommonConverters.toGitPerson;
      30             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      31             : 
      32             : import com.google.common.collect.ImmutableList;
      33             : import com.google.common.collect.ImmutableSet;
      34             : import com.google.common.flogger.FluentLogger;
      35             : import com.google.gerrit.common.Nullable;
      36             : import com.google.gerrit.entities.Change;
      37             : import com.google.gerrit.entities.Patch;
      38             : import com.google.gerrit.entities.PatchSet;
      39             : import com.google.gerrit.entities.Project;
      40             : import com.google.gerrit.extensions.client.ListChangesOption;
      41             : import com.google.gerrit.extensions.common.ChangeInfo;
      42             : import com.google.gerrit.extensions.common.CommitInfo;
      43             : import com.google.gerrit.extensions.common.FetchInfo;
      44             : import com.google.gerrit.extensions.common.PushCertificateInfo;
      45             : import com.google.gerrit.extensions.common.RevisionInfo;
      46             : import com.google.gerrit.extensions.common.WebLinkInfo;
      47             : import com.google.gerrit.extensions.config.DownloadCommand;
      48             : import com.google.gerrit.extensions.config.DownloadScheme;
      49             : import com.google.gerrit.extensions.registration.DynamicMap;
      50             : import com.google.gerrit.extensions.registration.Extension;
      51             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      52             : import com.google.gerrit.server.AnonymousUser;
      53             : import com.google.gerrit.server.CurrentUser;
      54             : import com.google.gerrit.server.GpgException;
      55             : import com.google.gerrit.server.IdentifiedUser;
      56             : import com.google.gerrit.server.WebLinks;
      57             : import com.google.gerrit.server.account.AccountLoader;
      58             : import com.google.gerrit.server.account.GpgApiAdapter;
      59             : import com.google.gerrit.server.git.GitRepositoryManager;
      60             : import com.google.gerrit.server.git.MergeUtilFactory;
      61             : import com.google.gerrit.server.patch.PatchListNotAvailableException;
      62             : import com.google.gerrit.server.permissions.ChangePermission;
      63             : import com.google.gerrit.server.permissions.PermissionBackend;
      64             : import com.google.gerrit.server.permissions.PermissionBackendException;
      65             : import com.google.gerrit.server.project.ProjectCache;
      66             : import com.google.gerrit.server.project.ProjectState;
      67             : import com.google.gerrit.server.query.change.ChangeData;
      68             : import com.google.inject.Inject;
      69             : import com.google.inject.Provider;
      70             : import com.google.inject.assistedinject.Assisted;
      71             : import java.io.IOException;
      72             : import java.util.ArrayList;
      73             : import java.util.LinkedHashMap;
      74             : import java.util.Map;
      75             : import java.util.Optional;
      76             : import org.eclipse.jgit.lib.ObjectId;
      77             : import org.eclipse.jgit.lib.Ref;
      78             : import org.eclipse.jgit.lib.Repository;
      79             : import org.eclipse.jgit.revwalk.RevCommit;
      80             : import org.eclipse.jgit.revwalk.RevWalk;
      81             : 
      82             : /** Produces {@link RevisionInfo} and {@link CommitInfo} which are serialized to JSON afterwards. */
      83             : public class RevisionJson {
      84         103 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      85             : 
      86             :   public interface Factory {
      87             :     RevisionJson create(Iterable<ListChangesOption> options);
      88             :   }
      89             : 
      90             :   private final MergeUtilFactory mergeUtilFactory;
      91             :   private final IdentifiedUser.GenericFactory userFactory;
      92             :   private final FileInfoJson fileInfoJson;
      93             :   private final GpgApiAdapter gpgApi;
      94             :   private final ChangeResource.Factory changeResourceFactory;
      95             :   private final ChangeKindCache changeKindCache;
      96             :   private final ActionJson actionJson;
      97             :   private final DynamicMap<DownloadScheme> downloadSchemes;
      98             :   private final DynamicMap<DownloadCommand> downloadCommands;
      99             :   private final WebLinks webLinks;
     100             :   private final Provider<CurrentUser> userProvider;
     101             :   private final ProjectCache projectCache;
     102             :   private final ImmutableSet<ListChangesOption> options;
     103             :   private final AccountLoader.Factory accountLoaderFactory;
     104             :   private final AnonymousUser anonymous;
     105             :   private final GitRepositoryManager repoManager;
     106             :   private final PermissionBackend permissionBackend;
     107             : 
     108             :   @Inject
     109             :   RevisionJson(
     110             :       Provider<CurrentUser> userProvider,
     111             :       AnonymousUser anonymous,
     112             :       ProjectCache projectCache,
     113             :       IdentifiedUser.GenericFactory userFactory,
     114             :       MergeUtilFactory mergeUtilFactory,
     115             :       FileInfoJson fileInfoJson,
     116             :       AccountLoader.Factory accountLoaderFactory,
     117             :       DynamicMap<DownloadScheme> downloadSchemes,
     118             :       DynamicMap<DownloadCommand> downloadCommands,
     119             :       WebLinks webLinks,
     120             :       ActionJson actionJson,
     121             :       GpgApiAdapter gpgApi,
     122             :       ChangeResource.Factory changeResourceFactory,
     123             :       ChangeKindCache changeKindCache,
     124             :       GitRepositoryManager repoManager,
     125             :       PermissionBackend permissionBackend,
     126         103 :       @Assisted Iterable<ListChangesOption> options) {
     127         103 :     this.userProvider = userProvider;
     128         103 :     this.anonymous = anonymous;
     129         103 :     this.projectCache = projectCache;
     130         103 :     this.userFactory = userFactory;
     131         103 :     this.mergeUtilFactory = mergeUtilFactory;
     132         103 :     this.fileInfoJson = fileInfoJson;
     133         103 :     this.accountLoaderFactory = accountLoaderFactory;
     134         103 :     this.downloadSchemes = downloadSchemes;
     135         103 :     this.downloadCommands = downloadCommands;
     136         103 :     this.webLinks = webLinks;
     137         103 :     this.actionJson = actionJson;
     138         103 :     this.gpgApi = gpgApi;
     139         103 :     this.changeResourceFactory = changeResourceFactory;
     140         103 :     this.changeKindCache = changeKindCache;
     141         103 :     this.permissionBackend = permissionBackend;
     142         103 :     this.repoManager = repoManager;
     143         103 :     this.options = ImmutableSet.copyOf(options);
     144         103 :   }
     145             : 
     146             :   /**
     147             :    * Returns a {@link RevisionInfo} based on a change and patch set. Reads from the repository
     148             :    * depending on the options provided when constructing this instance.
     149             :    */
     150             :   public RevisionInfo getRevisionInfo(ChangeData cd, PatchSet in)
     151             :       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     152          99 :     AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
     153          99 :     try (Repository repo = openRepoIfNecessary(cd.project());
     154          99 :         RevWalk rw = newRevWalk(repo)) {
     155          99 :       RevisionInfo rev = toRevisionInfo(accountLoader, cd, in, repo, rw, true, null);
     156          99 :       accountLoader.fill();
     157          99 :       return rev;
     158             :     }
     159             :   }
     160             : 
     161             :   /**
     162             :    * Returns a {@link CommitInfo} based on a commit and formatting options. Uses the provided
     163             :    * RevWalk and assumes it is backed by an open repository.
     164             :    */
     165             :   public CommitInfo getCommitInfo(
     166             :       Project.NameKey project,
     167             :       RevWalk rw,
     168             :       RevCommit commit,
     169             :       boolean addLinks,
     170             :       boolean fillCommit,
     171             :       String branchName,
     172             :       String changeKey)
     173             :       throws IOException {
     174         103 :     CommitInfo info = new CommitInfo();
     175         103 :     if (fillCommit) {
     176          99 :       info.commit = commit.name();
     177             :     }
     178         103 :     info.parents = new ArrayList<>(commit.getParentCount());
     179         103 :     info.author = toGitPerson(commit.getAuthorIdent());
     180         103 :     info.committer = toGitPerson(commit.getCommitterIdent());
     181         103 :     info.subject = commit.getShortMessage();
     182         103 :     info.message = commit.getFullMessage();
     183             : 
     184         103 :     if (addLinks) {
     185         103 :       ImmutableList<WebLinkInfo> patchSetLinks =
     186         103 :           webLinks.getPatchSetLinks(
     187         103 :               project, commit.name(), commit.getFullMessage(), branchName, changeKey);
     188         103 :       info.webLinks = patchSetLinks.isEmpty() ? null : patchSetLinks;
     189         103 :       ImmutableList<WebLinkInfo> resolveConflictsLinks =
     190         103 :           webLinks.getResolveConflictsLinks(
     191         103 :               project, commit.name(), commit.getFullMessage(), branchName);
     192         103 :       info.resolveConflictsWebLinks =
     193         103 :           resolveConflictsLinks.isEmpty() ? null : resolveConflictsLinks;
     194             :     }
     195             : 
     196         103 :     for (RevCommit parent : commit.getParents()) {
     197          98 :       rw.parseBody(parent);
     198          98 :       CommitInfo i = new CommitInfo();
     199          98 :       i.commit = parent.name();
     200          98 :       i.subject = parent.getShortMessage();
     201          98 :       if (addLinks) {
     202          98 :         ImmutableList<WebLinkInfo> parentLinks =
     203          98 :             webLinks.getParentLinks(project, parent.name(), parent.getFullMessage(), branchName);
     204          98 :         i.webLinks = parentLinks.isEmpty() ? null : parentLinks;
     205             :       }
     206          98 :       info.parents.add(i);
     207             :     }
     208         103 :     return info;
     209             :   }
     210             : 
     211             :   /**
     212             :    * Returns multiple {@link RevisionInfo}s for a single change. Uses the provided {@link
     213             :    * AccountLoader} to lazily populate accounts. Callers have to call {@link AccountLoader#fill()}
     214             :    * afterwards to populate all accounts in the returned {@link RevisionInfo}s.
     215             :    */
     216             :   Map<String, RevisionInfo> getRevisions(
     217             :       AccountLoader accountLoader,
     218             :       ChangeData cd,
     219             :       Map<PatchSet.Id, PatchSet> map,
     220             :       Optional<PatchSet.Id> limitToPsId,
     221             :       ChangeInfo changeInfo)
     222             :       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     223         103 :     Map<String, RevisionInfo> res = new LinkedHashMap<>();
     224         103 :     try (Repository repo = openRepoIfNecessary(cd.project());
     225         103 :         RevWalk rw = newRevWalk(repo)) {
     226         103 :       for (PatchSet in : map.values()) {
     227         103 :         PatchSet.Id id = in.id();
     228             :         boolean want;
     229         103 :         if (has(ALL_REVISIONS)) {
     230         103 :           want = true;
     231          28 :         } else if (limitToPsId.isPresent()) {
     232           3 :           want = id.equals(limitToPsId.get());
     233             :         } else {
     234          26 :           want = id.equals(cd.change().currentPatchSetId());
     235             :         }
     236         103 :         if (want) {
     237         103 :           res.put(
     238         103 :               in.commitId().name(),
     239         103 :               toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
     240             :         }
     241         103 :       }
     242         103 :       return res;
     243             :     }
     244             :   }
     245             : 
     246             :   private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
     247             :       throws PermissionBackendException {
     248         103 :     Map<String, FetchInfo> r = new LinkedHashMap<>();
     249         103 :     for (Extension<DownloadScheme> e : downloadSchemes) {
     250           0 :       String schemeName = e.getExportName();
     251           0 :       DownloadScheme scheme = e.getProvider().get();
     252           0 :       if (!scheme.isEnabled()
     253           0 :           || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
     254           0 :         continue;
     255             :       }
     256           0 :       if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
     257           0 :         continue;
     258             :       }
     259             : 
     260           0 :       String projectName = cd.project().get();
     261           0 :       String url = scheme.getUrl(projectName);
     262           0 :       String refName = in.refName();
     263           0 :       FetchInfo fetchInfo = new FetchInfo(url, refName);
     264           0 :       r.put(schemeName, fetchInfo);
     265             : 
     266           0 :       if (has(DOWNLOAD_COMMANDS)) {
     267           0 :         DownloadCommandsJson.populateFetchMap(
     268             :             scheme, downloadCommands, projectName, refName, fetchInfo);
     269             :       }
     270           0 :     }
     271             : 
     272         103 :     return r;
     273             :   }
     274             : 
     275             :   private RevisionInfo toRevisionInfo(
     276             :       AccountLoader accountLoader,
     277             :       ChangeData cd,
     278             :       PatchSet in,
     279             :       @Nullable Repository repo,
     280             :       @Nullable RevWalk rw,
     281             :       boolean fillCommit,
     282             :       @Nullable ChangeInfo changeInfo)
     283             :       throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
     284         103 :     Change c = cd.change();
     285         103 :     RevisionInfo out = new RevisionInfo();
     286         103 :     out.isCurrent = in.id().equals(c.currentPatchSetId());
     287         103 :     out._number = in.id().get();
     288         103 :     out.ref = in.refName();
     289         103 :     out.setCreated(in.createdOn());
     290         103 :     out.uploader = accountLoader.get(in.uploader());
     291         103 :     out.fetch = makeFetchMap(cd, in);
     292         103 :     out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
     293         103 :     out.description = in.description().orElse(null);
     294             : 
     295         103 :     boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
     296         103 :     boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
     297         103 :     if (setCommit || addFooters) {
     298         103 :       checkState(rw != null);
     299         103 :       checkState(repo != null);
     300         103 :       Project.NameKey project = c.getProject();
     301         103 :       String rev = in.commitId().name();
     302         103 :       RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
     303         103 :       rw.parseBody(commit);
     304         103 :       String branchName = cd.change().getDest().branch();
     305         103 :       if (setCommit) {
     306         103 :         out.commit =
     307         103 :             getCommitInfo(
     308         103 :                 project, rw, commit, has(WEB_LINKS), fillCommit, branchName, c.getKey().get());
     309             :       }
     310         103 :       if (addFooters) {
     311         103 :         Ref ref = repo.exactRef(branchName);
     312         103 :         RevCommit mergeTip = null;
     313         103 :         if (ref != null) {
     314          98 :           mergeTip = rw.parseCommit(ref.getObjectId());
     315          98 :           rw.parseBody(mergeTip);
     316             :         }
     317         103 :         out.commitWithFooters =
     318             :             mergeUtilFactory
     319         103 :                 .create(projectCache.get(project).orElseThrow(illegalState(project)))
     320         103 :                 .createCommitMessageOnSubmit(commit, mergeTip, cd.notes(), in.id());
     321             :       }
     322             :     }
     323             : 
     324         103 :     if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
     325             :       try {
     326         103 :         out.files = fileInfoJson.getFileInfoMap(c, in);
     327         103 :         out.files.remove(Patch.COMMIT_MSG);
     328         103 :         out.files.remove(Patch.MERGE_LIST);
     329           0 :       } catch (ResourceConflictException e) {
     330           0 :         logger.atWarning().withCause(e).log("creating file list failed");
     331         103 :       }
     332             :     }
     333             : 
     334         103 :     if (out.isCurrent && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
     335          57 :       actionJson.addRevisionActions(
     336             :           changeInfo,
     337             :           out,
     338          57 :           new RevisionResource(changeResourceFactory.create(cd, userProvider.get()), in));
     339             :     }
     340             : 
     341         103 :     if (gpgApi.isEnabled() && has(PUSH_CERTIFICATES)) {
     342           3 :       if (in.pushCertificate().isPresent()) {
     343           0 :         out.pushCertificate =
     344           0 :             gpgApi.checkPushCertificate(
     345           0 :                 in.pushCertificate().get(), userFactory.create(in.uploader()));
     346             :       } else {
     347           3 :         out.pushCertificate = new PushCertificateInfo();
     348             :       }
     349             :     }
     350             : 
     351         103 :     return out;
     352             :   }
     353             : 
     354             :   private boolean has(ListChangesOption option) {
     355         103 :     return options.contains(option);
     356             :   }
     357             : 
     358             :   private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException {
     359           0 :     if (!permissionBackend.user(anonymous).change(cd).test(ChangePermission.READ)) {
     360           0 :       return false;
     361             :     }
     362           0 :     ProjectState projectState =
     363           0 :         projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
     364           0 :     return projectState.statePermitsRead();
     365             :   }
     366             : 
     367             :   @Nullable
     368             :   private Repository openRepoIfNecessary(Project.NameKey project) throws IOException {
     369         103 :     if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
     370         103 :       return repoManager.openRepository(project);
     371             :     }
     372          25 :     return null;
     373             :   }
     374             : 
     375             :   @Nullable
     376             :   private RevWalk newRevWalk(@Nullable Repository repo) {
     377         103 :     return repo != null ? new RevWalk(repo) : null;
     378             :   }
     379             : }

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