Line data Source code
1 : // Copyright (C) 2012 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.submit;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
20 : import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
21 :
22 : import com.google.common.collect.ImmutableList;
23 : import com.google.gerrit.common.Nullable;
24 : import com.google.gerrit.entities.BooleanProjectConfig;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.exceptions.StorageException;
27 : import com.google.gerrit.extensions.restapi.BadRequestException;
28 : import com.google.gerrit.extensions.restapi.MergeConflictException;
29 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
30 : import com.google.gerrit.extensions.restapi.RestApiException;
31 : import com.google.gerrit.server.ChangeUtil;
32 : import com.google.gerrit.server.change.RebaseChangeOp;
33 : import com.google.gerrit.server.git.CodeReviewCommit;
34 : import com.google.gerrit.server.git.MergeTip;
35 : import com.google.gerrit.server.permissions.PermissionBackendException;
36 : import com.google.gerrit.server.project.InvalidChangeOperationException;
37 : import com.google.gerrit.server.project.NoSuchChangeException;
38 : import com.google.gerrit.server.update.ChangeContext;
39 : import com.google.gerrit.server.update.PostUpdateContext;
40 : import com.google.gerrit.server.update.RepoContext;
41 : import java.io.IOException;
42 : import java.util.Collection;
43 : import java.util.List;
44 : import org.eclipse.jgit.lib.ObjectId;
45 : import org.eclipse.jgit.lib.PersonIdent;
46 : import org.eclipse.jgit.lib.Repository;
47 : import org.eclipse.jgit.revwalk.RevCommit;
48 :
49 : /** This strategy covers RebaseAlways and RebaseIfNecessary ones. */
50 : public class RebaseSubmitStrategy extends SubmitStrategy {
51 : private final boolean rebaseAlways;
52 :
53 : RebaseSubmitStrategy(SubmitStrategy.Arguments args, boolean rebaseAlways) {
54 5 : super(args);
55 5 : this.rebaseAlways = rebaseAlways;
56 5 : }
57 :
58 : @Override
59 : public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
60 : List<CodeReviewCommit> sorted;
61 : try {
62 5 : sorted = args.rebaseSorter.sort(toMerge);
63 0 : } catch (IOException | StorageException e) {
64 0 : throw new StorageException("Commit sorting failed", e);
65 5 : }
66 :
67 : // We cannot rebase merge commits. This is why we integrate merge changes into the target branch
68 : // the same way as if MERGE_IF_NECESSARY was the submit strategy. This means if needed we create
69 : // a merge commit that integrates the merge change into the target branch.
70 : // If we integrate a change series that consists out of a normal change and a merge change,
71 : // where the merge change depends on the normal change, we must skip rebasing the normal change,
72 : // because it already gets integrated by merging the merge change. If the rebasing of the normal
73 : // change is not skipped, it would appear twice in the history after the submit is done (once
74 : // through its rebased commit, and once through its original commit which is a parent of the
75 : // merge change that was merged into the target branch. To skip the rebasing of the normal
76 : // change, we call MergeUtil#reduceToMinimalMerge, as it excludes commits which will be
77 : // implicitly integrated by merging the series. Then we use the MergeIfNecessaryOp to integrate
78 : // the whole series.
79 : // If on the other hand, we integrate a change series that consists out of a merge change and a
80 : // normal change, where the normal change depends on the merge change, we can first integrate
81 : // the merge change by a merge and then integrate the normal change by a rebase. In this case we
82 : // do not want to call MergeUtil#reduceToMinimalMerge as we are not intending to integrate the
83 : // whole series by a merge, but rather do the integration of the commits one by one.
84 5 : boolean foundNonMerge = false;
85 5 : for (CodeReviewCommit c : sorted) {
86 5 : if (c.getParentCount() > 1) {
87 2 : if (!foundNonMerge) {
88 : // found a merge change, but it doesn't depend on a normal change, this means we are not
89 : // required to merge the whole series at once
90 2 : continue;
91 : }
92 : // found a merge commit that depends on a normal change, this means we are required to merge
93 : // the whole series at once
94 2 : sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
95 2 : return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toImmutableList());
96 : }
97 5 : foundNonMerge = true;
98 5 : }
99 :
100 5 : ImmutableList.Builder<SubmitStrategyOp> ops =
101 5 : ImmutableList.builderWithExpectedSize(sorted.size());
102 5 : boolean first = true;
103 5 : while (!sorted.isEmpty()) {
104 5 : CodeReviewCommit n = sorted.remove(0);
105 5 : if (first && args.mergeTip.getInitialTip() == null) {
106 : // TODO(tandrii): Cherry-Pick strategy does this too, but it's wrong
107 : // and can be fixed.
108 3 : ops.add(new FastForwardOp(args, n));
109 5 : } else if (n.getParentCount() == 0) {
110 0 : ops.add(new RebaseRootOp(n));
111 5 : } else if (n.getParentCount() == 1) {
112 5 : ops.add(new RebaseOneOp(n));
113 : } else {
114 2 : ops.add(new MergeIfNecessaryOp(n));
115 : }
116 5 : first = false;
117 5 : }
118 5 : return ops.build();
119 : }
120 :
121 : private class RebaseRootOp extends SubmitStrategyOp {
122 0 : private RebaseRootOp(CodeReviewCommit toMerge) {
123 0 : super(RebaseSubmitStrategy.this.args, toMerge);
124 0 : }
125 :
126 : @Override
127 : public void updateRepoImpl(RepoContext ctx) {
128 : // Refuse to merge a root commit into an existing branch, we cannot obtain
129 : // a delta for the cherry-pick to apply.
130 0 : toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
131 0 : }
132 : }
133 :
134 : private class RebaseOneOp extends SubmitStrategyOp {
135 : private RebaseChangeOp rebaseOp;
136 : private CodeReviewCommit newCommit;
137 : private PatchSet.Id newPatchSetId;
138 :
139 5 : private RebaseOneOp(CodeReviewCommit toMerge) {
140 5 : super(RebaseSubmitStrategy.this.args, toMerge);
141 5 : }
142 :
143 : @Override
144 : public void updateRepoImpl(RepoContext ctx)
145 : throws InvalidChangeOperationException, RestApiException, IOException,
146 : PermissionBackendException {
147 5 : if (args.mergeUtil.canFastForward(
148 5 : args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
149 5 : if (!rebaseAlways) {
150 4 : if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
151 1 : && toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
152 1 : toMerge.setStatusCode(EMPTY_COMMIT);
153 1 : return;
154 : }
155 :
156 4 : args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
157 4 : toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
158 4 : acceptMergeTip(args.mergeTip);
159 4 : return;
160 : }
161 : // RebaseAlways means we modify commit message.
162 4 : args.rw.parseBody(toMerge);
163 4 : newPatchSetId =
164 4 : ChangeUtil.nextPatchSetIdFromChangeRefs(
165 4 : ctx.getRepoView().getRefs(getId().toRefPrefix()).keySet(),
166 4 : toMerge.change().currentPatchSetId());
167 4 : RevCommit mergeTip = args.mergeTip.getCurrentTip();
168 4 : args.rw.parseBody(mergeTip);
169 4 : String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
170 4 : PersonIdent committer = ctx.newCommitterIdent(args.caller);
171 : try {
172 4 : newCommit =
173 4 : args.mergeUtil.createCherryPickFromCommit(
174 4 : ctx.getInserter(),
175 4 : ctx.getRepoView().getConfig(),
176 4 : args.mergeTip.getCurrentTip(),
177 : toMerge,
178 : committer,
179 : cherryPickCmtMsg,
180 : args.rw,
181 : 0,
182 : true,
183 : false);
184 0 : } catch (MergeConflictException mce) {
185 : // Unlike in Cherry-pick case, this should never happen.
186 0 : toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
187 0 : throw new IllegalStateException(
188 : "MergeConflictException on message edit must not happen", mce);
189 0 : } catch (MergeIdenticalTreeException mie) {
190 : // this should not happen
191 0 : toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
192 0 : return;
193 4 : }
194 4 : ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName());
195 4 : } else {
196 : // Stale read of patch set is ok; see comments in RebaseChangeOp.
197 4 : PatchSet origPs = args.psUtil.get(toMerge.getNotes(), toMerge.getPatchsetId());
198 4 : rebaseOp =
199 : args.rebaseFactory
200 4 : .create(toMerge.notes(), origPs, args.mergeTip.getCurrentTip())
201 4 : .setFireRevisionCreated(false)
202 : // Bypass approval copier since SubmitStrategyOp copy all approvals
203 : // later anyway.
204 4 : .setValidate(false)
205 4 : .setCheckAddPatchSetPermission(false)
206 : // RebaseAlways should set always modify commit message like
207 : // Cherry-Pick strategy.
208 4 : .setDetailedCommitMessage(rebaseAlways)
209 : // Do not post message after inserting new patchset because there
210 : // will be one about change being merged already.
211 4 : .setPostMessage(false)
212 4 : .setSendEmail(false)
213 4 : .setMatchAuthorToCommitterDate(
214 4 : args.project.is(BooleanProjectConfig.MATCH_AUTHOR_TO_COMMITTER_DATE))
215 : // The votes are automatically copied and they don't count as copied votes. See
216 : // method's javadoc.
217 4 : .setStoreCopiedVotes(/* storeCopiedVotes = */ false);
218 : try {
219 4 : rebaseOp.updateRepo(ctx);
220 2 : } catch (MergeConflictException | NoSuchChangeException e) {
221 2 : toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
222 2 : throw new IntegrationConflictException(
223 2 : "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
224 4 : }
225 4 : newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
226 4 : newPatchSetId = rebaseOp.getPatchSetId();
227 : }
228 5 : if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
229 2 : && newCommit.getTree().equals(newCommit.getParent(0).getTree())) {
230 2 : toMerge.setStatusCode(EMPTY_COMMIT);
231 2 : return;
232 : }
233 5 : newCommit = amendGitlink(newCommit);
234 5 : newCommit.copyFrom(toMerge);
235 5 : newCommit.setPatchsetId(newPatchSetId);
236 5 : newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
237 5 : args.mergeTip.moveTipTo(newCommit, newCommit);
238 5 : args.commitStatus.put(args.mergeTip.getCurrentTip());
239 5 : acceptMergeTip(args.mergeTip);
240 5 : }
241 :
242 : @Nullable
243 : @Override
244 : public PatchSet updateChangeImpl(ChangeContext ctx)
245 : throws NoSuchChangeException, ResourceConflictException, IOException, BadRequestException {
246 5 : if (newCommit == null) {
247 4 : checkState(!rebaseAlways, "RebaseAlways must never fast forward");
248 : // otherwise, took the fast-forward option, nothing to do.
249 4 : return null;
250 : }
251 :
252 : PatchSet newPs;
253 5 : if (rebaseOp != null) {
254 4 : rebaseOp.updateChange(ctx);
255 4 : newPs = rebaseOp.getPatchSet();
256 : } else {
257 : // CherryPick
258 4 : PatchSet prevPs = args.psUtil.current(ctx.getNotes());
259 4 : newPs =
260 4 : args.psUtil.insert(
261 4 : ctx.getRevWalk(),
262 4 : ctx.getUpdate(newPatchSetId),
263 : newPatchSetId,
264 : newCommit,
265 4 : prevPs != null ? prevPs.groups() : ImmutableList.of(),
266 : null,
267 : null);
268 : }
269 5 : ctx.getChange()
270 5 : .setCurrentPatchSet(
271 5 : args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, newPatchSetId));
272 5 : newCommit.setNotes(ctx.getNotes());
273 5 : return newPs;
274 : }
275 :
276 : @Override
277 : public void postUpdateImpl(PostUpdateContext ctx) {
278 5 : if (rebaseOp != null) {
279 4 : rebaseOp.postUpdate(ctx);
280 : }
281 5 : }
282 : }
283 :
284 : private class MergeIfNecessaryOp extends SubmitStrategyOp {
285 2 : private MergeIfNecessaryOp(CodeReviewCommit toMerge) {
286 2 : super(RebaseSubmitStrategy.this.args, toMerge);
287 2 : }
288 :
289 : @Override
290 : public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
291 : // There are multiple parents, so this is a merge commit. We don't want
292 : // to rebase the merge as clients can't easily rebase their history with
293 : // that merge present and replaced by an equivalent merge with a different
294 : // first parent. So instead behave as though MERGE_IF_NECESSARY was
295 : // configured.
296 : // TODO(tandrii): this is not in spirit of RebaseAlways strategy because
297 : // the commit messages can not be modified in the process. It's also
298 : // possible to implement rebasing of merge commits. E.g., the Cherry Pick
299 : // REST endpoint already supports cherry-picking of merge commits.
300 : // For now, users of RebaseAlways strategy for whom changed commit footers
301 : // are important would be well advised to prohibit uploading patches with
302 : // merge commits.
303 2 : MergeTip mergeTip = args.mergeTip;
304 2 : if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)
305 2 : && !args.subscriptionGraph.hasSubscription(args.destBranch)) {
306 2 : mergeTip.moveTipTo(toMerge, toMerge);
307 : } else {
308 2 : PersonIdent caller = ctx.newCommitterIdent();
309 2 : CodeReviewCommit newTip =
310 2 : args.mergeUtil.mergeOneCommit(
311 : caller,
312 : caller,
313 : args.rw,
314 2 : ctx.getInserter(),
315 2 : ctx.getRepoView().getConfig(),
316 : args.destBranch,
317 2 : mergeTip.getCurrentTip(),
318 : toMerge);
319 2 : mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
320 : }
321 2 : args.mergeUtil.markCleanMerges(
322 2 : args.rw, args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
323 2 : acceptMergeTip(mergeTip);
324 2 : }
325 : }
326 :
327 : private void acceptMergeTip(MergeTip mergeTip) {
328 5 : args.alreadyAccepted.add(mergeTip.getCurrentTip());
329 5 : }
330 :
331 : static boolean dryRun(
332 : SubmitDryRun.Arguments args,
333 : Repository repo,
334 : CodeReviewCommit mergeTip,
335 : CodeReviewCommit toMerge) {
336 : // Test for merge instead of cherry pick to avoid false negatives
337 : // on commit chains.
338 3 : return args.mergeUtil.canMerge(args.mergeSorter, repo, mergeTip, toMerge);
339 : }
340 : }
|