LCOV - code coverage report
Current view: top level - server/change - RebaseUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 60 72 83.3 %
Date: 2022-11-19 15:00:39 Functions: 8 8 100.0 %

          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.change;
      16             : 
      17             : import com.google.auto.value.AutoValue;
      18             : import com.google.common.flogger.FluentLogger;
      19             : import com.google.common.primitives.Ints;
      20             : import com.google.gerrit.common.Nullable;
      21             : import com.google.gerrit.entities.BranchNameKey;
      22             : import com.google.gerrit.entities.Change;
      23             : import com.google.gerrit.entities.PatchSet;
      24             : import com.google.gerrit.exceptions.StorageException;
      25             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      26             : import com.google.gerrit.extensions.restapi.RestApiException;
      27             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      28             : import com.google.gerrit.git.ObjectIds;
      29             : import com.google.gerrit.server.PatchSetUtil;
      30             : import com.google.gerrit.server.notedb.ChangeNotes;
      31             : import com.google.gerrit.server.query.change.ChangeData;
      32             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      33             : import com.google.inject.Inject;
      34             : import com.google.inject.Provider;
      35             : import java.io.IOException;
      36             : import org.eclipse.jgit.lib.ObjectId;
      37             : import org.eclipse.jgit.lib.Ref;
      38             : import org.eclipse.jgit.lib.Repository;
      39             : import org.eclipse.jgit.revwalk.RevCommit;
      40             : import org.eclipse.jgit.revwalk.RevWalk;
      41             : 
      42             : /** Utility methods related to rebasing changes. */
      43             : public class RebaseUtil {
      44         145 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      45             : 
      46             :   private final Provider<InternalChangeQuery> queryProvider;
      47             :   private final ChangeNotes.Factory notesFactory;
      48             :   private final PatchSetUtil psUtil;
      49             : 
      50             :   @Inject
      51             :   RebaseUtil(
      52             :       Provider<InternalChangeQuery> queryProvider,
      53             :       ChangeNotes.Factory notesFactory,
      54         145 :       PatchSetUtil psUtil) {
      55         145 :     this.queryProvider = queryProvider;
      56         145 :     this.notesFactory = notesFactory;
      57         145 :     this.psUtil = psUtil;
      58         145 :   }
      59             : 
      60             :   public boolean canRebase(PatchSet patchSet, BranchNameKey dest, Repository git, RevWalk rw) {
      61             :     try {
      62          11 :       findBaseRevision(patchSet, dest, git, rw);
      63          11 :       return true;
      64          49 :     } catch (RestApiException e) {
      65          49 :       return false;
      66           0 :     } catch (StorageException | IOException e) {
      67           0 :       logger.atWarning().withCause(e).log(
      68           0 :           "Error checking if patch set %s on %s can be rebased", patchSet.id(), dest);
      69           0 :       return false;
      70             :     }
      71             :   }
      72             : 
      73             :   @AutoValue
      74          12 :   public abstract static class Base {
      75             :     @Nullable
      76             :     private static Base create(ChangeNotes notes, PatchSet ps) {
      77          12 :       if (notes == null) {
      78           0 :         return null;
      79             :       }
      80          12 :       return new AutoValue_RebaseUtil_Base(notes, ps);
      81             :     }
      82             : 
      83             :     public abstract ChangeNotes notes();
      84             : 
      85             :     public abstract PatchSet patchSet();
      86             :   }
      87             : 
      88             :   public Base parseBase(RevisionResource rsrc, String base) {
      89             :     // Try parsing the base as a ref string.
      90          13 :     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
      91          13 :     if (basePatchSetId != null) {
      92           1 :       Change.Id baseChangeId = basePatchSetId.changeId();
      93           1 :       ChangeNotes baseNotes = notesFor(rsrc, baseChangeId);
      94           1 :       if (baseNotes != null) {
      95           1 :         return Base.create(
      96           1 :             notesFor(rsrc, basePatchSetId.changeId()), psUtil.get(baseNotes, basePatchSetId));
      97             :       }
      98             :     }
      99             : 
     100             :     // Try parsing base as a change number (assume current patch set).
     101          13 :     Integer baseChangeId = Ints.tryParse(base);
     102          13 :     if (baseChangeId != null) {
     103           1 :       ChangeNotes baseNotes = notesFor(rsrc, Change.id(baseChangeId));
     104           1 :       if (baseNotes != null) {
     105           1 :         return Base.create(baseNotes, psUtil.current(baseNotes));
     106             :       }
     107             :     }
     108             : 
     109             :     // Try parsing as SHA-1.
     110          13 :     Base ret = null;
     111          13 :     for (ChangeData cd : queryProvider.get().byProjectCommit(rsrc.getProject(), base)) {
     112          12 :       for (PatchSet ps : cd.patchSets()) {
     113          12 :         if (!ObjectIds.matchesAbbreviation(ps.commitId(), base)) {
     114           6 :           continue;
     115             :         }
     116          12 :         if (ret == null || ret.patchSet().id().get() < ps.id().get()) {
     117          12 :           ret = Base.create(cd.notes(), ps);
     118             :         }
     119          12 :       }
     120          12 :     }
     121          13 :     return ret;
     122             :   }
     123             : 
     124             :   private ChangeNotes notesFor(RevisionResource rsrc, Change.Id id) {
     125           1 :     if (rsrc.getChange().getId().equals(id)) {
     126           0 :       return rsrc.getNotes();
     127             :     }
     128           1 :     return notesFactory.createChecked(rsrc.getProject(), id);
     129             :   }
     130             : 
     131             :   /**
     132             :    * Find the commit onto which a patch set should be rebased.
     133             :    *
     134             :    * <p>This is defined as the latest patch set of the change corresponding to this commit's parent,
     135             :    * or the destination branch tip in the case where the parent's change is merged.
     136             :    *
     137             :    * @param patchSet patch set for which the new base commit should be found.
     138             :    * @param destBranch the destination branch.
     139             :    * @param git the repository.
     140             :    * @param rw the RevWalk.
     141             :    * @return the commit onto which the patch set should be rebased.
     142             :    * @throws RestApiException if rebase is not possible.
     143             :    * @throws IOException if accessing the repository fails.
     144             :    */
     145             :   public ObjectId findBaseRevision(
     146             :       PatchSet patchSet, BranchNameKey destBranch, Repository git, RevWalk rw)
     147             :       throws RestApiException, IOException {
     148          49 :     ObjectId baseId = null;
     149          49 :     RevCommit commit = rw.parseCommit(patchSet.commitId());
     150             : 
     151          49 :     if (commit.getParentCount() > 1) {
     152           0 :       throw new UnprocessableEntityException("Cannot rebase a change with multiple parents.");
     153          49 :     } else if (commit.getParentCount() == 0) {
     154           0 :       throw new UnprocessableEntityException(
     155             :           "Cannot rebase a change without any parents (is this the initial commit?).");
     156             :     }
     157             : 
     158          49 :     ObjectId parentId = commit.getParent(0);
     159             : 
     160             :     CHANGES:
     161          49 :     for (ChangeData cd : queryProvider.get().byBranchCommit(destBranch, parentId.name())) {
     162          25 :       for (PatchSet depPatchSet : cd.patchSets()) {
     163          25 :         if (!depPatchSet.commitId().equals(parentId)) {
     164           4 :           continue;
     165             :         }
     166          25 :         Change depChange = cd.change();
     167          25 :         if (depChange.isAbandoned()) {
     168           0 :           throw new ResourceConflictException(
     169           0 :               "Cannot rebase a change with an abandoned parent: " + depChange.getKey());
     170             :         }
     171             : 
     172          25 :         if (depChange.isNew()) {
     173          16 :           if (depPatchSet.id().equals(depChange.currentPatchSetId())) {
     174          16 :             throw new ResourceConflictException(
     175             :                 "Change is already based on the latest patch set of the dependent change.");
     176             :           }
     177           0 :           baseId = cd.currentPatchSet().commitId();
     178             :         }
     179             :         break CHANGES;
     180             :       }
     181           0 :     }
     182             : 
     183          45 :     if (baseId == null) {
     184             :       // We are dependent on a merged PatchSet or have no PatchSet
     185             :       // dependencies at all.
     186          45 :       Ref destRef = git.getRefDatabase().exactRef(destBranch.branch());
     187          45 :       if (destRef == null) {
     188           2 :         throw new UnprocessableEntityException(
     189           2 :             "The destination branch does not exist: " + destBranch.branch());
     190             :       }
     191          45 :       baseId = destRef.getObjectId();
     192          45 :       if (baseId.equals(parentId)) {
     193          45 :         throw new ResourceConflictException("Change is already up to date.");
     194             :       }
     195             :     }
     196          12 :     return baseId;
     197             :   }
     198             : }

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