Allow creating merge commits with conflicts
There are 2 REST endpoints that allow callers to create merge commits in Gerrit: 1. Create Change REST endpoint: Creates a new change. If ChangeInput.merge is specified a merge commit for the new change is created. 2. Create Merge Patch Set REST endpoint: Creates a merge commit and adds it as a new patch set to an existing change. In both cases the merge is performed by Gerrit and if the merge fails due to conflicts in the files, so far the request is always rejected with '409 Conflict'. This change adds a new option 'allowConflicts' in MergeInput that allows the merge to succeed even if there are conflicts. If this option is set and there are conflicts: * the request still succeeds and the new change / patch set gets created * the conflicting files in the merge commit contain Git conflict markers * callers can know that there were conflicts by checking the 'containsGitConflicts' field in the returned ChangeInfo * the change is set to work-in-progress so that it's not accidentally submitted with conflicts * a change message is posted on the change that lists the files that have conflicts This functionality is consistent with the existing 'allowConflicts' option in CherryPickInput which allows to let a cherry-pick succeed even if there are conflicts. Also here the request succeeds, callers can check the 'containsGitConflicts' field in the returned ChangeInfo, the change is set to work-in-progress and a change message lists the files that have conflicts. Being able to create merge commits even if there are conflicts is useful because it: * allows robots to create merge commits, and let human users resolve conflicts if needed * allows to resolve conflicts on merge without having a local git client, e.g. by using online edit Implementation-wise some aspects should be pointed out: * To let callers know whether the request resulted in a merge with conflicts, ChangeInfo contains a new 'containsGitConflicts' field now. This field is only populated if the change info is returned in response to a request that creates a new change or patch set and conflicts are allowed. Doing this was already considered when the allow conflicts option was added for cherry-picks (see alternative 1. in the commit message of change Iae9eef38a). At that time we didn't take this approach because it might confuse users if this field is not populated for other requests. Instead we decided to add CherryPickChangeInfo that extends ChangeInfo. However now this approach doesn't scale well as we would need to add further classes that extend ChangeInfo (e.g. NewChangeInfo for the Create Change REST endpoint). To mitigate the concern that the 'containsGitConflicts' field might be confusing for users, it states very explicitly in the documentation when it is populated. * CherryPickChangeInfo is obsolete now, but we cannot remove it without breaking the Java API, hence we keep it. * To be able to test the ChangeInfo that is returned by the Create Change REST endpoint we need a new createAsInfo(ChangeInput) method in the Changes API that returns the ChangeInfo (instead of a ChangeApi). This follows the example of the cherryPickAsInfo(CherryPickInput) method that was added by change Iae9eef38a. * PatchSetInserter is enhanced with a method that allows to set the change to work-in-progress so that this flag can be set in the same BatchUpdateOp which also creates the patch set. It's not possible to set this flag from a separate BatchUpdateOp in the same BatchUpdate because the second BatchUpdateOp cannot observe the patch set that is created by the first BatchUpdateOp (and the patch set data is needed to send out the WorkInProgressStateChanged event). * PatchSetInserter is also bound in BatchProgramModule, but EventUtil is not available in this Guice stack which is why injecting WorkInProgressStateChanged into PatchSetInserter fails in this setup. To solve this, WorkInProgressStateChanged defines a disabled implementation that is bound in BatchProgramModule (this follows the example of RevisionCreated which is also needed by PatchSetInserter). Change-Id: Ib6bc8eedfd8a98bf1088660e27610e1eafb095fc Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
@@ -5502,8 +5502,8 @@ does not specify a Change-Id, a new one is picked for the destination change.
|
||||
}
|
||||
----
|
||||
|
||||
As response a link:#cherry-pick-change-info[CherryPickChangeInfo]
|
||||
entity is returned that describes the resulting cherry-pick change.
|
||||
As response a link:#change-info[ChangeInfo] entity is returned that
|
||||
describes the resulting cherry-pick change.
|
||||
|
||||
.Response
|
||||
----
|
||||
@@ -6044,8 +6044,19 @@ If the change that triggered the submission also has a topic, it will be
|
||||
The callers must not rely on the format of the submission ID.
|
||||
|`cherry_pick_of_change` |optional|
|
||||
The numeric Change-Id of the change that this change was cherry-picked from.
|
||||
|`cherry_pick_of_patch_set` |optional|
|
||||
|`cherry_pick_of_patch_set`|optional|
|
||||
The patchset number of the change that this change was cherry-picked from.
|
||||
|`contains_git_conflicts` |optional, not set if `false`|
|
||||
Whether the change contains conflicts. +
|
||||
If `true`, some of the file contents of the change contain git conflict
|
||||
markers to indicate the conflicts. +
|
||||
Only set if this change info is returned in response to a request that
|
||||
creates a new change or patch set and conflicts are allowed. In
|
||||
particular this field is only populated if the change info is returned
|
||||
by one of the following REST endpoints: link:#create-change[Create
|
||||
Change], link:#create-merge-patch-set-for-change[Create Merge Patch Set
|
||||
For Change], link:#cherry-pick[Cherry Pick Revision],
|
||||
link:rest-api-project.html#cherry-pick-commit[Cherry Pick Commit]
|
||||
|==================================
|
||||
|
||||
[[change-input]]
|
||||
@@ -6124,23 +6135,6 @@ invocations of the REST call are required.
|
||||
Which patchset (if any) generated this message.
|
||||
|==================================
|
||||
|
||||
[[cherry-pick-change-info]]
|
||||
=== CherryPickChangeInfo
|
||||
The `CherryPickChangeInfo` entity contains information about a
|
||||
cherry-pick change.
|
||||
|
||||
`CherryPickChangeInfo` has the same fields as link:#change-info[
|
||||
ChangeInfo]. In addition `CherryPickChangeInfo` has the following
|
||||
fields:
|
||||
|
||||
[options="header",cols="1,^1,5"]
|
||||
|======================================
|
||||
|Field Name ||Description
|
||||
|`contains_git_conflicts` |optional, not set if `false`|
|
||||
Whether any file in the change contains Git conflict markers.
|
||||
|======================================
|
||||
|
||||
|
||||
[[cherrypick-input]]
|
||||
=== CherryPickInput
|
||||
The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
|
||||
@@ -6172,9 +6166,8 @@ If `true`, the cherry-pick uses content merge and succeeds also if
|
||||
there are conflicts. If there are conflicts the file contents of the
|
||||
created change contain git conflict markers to indicate the conflicts.
|
||||
Callers can find out if there were conflicts by checking the
|
||||
`contains_git_conflicts` field in the link:#cherry-pick-change-info[
|
||||
CherryPickChangeInfo] that is returned by the cherry-pick REST
|
||||
endpoints. If there are conflicts the cherry-pick change is marked as
|
||||
`contains_git_conflicts` field in the link:#change-info[ChangeInfo]. If
|
||||
there are conflicts the cherry-pick change is marked as
|
||||
work-in-progress.
|
||||
|===========================
|
||||
|
||||
@@ -6804,7 +6797,7 @@ A list of other branch names where this change could merge cleanly
|
||||
The `MergeInput` entity contains information about the merge
|
||||
|
||||
[options="header",cols="1,^1,5"]
|
||||
|============================
|
||||
|==============================
|
||||
|Field Name ||Description
|
||||
|`source` ||
|
||||
The source to merge from, e.g. a complete or abbreviated commit SHA-1,
|
||||
@@ -6818,7 +6811,17 @@ many branches.
|
||||
|`strategy` |optional|
|
||||
The strategy of the merge, can be `recursive`, `resolve`,
|
||||
`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
|
||||
|============================
|
||||
|`allow_conflicts`|optional, defaults to false|
|
||||
If `true`, creating the merge succeeds also if there are conflicts. +
|
||||
If there are conflicts the file contents of the created change 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 link:#change-info[ChangeInfo]. +
|
||||
If there are conflicts the change is marked as work-in-progress. +
|
||||
This option is not supported for all merge strategies (e.g. it's
|
||||
supported for `recursive` and `resolve`, but not for
|
||||
`simple-two-way-in-core`).
|
||||
|==============================
|
||||
|
||||
[[merge-patch-set-input]]
|
||||
=== MergePatchSetInput
|
||||
|
@@ -2576,9 +2576,8 @@ commit will be used.
|
||||
}
|
||||
----
|
||||
|
||||
As response a link:rest-api-changes.html#cherry-pick-change-info[
|
||||
CherryPickChangeInfo] entity is returned that describes the resulting
|
||||
cherry-picked change.
|
||||
As response a link:rest-api-changes.html#change-info[ChangeInfo] entity
|
||||
is returned that describes the resulting cherry-picked change.
|
||||
|
||||
.Response
|
||||
----
|
||||
|
@@ -23,6 +23,7 @@ import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
|
||||
import com.google.gerrit.extensions.events.GroupIndexedListener;
|
||||
import com.google.gerrit.extensions.events.ProjectIndexedListener;
|
||||
import com.google.gerrit.extensions.events.RevisionCreatedListener;
|
||||
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.registration.DynamicSet;
|
||||
import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
|
||||
@@ -69,6 +70,7 @@ public class ExtensionRegistry {
|
||||
private final DynamicSet<AccountActivationValidationListener>
|
||||
accountActivationValidationListeners;
|
||||
private final DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
|
||||
private final DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners;
|
||||
|
||||
@Inject
|
||||
ExtensionRegistry(
|
||||
@@ -93,7 +95,8 @@ public class ExtensionRegistry {
|
||||
DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
|
||||
DynamicSet<GroupBackend> groupBackends,
|
||||
DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
|
||||
DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners) {
|
||||
DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners,
|
||||
DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners) {
|
||||
this.accountIndexedListeners = accountIndexedListeners;
|
||||
this.changeIndexedListeners = changeIndexedListeners;
|
||||
this.groupIndexedListeners = groupIndexedListeners;
|
||||
@@ -116,6 +119,7 @@ public class ExtensionRegistry {
|
||||
this.groupBackends = groupBackends;
|
||||
this.accountActivationValidationListeners = accountActivationValidationListeners;
|
||||
this.onSubmitValidationListeners = onSubmitValidationListeners;
|
||||
this.workInProgressStateChangedListeners = workInProgressStateChangedListeners;
|
||||
}
|
||||
|
||||
public Registration newRegistration() {
|
||||
@@ -219,6 +223,10 @@ public class ExtensionRegistry {
|
||||
return add(onSubmitValidationListeners, onSubmitValidationListener);
|
||||
}
|
||||
|
||||
public Registration add(WorkInProgressStateChangedListener workInProgressStateChangedListener) {
|
||||
return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
|
||||
}
|
||||
|
||||
private <T> Registration add(DynamicSet<T> dynamicSet, T extension) {
|
||||
return add(dynamicSet, extension, "gerrit");
|
||||
}
|
||||
|
@@ -4,5 +4,8 @@ java_library(
|
||||
name = "exceptions",
|
||||
srcs = glob(["*.java"]),
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//java/com/google/gerrit/entities"],
|
||||
deps = [
|
||||
"//java/com/google/gerrit/entities",
|
||||
"//lib:jgit",
|
||||
],
|
||||
)
|
||||
|
@@ -0,0 +1,25 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.exceptions;
|
||||
|
||||
import org.eclipse.jgit.merge.MergeStrategy;
|
||||
|
||||
public class MergeWithConflictsNotSupportedException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MergeWithConflictsNotSupportedException(MergeStrategy strategy) {
|
||||
super("merge with conflicts is not supported with merge strategy: " + strategy.getName());
|
||||
}
|
||||
}
|
@@ -69,6 +69,8 @@ public interface Changes {
|
||||
|
||||
ChangeApi create(ChangeInput in) throws RestApiException;
|
||||
|
||||
ChangeInfo createAsInfo(ChangeInput in) throws RestApiException;
|
||||
|
||||
QueryRequest query();
|
||||
|
||||
QueryRequest query(String query);
|
||||
@@ -207,6 +209,11 @@ public interface Changes {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangeInfo createAsInfo(ChangeInput in) throws RestApiException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryRequest query() {
|
||||
throw new NotImplementedException();
|
||||
|
@@ -56,6 +56,22 @@ public class ChangeInfo {
|
||||
public Integer cherryPickOfChange;
|
||||
public Integer cherryPickOfPatchSet;
|
||||
|
||||
/**
|
||||
* Whether the change contains conflicts.
|
||||
*
|
||||
* <p>If {@code true}, some of the file contents of the change contain git conflict markers to
|
||||
* indicate the conflicts.
|
||||
*
|
||||
* <p>Only set if this change info is returned in response to a request that creates a new change
|
||||
* or patch set and conflicts are allowed. In particular this field is only populated if the
|
||||
* change info is returned by one of the following REST endpoints: {@link
|
||||
* 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}
|
||||
*/
|
||||
public Boolean containsGitConflicts;
|
||||
|
||||
public int _number;
|
||||
|
||||
public AccountInfo owner;
|
||||
|
@@ -14,6 +14,11 @@
|
||||
|
||||
package com.google.gerrit.extensions.common;
|
||||
|
||||
public class CherryPickChangeInfo extends ChangeInfo {
|
||||
public Boolean containsGitConflicts;
|
||||
}
|
||||
/**
|
||||
* {@link ChangeInfo} that is returned when a change was cherry-picked.
|
||||
*
|
||||
* <p>This class used to define additional fields to {@link ChangeInfo}, but those were pulled up
|
||||
* into {@link ChangeInfo}. Now this class is no longer needed, but we need to keep it for backwards
|
||||
* compatibility of the Java API.
|
||||
*/
|
||||
public class CherryPickChangeInfo extends ChangeInfo {}
|
||||
|
@@ -35,4 +35,12 @@ public class MergeInput {
|
||||
* @see org.eclipse.jgit.merge.MergeStrategy
|
||||
*/
|
||||
public String strategy;
|
||||
|
||||
/**
|
||||
* Whether the creation of the merge should succeed if there are conflicts.
|
||||
*
|
||||
* <p>If there are conflicts the file contents of the created change contain git conflict markers
|
||||
* to indicate the conflicts.
|
||||
*/
|
||||
public boolean allowConflicts;
|
||||
}
|
||||
|
@@ -57,6 +57,7 @@ import com.google.gerrit.server.config.SysExecutorModule;
|
||||
import com.google.gerrit.server.extensions.events.EventUtil;
|
||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||
import com.google.gerrit.server.extensions.events.RevisionCreated;
|
||||
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.git.PureRevertCache;
|
||||
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
||||
@@ -177,6 +178,7 @@ public class BatchProgramModule extends FactoryModule {
|
||||
bind(EventUtil.class).toProvider(Providers.of(null));
|
||||
bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
|
||||
bind(RevisionCreated.class).toInstance(RevisionCreated.DISABLED);
|
||||
bind(WorkInProgressStateChanged.class).toInstance(WorkInProgressStateChanged.DISABLED);
|
||||
bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
|
||||
}
|
||||
}
|
||||
|
@@ -98,6 +98,15 @@ class ChangesImpl implements Changes {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangeInfo createAsInfo(ChangeInput in) throws RestApiException {
|
||||
try {
|
||||
return createChange.apply(TopLevelResource.INSTANCE, in).value();
|
||||
} catch (Exception e) {
|
||||
throw asRestApiException("Cannot create change", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryRequest query() {
|
||||
return new QueryRequest() {
|
||||
|
@@ -34,6 +34,7 @@ import com.google.gerrit.server.PatchSetUtil;
|
||||
import com.google.gerrit.server.ReviewerSet;
|
||||
import com.google.gerrit.server.events.CommitReceivedEvent;
|
||||
import com.google.gerrit.server.extensions.events.RevisionCreated;
|
||||
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
|
||||
import com.google.gerrit.server.git.validators.CommitValidationException;
|
||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
|
||||
@@ -74,6 +75,7 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
private final ApprovalsUtil approvalsUtil;
|
||||
private final ChangeMessagesUtil cmUtil;
|
||||
private final PatchSetUtil psUtil;
|
||||
private final WorkInProgressStateChanged wipStateChanged;
|
||||
|
||||
// Assisted-injected fields.
|
||||
private final PatchSet.Id psId;
|
||||
@@ -86,6 +88,7 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
// Fields exposed as setters.
|
||||
private String message;
|
||||
private String description;
|
||||
private Boolean workInProgress;
|
||||
private boolean validate = true;
|
||||
private boolean checkAddPatchSetPermission = true;
|
||||
private List<String> groups = Collections.emptyList();
|
||||
@@ -99,6 +102,7 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
private PatchSetInfo patchSetInfo;
|
||||
private ChangeMessage changeMessage;
|
||||
private ReviewerSet oldReviewers;
|
||||
private boolean oldWorkInProgressState;
|
||||
|
||||
@Inject
|
||||
public PatchSetInserter(
|
||||
@@ -111,6 +115,7 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
PatchSetUtil psUtil,
|
||||
RevisionCreated revisionCreated,
|
||||
ProjectCache projectCache,
|
||||
WorkInProgressStateChanged wipStateChanged,
|
||||
@Assisted ChangeNotes notes,
|
||||
@Assisted PatchSet.Id psId,
|
||||
@Assisted ObjectId commitId) {
|
||||
@@ -123,6 +128,7 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
this.psUtil = psUtil;
|
||||
this.revisionCreated = revisionCreated;
|
||||
this.projectCache = projectCache;
|
||||
this.wipStateChanged = wipStateChanged;
|
||||
|
||||
this.origNotes = notes;
|
||||
this.psId = psId;
|
||||
@@ -143,6 +149,11 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
return this;
|
||||
}
|
||||
|
||||
public PatchSetInserter setWorkInProgress(boolean workInProgress) {
|
||||
this.workInProgress = workInProgress;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PatchSetInserter setValidate(boolean validate) {
|
||||
this.validate = validate;
|
||||
return this;
|
||||
@@ -230,6 +241,13 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
changeMessage.setMessage(message);
|
||||
}
|
||||
|
||||
oldWorkInProgressState = change.isWorkInProgress();
|
||||
if (workInProgress != null) {
|
||||
change.setWorkInProgress(workInProgress);
|
||||
change.setReviewStarted(!workInProgress);
|
||||
update.setWorkInProgress(workInProgress);
|
||||
}
|
||||
|
||||
patchSetInfo =
|
||||
patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
|
||||
if (!allowClosed) {
|
||||
@@ -265,6 +283,10 @@ public class PatchSetInserter implements BatchUpdateOp {
|
||||
if (fireRevisionCreated) {
|
||||
revisionCreated.fire(change, patchSet, ctx.getAccount(), ctx.getWhen(), notify);
|
||||
}
|
||||
|
||||
if (workInProgress != null && oldWorkInProgressState != workInProgress) {
|
||||
wipStateChanged.fire(change, patchSet, ctx.getAccount(), ctx.getWhen());
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(RepoContext ctx)
|
||||
|
@@ -38,6 +38,12 @@ import java.sql.Timestamp;
|
||||
public class WorkInProgressStateChanged {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
public static final WorkInProgressStateChanged DISABLED =
|
||||
new WorkInProgressStateChanged() {
|
||||
@Override
|
||||
public void fire(Change change, PatchSet patchSet, AccountState account, Timestamp when) {}
|
||||
};
|
||||
|
||||
private final PluginSetContext<WorkInProgressStateChangedListener> listeners;
|
||||
private final EventUtil util;
|
||||
|
||||
@@ -48,6 +54,11 @@ public class WorkInProgressStateChanged {
|
||||
this.util = util;
|
||||
}
|
||||
|
||||
private WorkInProgressStateChanged() {
|
||||
this.listeners = null;
|
||||
this.util = null;
|
||||
}
|
||||
|
||||
public void fire(Change change, PatchSet patchSet, AccountState account, Timestamp when) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
|
@@ -41,6 +41,7 @@ import com.google.gerrit.entities.LabelId;
|
||||
import com.google.gerrit.entities.PatchSet;
|
||||
import com.google.gerrit.entities.PatchSetApproval;
|
||||
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
|
||||
import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
|
||||
import com.google.gerrit.exceptions.StorageException;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.extensions.registration.DynamicSet;
|
||||
@@ -424,15 +425,16 @@ public class MergeUtil {
|
||||
return dc.writeTree(ins);
|
||||
}
|
||||
|
||||
public static RevCommit createMergeCommit(
|
||||
public static CodeReviewCommit createMergeCommit(
|
||||
ObjectInserter inserter,
|
||||
Config repoConfig,
|
||||
RevCommit mergeTip,
|
||||
RevCommit originalCommit,
|
||||
String mergeStrategy,
|
||||
boolean allowConflicts,
|
||||
PersonIdent committerIndent,
|
||||
String commitMsg,
|
||||
RevWalk rw)
|
||||
CodeReviewRevWalk rw)
|
||||
throws IOException, MergeIdenticalTreeException, MergeConflictException,
|
||||
InvalidMergeStrategyException {
|
||||
|
||||
@@ -443,8 +445,53 @@ public class MergeUtil {
|
||||
}
|
||||
|
||||
Merger m = newMerger(inserter, repoConfig, mergeStrategy);
|
||||
|
||||
DirCache dc = DirCache.newInCore();
|
||||
if (allowConflicts && m instanceof ResolveMerger) {
|
||||
// The DirCache must be set on ResolveMerger before calling
|
||||
// ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
|
||||
((ResolveMerger) m).setDirCache(dc);
|
||||
}
|
||||
|
||||
ObjectId tree;
|
||||
ImmutableSet<String> filesWithGitConflicts;
|
||||
if (m.merge(false, mergeTip, originalCommit)) {
|
||||
ObjectId tree = m.getResultTreeId();
|
||||
filesWithGitConflicts = null;
|
||||
tree = m.getResultTreeId();
|
||||
} else {
|
||||
List<String> conflicts = ImmutableList.of();
|
||||
if (m instanceof ResolveMerger) {
|
||||
conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
||||
}
|
||||
|
||||
if (!allowConflicts) {
|
||||
throw new MergeConflictException(createConflictMessage(conflicts));
|
||||
}
|
||||
|
||||
// For merging with conflict markers we need a ResolveMerger, double-check that we have one.
|
||||
if (!(m instanceof ResolveMerger)) {
|
||||
throw new MergeWithConflictsNotSupportedException(MergeStrategy.get(mergeStrategy));
|
||||
}
|
||||
Map<String, MergeResult<? extends Sequence>> mergeResults =
|
||||
((ResolveMerger) m).getMergeResults();
|
||||
|
||||
filesWithGitConflicts =
|
||||
mergeResults.entrySet().stream()
|
||||
.filter(e -> e.getValue().containsConflicts())
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(toImmutableSet());
|
||||
|
||||
tree =
|
||||
mergeWithConflicts(
|
||||
rw,
|
||||
inserter,
|
||||
dc,
|
||||
"TARGET BRANCH",
|
||||
mergeTip,
|
||||
"SOURCE BRANCH",
|
||||
originalCommit,
|
||||
mergeResults);
|
||||
}
|
||||
|
||||
CommitBuilder mergeCommit = new CommitBuilder();
|
||||
mergeCommit.setTreeId(tree);
|
||||
@@ -452,13 +499,9 @@ public class MergeUtil {
|
||||
mergeCommit.setAuthor(committerIndent);
|
||||
mergeCommit.setCommitter(committerIndent);
|
||||
mergeCommit.setMessage(commitMsg);
|
||||
return rw.parseCommit(inserter.insert(mergeCommit));
|
||||
}
|
||||
List<String> conflicts = ImmutableList.of();
|
||||
if (m instanceof ResolveMerger) {
|
||||
conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
||||
}
|
||||
throw new MergeConflictException(createConflictMessage(conflicts));
|
||||
CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit));
|
||||
commit.setFilesWithGitConflicts(filesWithGitConflicts);
|
||||
return commit;
|
||||
}
|
||||
|
||||
public static String createConflictMessage(List<String> conflicts) {
|
||||
|
@@ -29,6 +29,7 @@ import com.google.gerrit.entities.PatchSet;
|
||||
import com.google.gerrit.entities.Project;
|
||||
import com.google.gerrit.entities.RefNames;
|
||||
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
|
||||
import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
|
||||
import com.google.gerrit.extensions.api.changes.NotifyHandling;
|
||||
import com.google.gerrit.extensions.client.ChangeStatus;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
@@ -56,6 +57,8 @@ import com.google.gerrit.server.change.ChangeResource;
|
||||
import com.google.gerrit.server.change.NotifyResolver;
|
||||
import com.google.gerrit.server.config.AnonymousCowardName;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
@@ -186,9 +189,8 @@ public class CreateChange
|
||||
|
||||
checkRequiredPermissions(project, input.branch);
|
||||
|
||||
Change newChange = createNewChange(input, me, projectState, updateFactory);
|
||||
ChangeJson json = jsonFactory.noOptions();
|
||||
return Response.created(json.format(newChange));
|
||||
ChangeInfo newChange = createNewChange(input, me, projectState, updateFactory);
|
||||
return Response.created(newChange);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,7 +291,7 @@ public class CreateChange
|
||||
.check(RefPermission.CREATE_CHANGE);
|
||||
}
|
||||
|
||||
private Change createNewChange(
|
||||
private ChangeInfo createNewChange(
|
||||
ChangeInput input,
|
||||
IdentifiedUser me,
|
||||
ProjectState projectState,
|
||||
@@ -304,7 +306,7 @@ public class CreateChange
|
||||
try (Repository git = gitManager.openRepository(projectState.getNameKey());
|
||||
ObjectInserter oi = git.newObjectInserter();
|
||||
ObjectReader reader = oi.newReader();
|
||||
RevWalk rw = new RevWalk(reader)) {
|
||||
CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
|
||||
PatchSet basePatchSet = null;
|
||||
List<String> groups = Collections.emptyList();
|
||||
|
||||
@@ -327,10 +329,15 @@ public class CreateChange
|
||||
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
|
||||
String commitMessage = getCommitMessage(input.subject, me);
|
||||
|
||||
RevCommit c;
|
||||
CodeReviewCommit c;
|
||||
if (input.merge != null) {
|
||||
// create a merge commit
|
||||
c = newMergeCommit(git, oi, rw, projectState, mergeTip, input.merge, author, commitMessage);
|
||||
if (!c.getFilesWithGitConflicts().isEmpty()) {
|
||||
logger.atFine().log(
|
||||
"merge commit has conflicts in the following files: %s",
|
||||
c.getFilesWithGitConflicts());
|
||||
}
|
||||
} else {
|
||||
// create an empty commit
|
||||
c = newCommit(oi, rw, author, mergeTip, commitMessage);
|
||||
@@ -338,10 +345,10 @@ public class CreateChange
|
||||
|
||||
Change.Id changeId = Change.id(seq.nextChangeId());
|
||||
ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
|
||||
ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
|
||||
ins.setMessage(messageForNewChange(ins.getPatchSetId(), c));
|
||||
ins.setTopic(input.topic);
|
||||
ins.setPrivate(input.isPrivate);
|
||||
ins.setWorkInProgress(input.workInProgress);
|
||||
ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty());
|
||||
ins.setGroups(groups);
|
||||
try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
|
||||
bu.setRepository(git, rw, oi);
|
||||
@@ -351,8 +358,10 @@ public class CreateChange
|
||||
bu.insertChange(ins);
|
||||
bu.execute();
|
||||
}
|
||||
return ins.getChange();
|
||||
} catch (InvalidMergeStrategyException e) {
|
||||
ChangeInfo changeInfo = jsonFactory.noOptions().format(ins.getChange());
|
||||
changeInfo.containsGitConflicts = !c.getFilesWithGitConflicts().isEmpty() ? true : null;
|
||||
return changeInfo;
|
||||
} catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -469,9 +478,9 @@ public class CreateChange
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
private static RevCommit newCommit(
|
||||
private static CodeReviewCommit newCommit(
|
||||
ObjectInserter oi,
|
||||
RevWalk rw,
|
||||
CodeReviewRevWalk rw,
|
||||
PersonIdent authorIdent,
|
||||
RevCommit mergeTip,
|
||||
String commitMessage)
|
||||
@@ -490,10 +499,10 @@ public class CreateChange
|
||||
return rw.parseCommit(insert(oi, commit));
|
||||
}
|
||||
|
||||
private RevCommit newMergeCommit(
|
||||
private CodeReviewCommit newMergeCommit(
|
||||
Repository repo,
|
||||
ObjectInserter oi,
|
||||
RevWalk rw,
|
||||
CodeReviewRevWalk rw,
|
||||
ProjectState projectState,
|
||||
RevCommit mergeTip,
|
||||
MergeInput merge,
|
||||
@@ -501,7 +510,8 @@ public class CreateChange
|
||||
String commitMessage)
|
||||
throws RestApiException, IOException {
|
||||
logger.atFine().log(
|
||||
"Creating merge commit: source = %s, strategy = %s", merge.source, merge.strategy);
|
||||
"Creating merge commit: source = %s, strategy = %s, allowConflicts",
|
||||
merge.source, merge.strategy, merge.allowConflicts);
|
||||
|
||||
if (Strings.isNullOrEmpty(merge.source)) {
|
||||
throw new BadRequestException("merge.source must be non-empty");
|
||||
@@ -531,6 +541,7 @@ public class CreateChange
|
||||
mergeTip,
|
||||
sourceCommit,
|
||||
mergeStrategy,
|
||||
merge.allowConflicts,
|
||||
authorIdent,
|
||||
commitMessage,
|
||||
rw);
|
||||
@@ -540,6 +551,20 @@ public class CreateChange
|
||||
}
|
||||
}
|
||||
|
||||
private static String messageForNewChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
|
||||
StringBuilder stringBuilder =
|
||||
new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.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();
|
||||
}
|
||||
|
||||
private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException {
|
||||
ObjectId id = inserter.insert(commit);
|
||||
inserter.flush();
|
||||
|
@@ -22,6 +22,7 @@ import com.google.gerrit.entities.Change;
|
||||
import com.google.gerrit.entities.PatchSet;
|
||||
import com.google.gerrit.entities.Project;
|
||||
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
|
||||
import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
|
||||
import com.google.gerrit.extensions.client.ListChangesOption;
|
||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||
import com.google.gerrit.extensions.common.MergeInput;
|
||||
@@ -44,6 +45,8 @@ import com.google.gerrit.server.change.ChangeJson;
|
||||
import com.google.gerrit.server.change.ChangeResource;
|
||||
import com.google.gerrit.server.change.NotifyResolver;
|
||||
import com.google.gerrit.server.change.PatchSetInserter;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
@@ -71,7 +74,6 @@ import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.util.ChangeIdUtil;
|
||||
|
||||
@Singleton
|
||||
@@ -141,7 +143,7 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
try (Repository git = gitManager.openRepository(project);
|
||||
ObjectInserter oi = git.newObjectInserter();
|
||||
ObjectReader reader = oi.newReader();
|
||||
RevWalk rw = new RevWalk(reader)) {
|
||||
CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
|
||||
|
||||
RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, merge.source);
|
||||
if (!commits.canRead(projectState, git, sourceCommit)) {
|
||||
@@ -162,7 +164,7 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
Timestamp now = TimeUtil.nowTs();
|
||||
IdentifiedUser me = user.get().asIdentifiedUser();
|
||||
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
|
||||
RevCommit newCommit =
|
||||
CodeReviewCommit newCommit =
|
||||
createMergeCommit(
|
||||
in,
|
||||
projectState,
|
||||
@@ -182,7 +184,8 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
bu.setRepository(git, rw, oi);
|
||||
bu.setNotify(NotifyResolver.Result.none());
|
||||
psInserter
|
||||
.setMessage("Uploaded patch set " + nextPsId.get() + ".")
|
||||
.setMessage(messageForChange(nextPsId, newCommit))
|
||||
.setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
|
||||
.setCheckAddPatchSetPermission(false);
|
||||
if (groups != null) {
|
||||
psInserter.setGroups(groups);
|
||||
@@ -192,8 +195,11 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
}
|
||||
|
||||
ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
|
||||
return Response.ok(json.format(psInserter.getChange()));
|
||||
} catch (InvalidMergeStrategyException e) {
|
||||
ChangeInfo changeInfo = json.format(psInserter.getChange());
|
||||
changeInfo.containsGitConflicts =
|
||||
!newCommit.getFilesWithGitConflicts().isEmpty() ? true : null;
|
||||
return Response.ok(changeInfo);
|
||||
} catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -213,13 +219,13 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
return psUtil.current(change);
|
||||
}
|
||||
|
||||
private RevCommit createMergeCommit(
|
||||
private CodeReviewCommit createMergeCommit(
|
||||
MergePatchSetInput in,
|
||||
ProjectState projectState,
|
||||
BranchNameKey dest,
|
||||
Repository git,
|
||||
ObjectInserter oi,
|
||||
RevWalk rw,
|
||||
CodeReviewRevWalk rw,
|
||||
RevCommit currentPsCommit,
|
||||
RevCommit sourceCommit,
|
||||
PersonIdent author,
|
||||
@@ -258,6 +264,28 @@ public class CreateMergePatchSet implements RestModifyView<ChangeResource, Merge
|
||||
mergeUtilFactory.create(projectState).mergeStrategyName());
|
||||
|
||||
return MergeUtil.createMergeCommit(
|
||||
oi, git.getConfig(), mergeTip, sourceCommit, mergeStrategy, author, commitMsg, rw);
|
||||
oi,
|
||||
git.getConfig(),
|
||||
mergeTip,
|
||||
sourceCommit,
|
||||
mergeStrategy,
|
||||
in.merge.allowConflicts,
|
||||
author,
|
||||
commitMsg,
|
||||
rw);
|
||||
}
|
||||
|
||||
private static String messageForChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
|
||||
StringBuilder stringBuilder =
|
||||
new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.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();
|
||||
}
|
||||
}
|
||||
|
@@ -47,6 +47,7 @@ import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS
|
||||
import static com.google.gerrit.extensions.client.ReviewerState.CC;
|
||||
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
|
||||
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
|
||||
import static com.google.gerrit.git.ObjectIds.abbreviateName;
|
||||
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
|
||||
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
|
||||
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
|
||||
@@ -144,8 +145,10 @@ import com.google.gerrit.extensions.common.MergeInput;
|
||||
import com.google.gerrit.extensions.common.MergePatchSetInput;
|
||||
import com.google.gerrit.extensions.common.RevisionInfo;
|
||||
import com.google.gerrit.extensions.common.TrackingIdInfo;
|
||||
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
@@ -180,6 +183,7 @@ import com.google.gerrit.testing.FakeEmailSender.Message;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
@@ -3207,13 +3211,29 @@ public class ChangeIT extends AbstractDaemonTest {
|
||||
MergePatchSetInput in = new MergePatchSetInput();
|
||||
in.merge = mergeInput;
|
||||
in.subject = "update change by merge ps2";
|
||||
gApi.changes().id(changeId).createMergePatchSet(in);
|
||||
|
||||
TestWorkInProgressStateChangedListener wipStateChangedListener =
|
||||
new TestWorkInProgressStateChangedListener();
|
||||
try (Registration registration =
|
||||
extensionRegistry.newRegistration().add(wipStateChangedListener)) {
|
||||
ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in);
|
||||
assertThat(changeInfo.subject).isEqualTo(in.subject);
|
||||
assertThat(changeInfo.containsGitConflicts).isNull();
|
||||
assertThat(changeInfo.workInProgress).isNull();
|
||||
}
|
||||
assertThat(wipStateChangedListener.invoked).isFalse();
|
||||
|
||||
// To get the revisions, we must retrieve the change with more change options.
|
||||
ChangeInfo changeInfo =
|
||||
gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
|
||||
assertThat(changeInfo.revisions).hasSize(2);
|
||||
assertThat(changeInfo.subject).isEqualTo(in.subject);
|
||||
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
|
||||
.isEqualTo(parent);
|
||||
|
||||
// Verify the message that has been posted on the change.
|
||||
List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
|
||||
assertThat(messages).hasSize(2);
|
||||
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3251,6 +3271,142 @@ public class ChangeIT extends AbstractDaemonTest {
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergePatchSet_ConflictAllowed() throws Exception {
|
||||
RevCommit initialHead = projectOperations.project(project).getHead("master");
|
||||
createBranch("dev");
|
||||
|
||||
// create a change for master
|
||||
String changeId = createChange().getChangeId();
|
||||
|
||||
String fileName = "shared.txt";
|
||||
String sourceSubject = "source change";
|
||||
String sourceContent = "source content";
|
||||
String targetSubject = "target change";
|
||||
String targetContent = "target content";
|
||||
testRepo.reset(initialHead);
|
||||
PushOneCommit.Result currentMaster =
|
||||
pushFactory
|
||||
.create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
|
||||
.to("refs/heads/master");
|
||||
currentMaster.assertOkStatus();
|
||||
String parent = currentMaster.getCommit().getName();
|
||||
|
||||
// push a commit into dev branch
|
||||
testRepo.reset(initialHead);
|
||||
PushOneCommit.Result changeA =
|
||||
pushFactory
|
||||
.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
|
||||
.to("refs/heads/dev");
|
||||
changeA.assertOkStatus();
|
||||
MergeInput mergeInput = new MergeInput();
|
||||
mergeInput.source = "dev";
|
||||
mergeInput.allowConflicts = true;
|
||||
MergePatchSetInput in = new MergePatchSetInput();
|
||||
in.merge = mergeInput;
|
||||
in.subject = "update change by merge ps2";
|
||||
|
||||
TestWorkInProgressStateChangedListener wipStateChangedListener =
|
||||
new TestWorkInProgressStateChangedListener();
|
||||
try (Registration registration =
|
||||
extensionRegistry.newRegistration().add(wipStateChangedListener)) {
|
||||
ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in);
|
||||
assertThat(changeInfo.subject).isEqualTo(in.subject);
|
||||
assertThat(changeInfo.containsGitConflicts).isTrue();
|
||||
assertThat(changeInfo.workInProgress).isTrue();
|
||||
}
|
||||
assertThat(wipStateChangedListener.invoked).isTrue();
|
||||
assertThat(wipStateChangedListener.wip).isTrue();
|
||||
|
||||
// To get the revisions, we must retrieve the change with more change options.
|
||||
ChangeInfo changeInfo =
|
||||
gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
|
||||
assertThat(changeInfo.revisions).hasSize(2);
|
||||
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
|
||||
.isEqualTo(parent);
|
||||
|
||||
// Verify that the file content in the created patch set is correct.
|
||||
// We expect that it has conflict markers to indicate the conflict.
|
||||
BinaryResult bin = gApi.changes().id(changeId).current().file(fileName).content();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
bin.writeTo(os);
|
||||
String fileContent = new String(os.toByteArray(), UTF_8);
|
||||
String sourceSha1 = abbreviateName(changeA.getCommit(), 6);
|
||||
String targetSha1 = abbreviateName(currentMaster.getCommit(), 6);
|
||||
assertThat(fileContent)
|
||||
.isEqualTo(
|
||||
"<<<<<<< TARGET BRANCH ("
|
||||
+ targetSha1
|
||||
+ " "
|
||||
+ targetSubject
|
||||
+ ")\n"
|
||||
+ targetContent
|
||||
+ "\n"
|
||||
+ "=======\n"
|
||||
+ sourceContent
|
||||
+ "\n"
|
||||
+ ">>>>>>> SOURCE BRANCH ("
|
||||
+ sourceSha1
|
||||
+ " "
|
||||
+ sourceSubject
|
||||
+ ")\n");
|
||||
|
||||
// Verify the message that has been posted on the change.
|
||||
List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
|
||||
assertThat(messages).hasSize(2);
|
||||
assertThat(Iterables.getLast(messages).message)
|
||||
.isEqualTo(
|
||||
"Uploaded patch set 2.\n\n"
|
||||
+ "The following files contain Git conflicts:\n"
|
||||
+ "* "
|
||||
+ fileName
|
||||
+ "\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergePatchSet_ConflictAllowedNotSupportedByMergeStrategy() throws Exception {
|
||||
RevCommit initialHead = projectOperations.project(project).getHead("master");
|
||||
createBranch("dev");
|
||||
|
||||
// create a change for master
|
||||
String changeId = createChange().getChangeId();
|
||||
|
||||
String fileName = "shared.txt";
|
||||
String sourceSubject = "source change";
|
||||
String sourceContent = "source content";
|
||||
String targetSubject = "target change";
|
||||
String targetContent = "target content";
|
||||
testRepo.reset(initialHead);
|
||||
PushOneCommit.Result currentMaster =
|
||||
pushFactory
|
||||
.create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
|
||||
.to("refs/heads/master");
|
||||
currentMaster.assertOkStatus();
|
||||
|
||||
// push a commit into dev branch
|
||||
testRepo.reset(initialHead);
|
||||
PushOneCommit.Result changeA =
|
||||
pushFactory
|
||||
.create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
|
||||
.to("refs/heads/dev");
|
||||
changeA.assertOkStatus();
|
||||
MergeInput mergeInput = new MergeInput();
|
||||
mergeInput.source = "dev";
|
||||
mergeInput.allowConflicts = true;
|
||||
mergeInput.strategy = "simple-two-way-in-core";
|
||||
MergePatchSetInput in = new MergePatchSetInput();
|
||||
in.merge = mergeInput;
|
||||
in.subject = "update change by merge ps2";
|
||||
|
||||
BadRequestException ex =
|
||||
assertThrows(
|
||||
BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
|
||||
assertThat(ex)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"merge with conflicts is not supported with merge strategy: " + mergeInput.strategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergePatchSetInheritParent() throws Exception {
|
||||
RevCommit initialHead = projectOperations.project(project).getHead("master");
|
||||
@@ -4535,4 +4691,17 @@ public class ChangeIT extends AbstractDaemonTest {
|
||||
private interface AddReviewerCaller {
|
||||
void call(String changeId, String reviewer) throws RestApiException;
|
||||
}
|
||||
|
||||
private static class TestWorkInProgressStateChangedListener
|
||||
implements WorkInProgressStateChangedListener {
|
||||
boolean invoked;
|
||||
Boolean wip;
|
||||
|
||||
@Override
|
||||
public void onWorkInProgressStateChanged(Event event) {
|
||||
this.invoked = true;
|
||||
this.wip =
|
||||
event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,12 +18,15 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
|
||||
import static com.google.gerrit.common.data.Permission.READ;
|
||||
import static com.google.gerrit.entities.RefNames.changeMetaRef;
|
||||
import static com.google.gerrit.git.ObjectIds.abbreviateName;
|
||||
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
|
||||
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||
import com.google.gerrit.acceptance.PushOneCommit;
|
||||
import com.google.gerrit.acceptance.PushOneCommit.Result;
|
||||
@@ -44,9 +47,11 @@ import com.google.gerrit.extensions.client.ChangeStatus;
|
||||
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||
import com.google.gerrit.extensions.common.ChangeInput;
|
||||
import com.google.gerrit.extensions.common.ChangeMessageInfo;
|
||||
import com.google.gerrit.extensions.common.MergeInput;
|
||||
import com.google.gerrit.extensions.restapi.AuthException;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||
@@ -54,6 +59,7 @@ import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
||||
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
|
||||
import com.google.gerrit.testing.FakeEmailSender.Message;
|
||||
import com.google.inject.Inject;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -141,6 +147,11 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
|
||||
assertThat(info.revisions.get(info.currentRevision).commit.message)
|
||||
.contains("Change-Id: " + info.changeId);
|
||||
|
||||
// Verify the message that has been posted on the change.
|
||||
List<ChangeMessageInfo> messages = gApi.changes().id(info._number).messages();
|
||||
assertThat(messages).hasSize(1);
|
||||
assertThat(Iterables.getOnlyElement(messages).message).isEqualTo("Uploaded patch set 1.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -364,7 +375,12 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
public void createMergeChange() throws Exception {
|
||||
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
|
||||
ChangeInput in = newMergeChangeInput("branchA", "branchB", "");
|
||||
assertCreateSucceeds(in);
|
||||
ChangeInfo change = assertCreateSucceeds(in);
|
||||
|
||||
// Verify the message that has been posted on the change.
|
||||
List<ChangeMessageInfo> messages = gApi.changes().id(change._number).messages();
|
||||
assertThat(messages).hasSize(1);
|
||||
assertThat(Iterables.getOnlyElement(messages).message).isEqualTo("Uploaded patch set 1.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -381,6 +397,87 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
assertCreateSucceeds(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergeChange_ConflictsAllowed() throws Exception {
|
||||
String fileName = "shared.txt";
|
||||
String sourceBranch = "sourceBranch";
|
||||
String sourceSubject = "source change";
|
||||
String sourceContent = "source content";
|
||||
String targetBranch = "targetBranch";
|
||||
String targetSubject = "target change";
|
||||
String targetContent = "target content";
|
||||
changeInTwoBranches(
|
||||
sourceBranch,
|
||||
sourceSubject,
|
||||
fileName,
|
||||
sourceContent,
|
||||
targetBranch,
|
||||
targetSubject,
|
||||
fileName,
|
||||
targetContent);
|
||||
ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, "", true);
|
||||
ChangeInfo change = assertCreateSucceedsWithConflicts(in);
|
||||
|
||||
// Verify that the file content in the created change is correct.
|
||||
// We expect that it has conflict markers to indicate the conflict.
|
||||
BinaryResult bin = gApi.changes().id(change._number).current().file(fileName).content();
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
bin.writeTo(os);
|
||||
String fileContent = new String(os.toByteArray(), UTF_8);
|
||||
String sourceSha1 = abbreviateName(projectOperations.project(project).getHead(sourceBranch), 6);
|
||||
String targetSha1 = abbreviateName(projectOperations.project(project).getHead(targetBranch), 6);
|
||||
assertThat(fileContent)
|
||||
.isEqualTo(
|
||||
"<<<<<<< TARGET BRANCH ("
|
||||
+ targetSha1
|
||||
+ " "
|
||||
+ targetSubject
|
||||
+ ")\n"
|
||||
+ targetContent
|
||||
+ "\n"
|
||||
+ "=======\n"
|
||||
+ sourceContent
|
||||
+ "\n"
|
||||
+ ">>>>>>> SOURCE BRANCH ("
|
||||
+ sourceSha1
|
||||
+ " "
|
||||
+ sourceSubject
|
||||
+ ")\n");
|
||||
|
||||
// Verify the message that has been posted on the change.
|
||||
List<ChangeMessageInfo> messages = gApi.changes().id(change._number).messages();
|
||||
assertThat(messages).hasSize(1);
|
||||
assertThat(Iterables.getOnlyElement(messages).message)
|
||||
.isEqualTo(
|
||||
"Uploaded patch set 1.\n\n"
|
||||
+ "The following files contain Git conflicts:\n"
|
||||
+ "* "
|
||||
+ fileName
|
||||
+ "\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergeChange_ConflictAllowedNotSupportedByMergeStrategy() throws Exception {
|
||||
String fileName = "shared.txt";
|
||||
String sourceBranch = "sourceBranch";
|
||||
String targetBranch = "targetBranch";
|
||||
changeInTwoBranches(
|
||||
sourceBranch,
|
||||
"source change",
|
||||
fileName,
|
||||
"source content",
|
||||
targetBranch,
|
||||
"target change",
|
||||
fileName,
|
||||
"target content");
|
||||
String mergeStrategy = "simple-two-way-in-core";
|
||||
ChangeInput in = newMergeChangeInput(targetBranch, sourceBranch, mergeStrategy, true);
|
||||
assertCreateFails(
|
||||
in,
|
||||
BadRequestException.class,
|
||||
"merge with conflicts is not supported with merge strategy: " + mergeStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMergeChangeFailsWithConflictIfThereAreTooManyCommonPredecessors()
|
||||
throws Exception {
|
||||
@@ -732,6 +829,26 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
}
|
||||
assertThat(out.revisions).hasSize(1);
|
||||
assertThat(out.submitted).isNull();
|
||||
assertThat(out.containsGitConflicts).isNull();
|
||||
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
|
||||
return out;
|
||||
}
|
||||
|
||||
private ChangeInfo assertCreateSucceedsWithConflicts(ChangeInput in) throws Exception {
|
||||
ChangeInfo out = gApi.changes().createAsInfo(in);
|
||||
assertThat(out.project).isEqualTo(in.project);
|
||||
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
|
||||
assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
|
||||
assertThat(out.topic).isEqualTo(in.topic);
|
||||
assertThat(out.status).isEqualTo(in.status);
|
||||
if (in.isPrivate) {
|
||||
assertThat(out.isPrivate).isTrue();
|
||||
} else {
|
||||
assertThat(out.isPrivate).isNull();
|
||||
}
|
||||
assertThat(out.submitted).isNull();
|
||||
assertThat(out.containsGitConflicts).isTrue();
|
||||
assertThat(out.workInProgress).isTrue();
|
||||
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
|
||||
return out;
|
||||
}
|
||||
@@ -764,6 +881,11 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
}
|
||||
|
||||
private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef, String strategy) {
|
||||
return newMergeChangeInput(targetBranch, sourceRef, strategy, false);
|
||||
}
|
||||
|
||||
private ChangeInput newMergeChangeInput(
|
||||
String targetBranch, String sourceRef, String strategy, boolean allowConflicts) {
|
||||
// create a merge change from branchA to master in gerrit
|
||||
ChangeInput in = new ChangeInput();
|
||||
in.project = project.get();
|
||||
@@ -776,6 +898,7 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
if (!Strings.isNullOrEmpty(strategy)) {
|
||||
in.merge.strategy = strategy;
|
||||
}
|
||||
in.merge.allowConflicts = allowConflicts;
|
||||
return in;
|
||||
}
|
||||
|
||||
@@ -791,6 +914,34 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
*/
|
||||
private Map<String, Result> changeInTwoBranches(
|
||||
String branchA, String fileA, String branchB, String fileB) throws Exception {
|
||||
return changeInTwoBranches(
|
||||
branchA, "change A", fileA, "A content", branchB, "change B", fileB, "B content");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty commit in master, two new branches with one commit each.
|
||||
*
|
||||
* @param branchA name of first branch to create
|
||||
* @param subjectA commit message subject for the change on branchA
|
||||
* @param fileA name of file to commit to branchA
|
||||
* @param contentA file content to commit to branchA
|
||||
* @param branchB name of second branch to create
|
||||
* @param subjectB commit message subject for the change on branchB
|
||||
* @param fileB name of file to commit to branchB
|
||||
* @param contentB file content to commit to branchB
|
||||
* @return A {@code Map} of branchName => commit result.
|
||||
* @throws Exception
|
||||
*/
|
||||
private Map<String, Result> changeInTwoBranches(
|
||||
String branchA,
|
||||
String subjectA,
|
||||
String fileA,
|
||||
String contentA,
|
||||
String branchB,
|
||||
String subjectB,
|
||||
String fileB,
|
||||
String contentB)
|
||||
throws Exception {
|
||||
// create a initial commit in master
|
||||
Result initialCommit =
|
||||
pushFactory
|
||||
@@ -805,13 +956,13 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
||||
// create a commit in branchA
|
||||
Result changeA =
|
||||
pushFactory
|
||||
.create(user.newIdent(), testRepo, "change A", fileA, "A content")
|
||||
.create(user.newIdent(), testRepo, subjectA, fileA, contentA)
|
||||
.to("refs/heads/" + branchA);
|
||||
changeA.assertOkStatus();
|
||||
|
||||
// create a commit in branchB
|
||||
PushOneCommit commitB =
|
||||
pushFactory.create(user.newIdent(), testRepo, "change B", fileB, "B content");
|
||||
pushFactory.create(user.newIdent(), testRepo, subjectB, fileB, contentB);
|
||||
commitB.setParent(initialCommit.getCommit());
|
||||
Result changeB = commitB.to("refs/heads/" + branchB);
|
||||
changeB.assertOkStatus();
|
||||
|
Reference in New Issue
Block a user