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:
Edwin Kempin
2020-01-29 13:17:04 +01:00
parent 13f97a4424
commit aab27d38a0
18 changed files with 616 additions and 82 deletions

View File

@@ -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

View File

@@ -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
----

View File

@@ -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");
}

View File

@@ -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",
],
)

View File

@@ -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());
}
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();