LCOV - code coverage report
Current view: top level - server/edit - ChangeEditUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 91 97 93.8 %
Date: 2022-11-19 15:00:39 Functions: 12 12 100.0 %

          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.edit;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : 
      19             : import com.google.gerrit.entities.Change;
      20             : import com.google.gerrit.entities.PatchSet;
      21             : import com.google.gerrit.entities.RefNames;
      22             : import com.google.gerrit.exceptions.StorageException;
      23             : import com.google.gerrit.extensions.client.ChangeKind;
      24             : import com.google.gerrit.extensions.restapi.AuthException;
      25             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      26             : import com.google.gerrit.extensions.restapi.RestApiException;
      27             : import com.google.gerrit.git.LockFailureException;
      28             : import com.google.gerrit.server.ChangeUtil;
      29             : import com.google.gerrit.server.CurrentUser;
      30             : import com.google.gerrit.server.IdentifiedUser;
      31             : import com.google.gerrit.server.PatchSetUtil;
      32             : import com.google.gerrit.server.change.ChangeKindCache;
      33             : import com.google.gerrit.server.change.NotifyResolver;
      34             : import com.google.gerrit.server.change.PatchSetInserter;
      35             : import com.google.gerrit.server.git.GitRepositoryManager;
      36             : import com.google.gerrit.server.index.change.ChangeIndexer;
      37             : import com.google.gerrit.server.notedb.ChangeNotes;
      38             : import com.google.gerrit.server.update.BatchUpdate;
      39             : import com.google.gerrit.server.update.BatchUpdateOp;
      40             : import com.google.gerrit.server.update.RepoContext;
      41             : import com.google.gerrit.server.update.UpdateException;
      42             : import com.google.gerrit.server.util.time.TimeUtil;
      43             : import com.google.inject.Inject;
      44             : import com.google.inject.Provider;
      45             : import com.google.inject.Singleton;
      46             : import java.io.IOException;
      47             : import java.util.Optional;
      48             : import org.eclipse.jgit.lib.CommitBuilder;
      49             : import org.eclipse.jgit.lib.ObjectId;
      50             : import org.eclipse.jgit.lib.ObjectInserter;
      51             : import org.eclipse.jgit.lib.ObjectReader;
      52             : import org.eclipse.jgit.lib.Ref;
      53             : import org.eclipse.jgit.lib.RefUpdate;
      54             : import org.eclipse.jgit.lib.Repository;
      55             : import org.eclipse.jgit.revwalk.RevCommit;
      56             : import org.eclipse.jgit.revwalk.RevWalk;
      57             : 
      58             : /**
      59             :  * Utility functions to manipulate change edits.
      60             :  *
      61             :  * <p>This class contains methods to retrieve, publish and delete edits. For changing edits see
      62             :  * {@link ChangeEditModifier}.
      63             :  */
      64             : @Singleton
      65             : public class ChangeEditUtil {
      66             :   private final GitRepositoryManager gitManager;
      67             :   private final PatchSetInserter.Factory patchSetInserterFactory;
      68             :   private final ChangeIndexer indexer;
      69             :   private final Provider<CurrentUser> userProvider;
      70             :   private final ChangeKindCache changeKindCache;
      71             :   private final PatchSetUtil psUtil;
      72             : 
      73             :   @Inject
      74             :   ChangeEditUtil(
      75             :       GitRepositoryManager gitManager,
      76             :       PatchSetInserter.Factory patchSetInserterFactory,
      77             :       ChangeIndexer indexer,
      78             :       Provider<CurrentUser> userProvider,
      79             :       ChangeKindCache changeKindCache,
      80         145 :       PatchSetUtil psUtil) {
      81         145 :     this.gitManager = gitManager;
      82         145 :     this.patchSetInserterFactory = patchSetInserterFactory;
      83         145 :     this.indexer = indexer;
      84         145 :     this.userProvider = userProvider;
      85         145 :     this.changeKindCache = changeKindCache;
      86         145 :     this.psUtil = psUtil;
      87         145 :   }
      88             : 
      89             :   /**
      90             :    * Retrieve edit for a given change.
      91             :    *
      92             :    * <p>At most one change edit can exist per user and change.
      93             :    *
      94             :    * @param notes change notes of change to retrieve change edits for.
      95             :    * @return edit for this change for this user, if present.
      96             :    * @throws AuthException if this is not a logged-in user.
      97             :    * @throws IOException if an error occurs.
      98             :    */
      99             :   public Optional<ChangeEdit> byChange(ChangeNotes notes) throws AuthException, IOException {
     100          24 :     return byChange(notes, userProvider.get());
     101             :   }
     102             : 
     103             :   /**
     104             :    * Retrieve edit for a change and the given user.
     105             :    *
     106             :    * <p>At most one change edit can exist per user and change.
     107             :    *
     108             :    * @param notes change notes of change to retrieve change edits for.
     109             :    * @param user user to retrieve edits as.
     110             :    * @return edit for this change for this user, if present.
     111             :    * @throws AuthException if this is not a logged-in user.
     112             :    * @throws IOException if an error occurs.
     113             :    */
     114             :   public Optional<ChangeEdit> byChange(ChangeNotes notes, CurrentUser user)
     115             :       throws AuthException, IOException {
     116          28 :     if (!user.isIdentifiedUser()) {
     117           0 :       throw new AuthException("Authentication required");
     118             :     }
     119          28 :     IdentifiedUser u = user.asIdentifiedUser();
     120          28 :     Change change = notes.getChange();
     121          28 :     try (Repository repo = gitManager.openRepository(change.getProject())) {
     122          28 :       int n = change.currentPatchSetId().get();
     123          28 :       String[] refNames = new String[n];
     124          28 :       for (int i = n; i > 0; i--) {
     125          28 :         refNames[i - 1] =
     126          28 :             RefNames.refsEdit(u.getAccountId(), change.getId(), PatchSet.id(change.getId(), i));
     127             :       }
     128          28 :       Ref ref = repo.getRefDatabase().firstExactRef(refNames);
     129          28 :       if (ref == null) {
     130          28 :         return Optional.empty();
     131             :       }
     132          25 :       try (RevWalk rw = new RevWalk(repo)) {
     133          25 :         RevCommit commit = rw.parseCommit(ref.getObjectId());
     134          25 :         PatchSet basePs = getBasePatchSet(notes, ref);
     135          25 :         return Optional.of(new ChangeEdit(change, ref.getName(), commit, basePs));
     136             :       }
     137          28 :     }
     138             :   }
     139             : 
     140             :   /**
     141             :    * Promote change edit to patch set, by squashing the edit into its parent.
     142             :    *
     143             :    * @param updateFactory factory for creating updates.
     144             :    * @param notes the {@code ChangeNotes} of the change to which the change edit belongs
     145             :    * @param user the current user
     146             :    * @param edit change edit to publish
     147             :    * @param notify Notify handling that defines to whom email notifications should be sent after the
     148             :    *     change edit is published.
     149             :    */
     150             :   public void publish(
     151             :       BatchUpdate.Factory updateFactory,
     152             :       ChangeNotes notes,
     153             :       CurrentUser user,
     154             :       ChangeEdit edit,
     155             :       NotifyResolver.Result notify)
     156             :       throws IOException, RestApiException, UpdateException {
     157          20 :     Change change = edit.getChange();
     158          20 :     try (Repository repo = gitManager.openRepository(change.getProject());
     159          20 :         ObjectInserter oi = repo.newObjectInserter();
     160          20 :         ObjectReader reader = oi.newReader();
     161          20 :         RevWalk rw = new RevWalk(reader)) {
     162          20 :       PatchSet basePatchSet = edit.getBasePatchSet();
     163          19 :       if (!basePatchSet.id().equals(change.currentPatchSetId())) {
     164           0 :         throw new ResourceConflictException("only edit for current patch set can be published");
     165             :       }
     166             : 
     167          19 :       RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
     168          19 :       PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
     169          19 :       PatchSetInserter inserter =
     170             :           patchSetInserterFactory
     171          19 :               .create(notes, psId, squashed)
     172          19 :               .setSendEmail(!change.isWorkInProgress());
     173             : 
     174          19 :       StringBuilder message =
     175          19 :           new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
     176             : 
     177             :       // Previously checked that the base patch set is the current patch set.
     178          19 :       ObjectId prior = basePatchSet.commitId();
     179          19 :       ChangeKind kind =
     180          19 :           changeKindCache.getChangeKind(change.getProject(), rw, repo.getConfig(), prior, squashed);
     181          19 :       if (kind == ChangeKind.NO_CODE_CHANGE) {
     182           6 :         message.append("Commit message was updated.");
     183           6 :         inserter.setDescription("Edit commit message");
     184             :       } else {
     185          17 :         message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
     186             :       }
     187             : 
     188          19 :       try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
     189          19 :         bu.setRepository(repo, rw, oi);
     190          19 :         bu.setNotify(notify);
     191          19 :         bu.addOp(change.getId(), inserter.setMessage(message.toString()));
     192          19 :         bu.addOp(
     193          19 :             change.getId(),
     194          19 :             new BatchUpdateOp() {
     195             :               @Override
     196             :               public void updateRepo(RepoContext ctx) throws Exception {
     197          19 :                 ctx.addRefUpdate(edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName());
     198          19 :               }
     199             :             });
     200          19 :         bu.execute();
     201             :       }
     202             :     }
     203          19 :   }
     204             : 
     205             :   /**
     206             :    * Delete change edit.
     207             :    *
     208             :    * @param edit change edit to delete
     209             :    */
     210             :   public void delete(ChangeEdit edit) throws IOException {
     211           2 :     Change change = edit.getChange();
     212           2 :     try (Repository repo = gitManager.openRepository(change.getProject())) {
     213           2 :       deleteRef(repo, edit);
     214             :     }
     215           2 :     indexer.index(change);
     216           2 :   }
     217             : 
     218             :   private PatchSet getBasePatchSet(ChangeNotes notes, Ref ref) throws IOException {
     219             :     try {
     220          25 :       int pos = ref.getName().lastIndexOf('/');
     221          25 :       checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
     222          25 :       String psId = ref.getName().substring(pos + 1);
     223          25 :       return psUtil.get(notes, PatchSet.id(notes.getChange().getId(), Integer.parseInt(psId)));
     224           0 :     } catch (StorageException | NumberFormatException e) {
     225           0 :       throw new IOException(e);
     226             :     }
     227             :   }
     228             : 
     229             :   private RevCommit squashEdit(
     230             :       RevWalk rw, ObjectInserter inserter, RevCommit edit, PatchSet basePatchSet)
     231             :       throws IOException, ResourceConflictException {
     232          20 :     RevCommit parent = rw.parseCommit(basePatchSet.commitId());
     233          20 :     if (parent.getTree().equals(edit.getTree())
     234           7 :         && edit.getFullMessage().equals(parent.getFullMessage())) {
     235           1 :       throw new ResourceConflictException("identical tree and message");
     236             :     }
     237          19 :     return writeSquashedCommit(rw, inserter, parent, edit);
     238             :   }
     239             : 
     240             :   private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
     241           2 :     String refName = edit.getRefName();
     242           2 :     RefUpdate ru = repo.updateRef(refName, true);
     243           2 :     ru.setExpectedOldObjectId(edit.getEditCommit());
     244           2 :     ru.setForceUpdate(true);
     245           2 :     RefUpdate.Result result = ru.delete();
     246           2 :     switch (result) {
     247             :       case FORCED:
     248             :       case NEW:
     249             :       case NO_CHANGE:
     250           2 :         break;
     251             :       case LOCK_FAILURE:
     252           0 :         throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru);
     253             :       case FAST_FORWARD:
     254             :       case IO_FAILURE:
     255             :       case NOT_ATTEMPTED:
     256             :       case REJECTED:
     257             :       case REJECTED_CURRENT_BRANCH:
     258             :       case RENAMED:
     259             :       case REJECTED_MISSING_OBJECT:
     260             :       case REJECTED_OTHER_REASON:
     261             :       default:
     262           0 :         throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
     263             :     }
     264           2 :   }
     265             : 
     266             :   private static RevCommit writeSquashedCommit(
     267             :       RevWalk rw, ObjectInserter inserter, RevCommit parent, RevCommit edit) throws IOException {
     268          19 :     CommitBuilder mergeCommit = new CommitBuilder();
     269          19 :     for (int i = 0; i < parent.getParentCount(); i++) {
     270          18 :       mergeCommit.addParentId(parent.getParent(i));
     271             :     }
     272          19 :     mergeCommit.setAuthor(parent.getAuthorIdent());
     273          19 :     mergeCommit.setMessage(edit.getFullMessage());
     274          19 :     mergeCommit.setCommitter(edit.getCommitterIdent());
     275          19 :     mergeCommit.setTreeId(edit.getTree());
     276             : 
     277          19 :     return rw.parseCommit(commit(inserter, mergeCommit));
     278             :   }
     279             : 
     280             :   private static ObjectId commit(ObjectInserter inserter, CommitBuilder mergeCommit)
     281             :       throws IOException {
     282          19 :     ObjectId id = inserter.insert(mergeCommit);
     283          19 :     inserter.flush();
     284          19 :     return id;
     285             :   }
     286             : }

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