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 : }