diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 3f0c751437..415465e5d8 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -7488,6 +7488,13 @@ Alternatively, a change number can be specified, in which case the current patch set is inferred. + Empty string is used for rebasing directly on top of the target branch, which effectively breaks dependency towards a parent change. +|`allow_conflicts`|optional, defaults to false| +If `true`, the rebase also succeeds if there are conflicts. + +If there are conflicts the file contents of the rebased patch set contain +git conflict markers to indicate the conflicts. + +Callers can find out whether there were conflicts by checking the +`contains_git_conflicts` field in the returned link:#change-info[ChangeInfo]. + +If there are conflicts the change is marked as work-in-progress. |=========================== [[related-change-and-commit-info]] diff --git a/java/com/google/gerrit/extensions/api/changes/RebaseInput.java b/java/com/google/gerrit/extensions/api/changes/RebaseInput.java index 5f4a014e59..10559a3667 100644 --- a/java/com/google/gerrit/extensions/api/changes/RebaseInput.java +++ b/java/com/google/gerrit/extensions/api/changes/RebaseInput.java @@ -16,4 +16,12 @@ package com.google.gerrit.extensions.api.changes; public class RebaseInput { public String base; + + /** + * Whether the rebase should succeed if there are conflicts. + * + *
If there are conflicts the file contents of the rebased change contain git conflict markers + * to indicate the conflicts. + */ + public boolean allowConflicts; } diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java index b419c2fc3f..73e6a4efc1 100644 --- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java +++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java @@ -68,6 +68,8 @@ public interface RevisionApi { ChangeApi rebase(RebaseInput in) throws RestApiException; + ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException; + boolean canRebase() throws RestApiException; RevisionReviewerApi reviewer(String id) throws RestApiException; @@ -217,6 +219,11 @@ public interface RevisionApi { throw new NotImplementedException(); } + @Override + public ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException { + throw new NotImplementedException(); + } + @Override public boolean canRebase() throws RestApiException { throw new NotImplementedException(); diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java index 190a97eb64..7ed2f954e1 100644 --- a/java/com/google/gerrit/extensions/common/ChangeInfo.java +++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java @@ -83,7 +83,8 @@ public class ChangeInfo { * com.google.gerrit.server.restapi.change.CreateChange}, {@link * com.google.gerrit.server.restapi.change.CreateMergePatchSet}, {@link * com.google.gerrit.server.restapi.change.CherryPick}, {@link - * com.google.gerrit.server.restapi.change.CherryPickCommit} + * com.google.gerrit.server.restapi.change.CherryPickCommit}, {@link + * com.google.gerrit.server.restapi.change.Rebase} */ public Boolean containsGitConflicts; diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java index 04d2e8aee2..36d48033b8 100644 --- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java +++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java @@ -282,7 +282,16 @@ class RevisionApiImpl implements RevisionApi { @Override public ChangeApi rebase(RebaseInput in) throws RestApiException { try { - return changes.id(rebase.apply(revision, in).value()._number); + return changes.id(rebaseAsInfo(in)._number); + } catch (Exception e) { + throw asRestApiException("Cannot rebase ps", e); + } + } + + @Override + public ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException { + try { + return rebase.apply(revision, in).value(); } catch (Exception e) { throw asRestApiException("Cannot rebase ps", e); } diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java index 231359b2e9..acb4e3fdc7 100644 --- a/java/com/google/gerrit/server/change/RebaseChangeOp.java +++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java @@ -15,8 +15,10 @@ package com.google.gerrit.server.change; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.gerrit.server.project.ProjectCache.illegalState; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; @@ -26,6 +28,8 @@ import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.RebaseUtil.Base; +import com.google.gerrit.server.git.CodeReviewCommit; +import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.GroupCollector; import com.google.gerrit.server.git.MergeUtil; import com.google.gerrit.server.notedb.ChangeNotes; @@ -41,13 +45,26 @@ import com.google.gerrit.server.update.RepoContext; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.io.IOException; +import java.util.Map; +import org.eclipse.jgit.diff.Sequence; +import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.merge.MergeResult; +import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +/** + * BatchUpdate operation that rebases a change. + * + *
Can only be executed in a {@link com.google.gerrit.server.update.BatchUpdate} set has a {@link + * CodeReviewRevWalk} set as {@link RevWalk} (set via {@link + * com.google.gerrit.server.update.BatchUpdate#setRepository(org.eclipse.jgit.lib.Repository, + * RevWalk, org.eclipse.jgit.lib.ObjectInserter)}). + */ public class RebaseChangeOp implements BatchUpdateOp { public interface Factory { RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId); @@ -69,12 +86,13 @@ public class RebaseChangeOp implements BatchUpdateOp { private boolean validate = true; private boolean checkAddPatchSetPermission = true; private boolean forceContentMerge; + private boolean allowConflicts; private boolean detailedCommitMessage; private boolean postMessage = true; private boolean sendEmail = true; private boolean matchAuthorToCommitterDate = false; - private RevCommit rebasedCommit; + private CodeReviewCommit rebasedCommit; private PatchSet.Id rebasedPatchSetId; private PatchSetInserter patchSetInserter; private PatchSet rebasedPatchSet; @@ -126,6 +144,19 @@ public class RebaseChangeOp implements BatchUpdateOp { return this; } + /** + * Allows the rebase to succeed if there are conflicts. + * + *
This setting requires that {@link #forceContentMerge} is set {@code true}. If {@link
+ * #forceContentMerge} is {@code false} this setting has no effect.
+ *
+ * @see #setForceContentMerge(boolean)
+ */
+ public RebaseChangeOp setAllowConflicts(boolean allowConflicts) {
+ this.allowConflicts = allowConflicts;
+ return this;
+ }
+
public RebaseChangeOp setDetailedCommitMessage(boolean detailedCommitMessage) {
this.detailedCommitMessage = detailedCommitMessage;
return this;
@@ -186,14 +217,11 @@ public class RebaseChangeOp implements BatchUpdateOp {
.setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setValidate(validate)
- .setSendEmail(sendEmail);
+ .setSendEmail(sendEmail)
+ .setWorkInProgress(!rebasedCommit.getFilesWithGitConflicts().isEmpty());
if (postMessage) {
patchSetInserter.setMessage(
- "Patch Set "
- + rebasedPatchSetId.get()
- + ": Patch Set "
- + originalPatchSet.id().get()
- + " was rebased");
+ messageForRebasedChange(rebasedPatchSetId, originalPatchSet.id(), rebasedCommit));
}
if (base != null && !base.notes().getChange().isMerged()) {
@@ -208,6 +236,24 @@ public class RebaseChangeOp implements BatchUpdateOp {
patchSetInserter.updateRepo(ctx);
}
+ private static String messageForRebasedChange(
+ PatchSet.Id rebasePatchSetId, PatchSet.Id originalPatchSetId, CodeReviewCommit commit) {
+ StringBuilder stringBuilder =
+ new StringBuilder(
+ String.format(
+ "Patch Set %d: Patch Set %d was rebased",
+ rebasePatchSetId.get(), originalPatchSetId.get()));
+
+ if (!commit.getFilesWithGitConflicts().isEmpty()) {
+ stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
+ commit.getFilesWithGitConflicts().stream()
+ .sorted()
+ .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
+ }
+
+ return stringBuilder.toString();
+ }
+
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceConflictException, IOException, BadRequestException {
@@ -221,7 +267,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
patchSetInserter.postUpdate(ctx);
}
- public RevCommit getRebasedCommit() {
+ public CodeReviewCommit getRebasedCommit() {
checkState(rebasedCommit != null, "getRebasedCommit() only valid after updateRepo");
return rebasedCommit;
}
@@ -254,7 +300,7 @@ public class RebaseChangeOp implements BatchUpdateOp {
* @throws MergeConflictException the rebase failed due to a merge conflict.
* @throws IOException the merge failed for another reason.
*/
- private RevCommit rebaseCommit(
+ private CodeReviewCommit rebaseCommit(
RepoContext ctx, RevCommit original, ObjectId base, String commitMessage)
throws ResourceConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
@@ -266,15 +312,50 @@ public class RebaseChangeOp implements BatchUpdateOp {
ThreeWayMerger merger =
newMergeUtil().newThreeWayMerger(ctx.getInserter(), ctx.getRepoView().getConfig());
merger.setBase(parentCommit);
+
+ DirCache dc = DirCache.newInCore();
+ if (allowConflicts && merger instanceof ResolveMerger) {
+ // The DirCache must be set on ResolveMerger before calling
+ // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
+ ((ResolveMerger) merger).setDirCache(dc);
+ }
+
boolean success = merger.merge(original, base);
- if (!success || merger.getResultTreeId() == null) {
- throw new MergeConflictException(
- "The change could not be rebased due to a conflict during merge.");
+ ObjectId tree;
+ ImmutableSet