Merge "Allow creating merge commits with conflicts"
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
|
||||
----
|
||||
@@ -6042,10 +6042,21 @@ This ID is equal to the numeric ID of the change that triggered the submission.
|
||||
If the change that triggered the submission also has a topic, it will be
|
||||
"<id>-<topic>" of the change that triggered the submission.
|
||||
The callers must not rely on the format of the submission ID.
|
||||
|`cherry_pick_of_change` |optional|
|
||||
|`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,21 +6797,31 @@ 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` ||
|
||||
|==============================
|
||||
|Field Name ||Description
|
||||
|`source` ||
|
||||
The source to merge from, e.g. a complete or abbreviated commit SHA-1,
|
||||
a complete reference name, a short reference name under `refs/heads`, `refs/tags`,
|
||||
or `refs/remotes` namespace, etc.
|
||||
|`source_branch` |optional|
|
||||
|`source_branch` |optional|
|
||||
A branch from which `source` is reachable. If specified,
|
||||
`source` is checked for visibility and reachability against only this
|
||||
branch. This speeds up the operation, especially for large repos with
|
||||
many branches.
|
||||
|`strategy` |optional|
|
||||
|`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,22 +445,63 @@ public class MergeUtil {
|
||||
}
|
||||
|
||||
Merger m = newMerger(inserter, repoConfig, mergeStrategy);
|
||||
if (m.merge(false, mergeTip, originalCommit)) {
|
||||
ObjectId tree = m.getResultTreeId();
|
||||
|
||||
CommitBuilder mergeCommit = new CommitBuilder();
|
||||
mergeCommit.setTreeId(tree);
|
||||
mergeCommit.setParentIds(mergeTip, originalCommit);
|
||||
mergeCommit.setAuthor(committerIndent);
|
||||
mergeCommit.setCommitter(committerIndent);
|
||||
mergeCommit.setMessage(commitMsg);
|
||||
return rw.parseCommit(inserter.insert(mergeCommit));
|
||||
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);
|
||||
}
|
||||
List<String> conflicts = ImmutableList.of();
|
||||
if (m instanceof ResolveMerger) {
|
||||
conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
||||
|
||||
ObjectId tree;
|
||||
ImmutableSet<String> filesWithGitConflicts;
|
||||
if (m.merge(false, mergeTip, originalCommit)) {
|
||||
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);
|
||||
}
|
||||
throw new MergeConflictException(createConflictMessage(conflicts));
|
||||
|
||||
CommitBuilder mergeCommit = new CommitBuilder();
|
||||
mergeCommit.setTreeId(tree);
|
||||
mergeCommit.setParentIds(mergeTip, originalCommit);
|
||||
mergeCommit.setAuthor(committerIndent);
|
||||
mergeCommit.setCommitter(committerIndent);
|
||||
mergeCommit.setMessage(commitMsg);
|
||||
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