Add merge feature into /changes REST endpoint
Allow user to create a merge in Gerrit via POST /changes REST API.
By adding a "merge" attribute which could be a SHA1, a branch name or a
tag name, etc. in the post JSON, gerrit will create a merge commit
instead of an empty commit. If there are conflicts, the response will
be rejected with a MergeConfictException which contains conflict
message.
Add dry run end point into GET
/projects/{project}/branches/{branch}/mergeable with query parameters
source (required) and strategy (optional) and return a
MergeableInfo entity.
Change-Id: I8f45f324704b3ff3eb20cb57c6e3bd75f2bf60ef
This commit is contained in:
@@ -3057,7 +3057,8 @@ As response a link:#mergeable-info[MergeableInfo] entity is returned.
|
|||||||
)]}'
|
)]}'
|
||||||
{
|
{
|
||||||
submit_type: "MERGE_IF_NECESSARY",
|
submit_type: "MERGE_IF_NECESSARY",
|
||||||
mergeable: true,
|
strategy: "recursive",
|
||||||
|
mergeable: true
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -4248,6 +4249,8 @@ A link:#change-id[\{change-id\}] that identifies the base change for a create
|
|||||||
change operation.
|
change operation.
|
||||||
|`new_branch` |optional, default to `false`|
|
|`new_branch` |optional, default to `false`|
|
||||||
Allow creating a new branch when set to `true`.
|
Allow creating a new branch when set to `true`.
|
||||||
|
|`merge` |optional|
|
||||||
|
The detail of a merge commit as a link:#merge-input[MergeInput] entity.
|
||||||
|==================================
|
|==================================
|
||||||
|
|
||||||
[[change-message-info]]
|
[[change-message-info]]
|
||||||
@@ -4733,12 +4736,33 @@ change.
|
|||||||
Submit type used for this change, can be `MERGE_IF_NECESSARY`,
|
Submit type used for this change, can be `MERGE_IF_NECESSARY`,
|
||||||
`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
|
`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
|
||||||
`CHERRY_PICK`.
|
`CHERRY_PICK`.
|
||||||
|
|`merge_strategy` |optional|
|
||||||
|
The strategy of the merge, can be `recursive`, `resolve`,
|
||||||
|
`simple-two-way-in-core`, `ours` or `theirs`.
|
||||||
|`mergeable` ||
|
|`mergeable` ||
|
||||||
`true` if this change is cleanly mergeable, `false` otherwise
|
`true` if this change is cleanly mergeable, `false` otherwise
|
||||||
|
|`conflicts`|optional|
|
||||||
|
A list of paths with conflicts
|
||||||
|`mergeable_into`|optional|
|
|`mergeable_into`|optional|
|
||||||
A list of other branch names where this change could merge cleanly
|
A list of other branch names where this change could merge cleanly
|
||||||
|============================
|
|============================
|
||||||
|
|
||||||
|
[[merge-input]]
|
||||||
|
=== MergeInput
|
||||||
|
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,
|
||||||
|
a complete reference name, a short reference name under refs/heads, refs/tags,
|
||||||
|
or refs/remotes namespace, etc.
|
||||||
|
|`strategy` |optional|
|
||||||
|
The strategy of the merge, can be `recursive`, `resolve`,
|
||||||
|
`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
|
||||||
|
|============================
|
||||||
|
|
||||||
[[move-input]]
|
[[move-input]]
|
||||||
=== MoveInput
|
=== MoveInput
|
||||||
The `MoveInput` entity contains information for moving a change to a new branch.
|
The `MoveInput` entity contains information for moving a change to a new branch.
|
||||||
|
|||||||
@@ -1354,6 +1354,42 @@ The content is returned as base64 encoded string.
|
|||||||
Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
|
Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
[[get-mergeable-info]]
|
||||||
|
=== Get Mergeable Information
|
||||||
|
--
|
||||||
|
'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]/mergeable'
|
||||||
|
--
|
||||||
|
|
||||||
|
Gets whether the source is mergeable with the target branch.
|
||||||
|
|
||||||
|
The `source` query parameter is required, which can be anything that could be resolved
|
||||||
|
to a commit, see examples in link:rest-api-changes.html#merge-input[MergeInput].
|
||||||
|
|
||||||
|
Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`,
|
||||||
|
`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
|
||||||
|
|
||||||
|
.Request
|
||||||
|
----
|
||||||
|
GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0
|
||||||
|
----
|
||||||
|
|
||||||
|
As response a link:#mergeable-info[MergeableInfo] entity is returned.
|
||||||
|
|
||||||
|
.Response
|
||||||
|
----
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Disposition: attachment
|
||||||
|
Content-Type: application/json; charset=UTF-8
|
||||||
|
|
||||||
|
)]}'
|
||||||
|
{
|
||||||
|
submit_type: "MERGE_IF_NECESSARY",
|
||||||
|
merge_strategy: "recursive",
|
||||||
|
mergeable: true
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
[[get-reflog]]
|
[[get-reflog]]
|
||||||
=== Get Reflog
|
=== Get Reflog
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ public class PushOneCommit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setParent(RevCommit parent) throws Exception {
|
||||||
|
commitBuilder.noParents();
|
||||||
|
commitBuilder.parent(parent);
|
||||||
|
}
|
||||||
|
|
||||||
public Result to(String ref) throws Exception {
|
public Result to(String ref) throws Exception {
|
||||||
commitBuilder.add(fileName, content);
|
commitBuilder.add(fileName, content);
|
||||||
return execute(ref);
|
return execute(ref);
|
||||||
|
|||||||
@@ -22,14 +22,19 @@ import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
|
|||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||||
|
import com.google.gerrit.acceptance.PushOneCommit;
|
||||||
|
import com.google.gerrit.acceptance.PushOneCommit.Result;
|
||||||
import com.google.gerrit.acceptance.RestResponse;
|
import com.google.gerrit.acceptance.RestResponse;
|
||||||
import com.google.gerrit.extensions.client.ChangeStatus;
|
import com.google.gerrit.extensions.client.ChangeStatus;
|
||||||
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
||||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||||
import com.google.gerrit.extensions.common.ChangeInput;
|
import com.google.gerrit.extensions.common.ChangeInput;
|
||||||
|
import com.google.gerrit.extensions.common.MergeInput;
|
||||||
|
import com.google.gerrit.extensions.common.MergeableInfo;
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
||||||
import com.google.gerrit.extensions.restapi.RestApiException;
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||||
|
import com.google.gerrit.reviewdb.client.Branch;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
|
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
|
||||||
import com.google.gerrit.testutil.ConfigSuite;
|
import com.google.gerrit.testutil.ConfigSuite;
|
||||||
@@ -191,4 +196,81 @@ public class CreateChangeIT extends AbstractDaemonTest {
|
|||||||
|
|
||||||
assertThat(o.signedOffBy).isTrue();
|
assertThat(o.signedOffBy).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createMergeChange() throws Exception {
|
||||||
|
changeInTwoBranches("a.txt", "b.txt");
|
||||||
|
ChangeInput in =
|
||||||
|
newMergeChangeInput("master", "branchA", ChangeStatus.NEW);
|
||||||
|
assertCreateSucceeds(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createMergeChange_Conflicts() throws Exception {
|
||||||
|
changeInTwoBranches("shared.txt", "shared.txt");
|
||||||
|
ChangeInput in =
|
||||||
|
newMergeChangeInput("master", "branchA", ChangeStatus.NEW);
|
||||||
|
assertCreateFails(in, RestApiException.class, "merge conflict");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dryRunMerge() throws Exception {
|
||||||
|
changeInTwoBranches("a.txt", "b.txt");
|
||||||
|
RestResponse r = adminRestSession.get("/projects/" + project.get()
|
||||||
|
+ "/branches/master/mergeable?source=branchA");
|
||||||
|
MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class);
|
||||||
|
assertThat(m.mergeable).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dryRunMerge_Conflicts() throws Exception {
|
||||||
|
changeInTwoBranches("a.txt", "a.txt");
|
||||||
|
RestResponse r = adminRestSession.get("/projects/" + project.get()
|
||||||
|
+ "/branches/master/mergeable?source=branchA");
|
||||||
|
MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class);
|
||||||
|
assertThat(m.mergeable).isFalse();
|
||||||
|
assertThat(m.conflicts).containsExactly("a.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChangeInput newMergeChangeInput(String targetBranch,
|
||||||
|
String sourceRef, ChangeStatus status) {
|
||||||
|
// create a merge change from branchA to master in gerrit
|
||||||
|
ChangeInput in = new ChangeInput();
|
||||||
|
in.project = project.get();
|
||||||
|
in.branch = targetBranch;
|
||||||
|
in.subject = "merge " + sourceRef + " to " + targetBranch;
|
||||||
|
in.status = status;
|
||||||
|
MergeInput mergeInput = new MergeInput();
|
||||||
|
mergeInput.source = sourceRef;
|
||||||
|
in.merge = mergeInput;
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeInTwoBranches(String fileInMaster, String fileInBranchA)
|
||||||
|
throws Exception {
|
||||||
|
// create a initial commit in master
|
||||||
|
Result initialCommit = pushFactory
|
||||||
|
.create(db, user.getIdent(), testRepo, "initial commit", "readme.txt",
|
||||||
|
"initial commit")
|
||||||
|
.to("refs/heads/master");
|
||||||
|
initialCommit.assertOkStatus();
|
||||||
|
|
||||||
|
// create a new branch branchA
|
||||||
|
createBranch(new Branch.NameKey(project, "branchA"));
|
||||||
|
|
||||||
|
// create another commit in master
|
||||||
|
Result changeToMaster = pushFactory
|
||||||
|
.create(db, user.getIdent(), testRepo, "change to master", fileInMaster,
|
||||||
|
"master content")
|
||||||
|
.to("refs/heads/master");
|
||||||
|
changeToMaster.assertOkStatus();
|
||||||
|
|
||||||
|
// create a commit in branchA
|
||||||
|
PushOneCommit commit = pushFactory
|
||||||
|
.create(db, user.getIdent(), testRepo, "change to branchA",
|
||||||
|
fileInBranchA, "branchA content");
|
||||||
|
commit.setParent(initialCommit.getCommit());
|
||||||
|
commit.to("refs/heads/branchA");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ public class ChangeInput {
|
|||||||
public ChangeStatus status;
|
public ChangeStatus status;
|
||||||
public String baseChange;
|
public String baseChange;
|
||||||
public Boolean newBranch;
|
public Boolean newBranch;
|
||||||
|
public MergeInput merge;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (C) 2016 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.extensions.common;
|
||||||
|
|
||||||
|
public class MergeInput {
|
||||||
|
/**
|
||||||
|
* {@code source} can be any Git object reference expression.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html">gitrevisions(7)</a>
|
||||||
|
*/
|
||||||
|
public String source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code strategy} name of the merge strategy.
|
||||||
|
*
|
||||||
|
* @see org.eclipse.jgit.merge.MergeStrategy
|
||||||
|
*/
|
||||||
|
public String strategy;
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ import java.util.List;
|
|||||||
|
|
||||||
public class MergeableInfo {
|
public class MergeableInfo {
|
||||||
public SubmitType submitType;
|
public SubmitType submitType;
|
||||||
|
public String strategy;
|
||||||
public boolean mergeable;
|
public boolean mergeable;
|
||||||
|
public List<String> conflicts;
|
||||||
public List<String> mergeableInto;
|
public List<String> mergeableInto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package com.google.gerrit.server.change;
|
|||||||
|
|
||||||
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
|
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.gerrit.common.TimeUtil;
|
import com.google.gerrit.common.TimeUtil;
|
||||||
@@ -24,6 +25,7 @@ import com.google.gerrit.extensions.client.ChangeStatus;
|
|||||||
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
||||||
import com.google.gerrit.extensions.common.ChangeInfo;
|
import com.google.gerrit.extensions.common.ChangeInfo;
|
||||||
import com.google.gerrit.extensions.common.ChangeInput;
|
import com.google.gerrit.extensions.common.ChangeInput;
|
||||||
|
import com.google.gerrit.extensions.common.MergeInput;
|
||||||
import com.google.gerrit.extensions.restapi.AuthException;
|
import com.google.gerrit.extensions.restapi.AuthException;
|
||||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
|
||||||
@@ -50,10 +52,12 @@ import com.google.gerrit.server.config.AnonymousCowardName;
|
|||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.git.BatchUpdate;
|
import com.google.gerrit.server.git.BatchUpdate;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.git.MergeUtil;
|
||||||
import com.google.gerrit.server.git.UpdateException;
|
import com.google.gerrit.server.git.UpdateException;
|
||||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
import com.google.gerrit.server.project.InvalidChangeOperationException;
|
import com.google.gerrit.server.project.InvalidChangeOperationException;
|
||||||
|
import com.google.gerrit.server.project.ProjectControl;
|
||||||
import com.google.gerrit.server.project.ProjectResource;
|
import com.google.gerrit.server.project.ProjectResource;
|
||||||
import com.google.gerrit.server.project.ProjectsCollection;
|
import com.google.gerrit.server.project.ProjectsCollection;
|
||||||
import com.google.gerrit.server.project.RefControl;
|
import com.google.gerrit.server.project.RefControl;
|
||||||
@@ -99,6 +103,7 @@ public class CreateChange implements
|
|||||||
private final BatchUpdate.Factory updateFactory;
|
private final BatchUpdate.Factory updateFactory;
|
||||||
private final PatchSetUtil psUtil;
|
private final PatchSetUtil psUtil;
|
||||||
private final boolean allowDrafts;
|
private final boolean allowDrafts;
|
||||||
|
private final MergeUtil.Factory mergeUtilFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateChange(@AnonymousCowardName String anonymousCowardName,
|
CreateChange(@AnonymousCowardName String anonymousCowardName,
|
||||||
@@ -114,7 +119,8 @@ public class CreateChange implements
|
|||||||
ChangeFinder changeFinder,
|
ChangeFinder changeFinder,
|
||||||
BatchUpdate.Factory updateFactory,
|
BatchUpdate.Factory updateFactory,
|
||||||
PatchSetUtil psUtil,
|
PatchSetUtil psUtil,
|
||||||
@GerritServerConfig Config config) {
|
@GerritServerConfig Config config,
|
||||||
|
MergeUtil.Factory mergeUtilFactory) {
|
||||||
this.anonymousCowardName = anonymousCowardName;
|
this.anonymousCowardName = anonymousCowardName;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.gitManager = gitManager;
|
this.gitManager = gitManager;
|
||||||
@@ -129,6 +135,7 @@ public class CreateChange implements
|
|||||||
this.updateFactory = updateFactory;
|
this.updateFactory = updateFactory;
|
||||||
this.psUtil = psUtil;
|
this.psUtil = psUtil;
|
||||||
this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
|
this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
|
||||||
|
this.mergeUtilFactory = mergeUtilFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -173,7 +180,8 @@ public class CreateChange implements
|
|||||||
|
|
||||||
Project.NameKey project = rsrc.getNameKey();
|
Project.NameKey project = rsrc.getNameKey();
|
||||||
try (Repository git = gitManager.openRepository(project);
|
try (Repository git = gitManager.openRepository(project);
|
||||||
RevWalk rw = new RevWalk(git)) {
|
ObjectInserter oi = git.newObjectInserter();
|
||||||
|
RevWalk rw = new RevWalk(oi.newReader())) {
|
||||||
ObjectId parentCommit;
|
ObjectId parentCommit;
|
||||||
List<String> groups;
|
List<String> groups;
|
||||||
if (input.baseChange != null) {
|
if (input.baseChange != null) {
|
||||||
@@ -219,7 +227,6 @@ public class CreateChange implements
|
|||||||
GeneralPreferencesInfo info =
|
GeneralPreferencesInfo info =
|
||||||
account.getAccount().getGeneralPreferencesInfo();
|
account.getAccount().getGeneralPreferencesInfo();
|
||||||
|
|
||||||
try (ObjectInserter oi = git.newObjectInserter()) {
|
|
||||||
ObjectId treeId =
|
ObjectId treeId =
|
||||||
mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
|
mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
|
||||||
ObjectId id = ChangeIdUtil.computeChangeId(treeId,
|
ObjectId id = ChangeIdUtil.computeChangeId(treeId,
|
||||||
@@ -231,7 +238,15 @@ public class CreateChange implements
|
|||||||
account.getAccount().getNameEmail(anonymousCowardName));
|
account.getAccount().getNameEmail(anonymousCowardName));
|
||||||
}
|
}
|
||||||
|
|
||||||
RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage);
|
RevCommit c;
|
||||||
|
if (input.merge != null) {
|
||||||
|
// create a merge commit
|
||||||
|
c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge,
|
||||||
|
author, commitMessage);
|
||||||
|
} else {
|
||||||
|
// create an empty commit
|
||||||
|
c = newCommit(oi, rw, author, mergeTip, commitMessage);
|
||||||
|
}
|
||||||
|
|
||||||
Change.Id changeId = new Change.Id(seq.nextChangeId());
|
Change.Id changeId = new Change.Id(seq.nextChangeId());
|
||||||
ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
|
ChangeInserter ins = changeInserterFactory.create(changeId, c, refName)
|
||||||
@@ -243,7 +258,7 @@ public class CreateChange implements
|
|||||||
topic = Strings.emptyToNull(topic.trim());
|
topic = Strings.emptyToNull(topic.trim());
|
||||||
}
|
}
|
||||||
ins.setTopic(topic);
|
ins.setTopic(topic);
|
||||||
ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
|
ins.setDraft(input.status == ChangeStatus.DRAFT);
|
||||||
ins.setGroups(groups);
|
ins.setGroups(groups);
|
||||||
try (BatchUpdate bu = updateFactory.create(
|
try (BatchUpdate bu = updateFactory.create(
|
||||||
db.get(), project, me, now)) {
|
db.get(), project, me, now)) {
|
||||||
@@ -254,8 +269,6 @@ public class CreateChange implements
|
|||||||
ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
|
ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS);
|
||||||
return Response.created(json.format(ins.getChange()));
|
return Response.created(json.format(ins.getChange()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RevCommit newCommit(ObjectInserter oi, RevWalk rw,
|
private static RevCommit newCommit(ObjectInserter oi, RevWalk rw,
|
||||||
@@ -274,6 +287,32 @@ public class CreateChange implements
|
|||||||
return rw.parseCommit(insert(oi, commit));
|
return rw.parseCommit(insert(oi, commit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RevCommit newMergeCommit(Repository repo, ObjectInserter oi,
|
||||||
|
RevWalk rw, ProjectControl projectControl, RevCommit mergeTip,
|
||||||
|
MergeInput merge, PersonIdent authorIdent, String commitMessage)
|
||||||
|
throws RestApiException, IOException {
|
||||||
|
if (Strings.isNullOrEmpty(merge.source)) {
|
||||||
|
throw new BadRequestException("merge.source must be non-empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
|
||||||
|
if (!projectControl.canReadCommit(db.get(), repo, sourceCommit)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"do not have read permission for: " + merge.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeUtil mergeUtil =
|
||||||
|
mergeUtilFactory.create(projectControl.getProjectState());
|
||||||
|
// default merge strategy from project settings
|
||||||
|
String mergeStrategy = MoreObjects.firstNonNull(
|
||||||
|
Strings.emptyToNull(merge.strategy),
|
||||||
|
mergeUtil.mergeStrategyName());
|
||||||
|
|
||||||
|
return rw.parseCommit(MergeUtil
|
||||||
|
.createMergeCommit(repo, oi, mergeTip, sourceCommit, mergeStrategy,
|
||||||
|
authorIdent, commitMessage, rw));
|
||||||
|
}
|
||||||
|
|
||||||
private static ObjectId insert(ObjectInserter inserter,
|
private static ObjectId insert(ObjectInserter inserter,
|
||||||
CommitBuilder commit) throws IOException,
|
CommitBuilder commit) throws IOException,
|
||||||
UnsupportedEncodingException {
|
UnsupportedEncodingException {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import org.eclipse.jgit.lib.Constants;
|
|||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.merge.MergeStrategy;
|
||||||
import org.kohsuke.args4j.Option;
|
import org.kohsuke.args4j.Option;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -110,6 +111,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
|
|||||||
ProjectState projectState = projectCache.get(change.getProject());
|
ProjectState projectState = projectCache.get(change.getProject());
|
||||||
String strategy = mergeUtilFactory.create(projectState)
|
String strategy = mergeUtilFactory.create(projectState)
|
||||||
.mergeStrategyName();
|
.mergeStrategyName();
|
||||||
|
result.strategy = strategy;
|
||||||
result.mergeable =
|
result.mergeable =
|
||||||
isMergable(git, change, commit, ref, result.submitType, strategy);
|
isMergable(git, change, commit, ref, result.submitType, strategy);
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,16 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.git;
|
package com.google.gerrit.server.git;
|
||||||
|
|
||||||
/** Indicates that the commit is already contained in destination banch. */
|
import com.google.gerrit.extensions.restapi.RestApiException;
|
||||||
public class MergeIdenticalTreeException extends Exception {
|
|
||||||
|
/**
|
||||||
|
* Indicates that the commit is already contained in destination branch.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class MergeIdenticalTreeException extends RestApiException {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** @param msg message to return to the client describing the error. */
|
||||||
public MergeIdenticalTreeException(String msg) {
|
public MergeIdenticalTreeException(String msg) {
|
||||||
super(msg, null);
|
super(msg, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.gerrit.common.FooterConstants;
|
import com.google.gerrit.common.FooterConstants;
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.common.data.LabelType;
|
import com.google.gerrit.common.data.LabelType;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
import com.google.gerrit.extensions.restapi.MergeConflictException;
|
import com.google.gerrit.extensions.restapi.MergeConflictException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.Branch;
|
import com.google.gerrit.reviewdb.client.Branch;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
@@ -45,11 +48,13 @@ import com.google.inject.Provider;
|
|||||||
import com.google.inject.assistedinject.Assisted;
|
import com.google.inject.assistedinject.Assisted;
|
||||||
import com.google.inject.assistedinject.AssistedInject;
|
import com.google.inject.assistedinject.AssistedInject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.AmbiguousObjectException;
|
||||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||||
import org.eclipse.jgit.errors.LargeObjectException;
|
import org.eclipse.jgit.errors.LargeObjectException;
|
||||||
import org.eclipse.jgit.errors.MissingObjectException;
|
import org.eclipse.jgit.errors.MissingObjectException;
|
||||||
import org.eclipse.jgit.errors.NoMergeBaseException;
|
import org.eclipse.jgit.errors.NoMergeBaseException;
|
||||||
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
|
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
|
||||||
|
import org.eclipse.jgit.errors.RevisionSyntaxException;
|
||||||
import org.eclipse.jgit.lib.AnyObjectId;
|
import org.eclipse.jgit.lib.AnyObjectId;
|
||||||
import org.eclipse.jgit.lib.CommitBuilder;
|
import org.eclipse.jgit.lib.CommitBuilder;
|
||||||
import org.eclipse.jgit.lib.Config;
|
import org.eclipse.jgit.lib.Config;
|
||||||
@@ -60,6 +65,7 @@ import org.eclipse.jgit.lib.PersonIdent;
|
|||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
import org.eclipse.jgit.merge.MergeStrategy;
|
import org.eclipse.jgit.merge.MergeStrategy;
|
||||||
import org.eclipse.jgit.merge.Merger;
|
import org.eclipse.jgit.merge.Merger;
|
||||||
|
import org.eclipse.jgit.merge.ResolveMerger;
|
||||||
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
|
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
|
||||||
import org.eclipse.jgit.merge.ThreeWayMerger;
|
import org.eclipse.jgit.merge.ThreeWayMerger;
|
||||||
import org.eclipse.jgit.revwalk.FooterKey;
|
import org.eclipse.jgit.revwalk.FooterKey;
|
||||||
@@ -202,6 +208,44 @@ public class MergeUtil {
|
|||||||
throw new MergeConflictException("merge conflict");
|
throw new MergeConflictException("merge conflict");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ObjectId createMergeCommit(Repository repo, ObjectInserter inserter,
|
||||||
|
RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy,
|
||||||
|
PersonIdent committerIndent, String commitMsg, RevWalk rw)
|
||||||
|
throws IOException, MergeIdenticalTreeException, MergeConflictException {
|
||||||
|
|
||||||
|
if (rw.isMergedInto(originalCommit, mergeTip)) {
|
||||||
|
throw new MergeIdenticalTreeException(
|
||||||
|
"merge identical tree: change(s) has been already merged!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Merger m = newMerger(repo, inserter, 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 inserter.insert(mergeCommit);
|
||||||
|
} else {
|
||||||
|
List<String> conflicts = ImmutableList.of();
|
||||||
|
if (m instanceof ResolveMerger) {
|
||||||
|
conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
||||||
|
}
|
||||||
|
throw new MergeConflictException(createConflictMessage(conflicts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String createConflictMessage(List<String> conflicts) {
|
||||||
|
StringBuilder sb = new StringBuilder("merge conflict(s)");
|
||||||
|
for (String c : conflicts) {
|
||||||
|
sb.append('\n' + c);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
|
public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
|
||||||
PatchSet.Id psId) {
|
PatchSet.Id psId) {
|
||||||
Change c = ctl.getChange();
|
Change c = ctl.getChange();
|
||||||
@@ -591,11 +635,17 @@ public class MergeUtil {
|
|||||||
|
|
||||||
public static ThreeWayMerger newThreeWayMerger(Repository repo,
|
public static ThreeWayMerger newThreeWayMerger(Repository repo,
|
||||||
final ObjectInserter inserter, String strategyName) {
|
final ObjectInserter inserter, String strategyName) {
|
||||||
|
Merger m = newMerger(repo, inserter, strategyName);
|
||||||
|
checkArgument(m instanceof ThreeWayMerger,
|
||||||
|
"merge strategy %s does not support three-way merging", strategyName);
|
||||||
|
return (ThreeWayMerger) m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Merger newMerger(Repository repo,
|
||||||
|
final ObjectInserter inserter, String strategyName) {
|
||||||
MergeStrategy strategy = MergeStrategy.get(strategyName);
|
MergeStrategy strategy = MergeStrategy.get(strategyName);
|
||||||
checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
|
checkArgument(strategy != null, "invalid merge strategy: %s", strategyName);
|
||||||
Merger m = strategy.newMerger(repo, true);
|
Merger m = strategy.newMerger(repo, true);
|
||||||
checkArgument(m instanceof ThreeWayMerger,
|
|
||||||
"merge strategy %s does not support three-way merging", strategyName);
|
|
||||||
m.setObjectInserter(new ObjectInserter.Filter() {
|
m.setObjectInserter(new ObjectInserter.Filter() {
|
||||||
@Override
|
@Override
|
||||||
protected ObjectInserter delegate() {
|
protected ObjectInserter delegate() {
|
||||||
@@ -610,7 +660,7 @@ public class MergeUtil {
|
|||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return (ThreeWayMerger) m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markCleanMerges(final RevWalk rw,
|
public void markCleanMerges(final RevWalk rw,
|
||||||
@@ -693,4 +743,16 @@ public class MergeUtil {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
|
||||||
|
throws BadRequestException, ResourceNotFoundException, IOException {
|
||||||
|
try {
|
||||||
|
return rw.parseCommit(repo.resolve(str));
|
||||||
|
} catch (AmbiguousObjectException | IncorrectObjectTypeException |
|
||||||
|
RevisionSyntaxException e) {
|
||||||
|
throw new BadRequestException(e.getMessage());
|
||||||
|
} catch (MissingObjectException e) {
|
||||||
|
throw new ResourceNotFoundException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// Copyright (C) 2016 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.server.project;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.common.MergeableInfo;
|
||||||
|
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
|
||||||
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.git.InMemoryInserter;
|
||||||
|
import com.google.gerrit.server.git.MergeUtil;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Provider;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
import org.eclipse.jgit.lib.ObjectInserter;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.merge.Merger;
|
||||||
|
import org.eclipse.jgit.merge.ResolveMerger;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the mergeability at current branch for a git object references expression.
|
||||||
|
*/
|
||||||
|
public class CheckMergeability implements RestReadView<BranchResource> {
|
||||||
|
|
||||||
|
private String source;
|
||||||
|
private String strategy;
|
||||||
|
private final Provider<ReviewDb> db;
|
||||||
|
|
||||||
|
@Option(name = "--source", metaVar = "COMMIT",
|
||||||
|
usage = "the source reference to merge, which could be any git object "
|
||||||
|
+ "references expression, refer to "
|
||||||
|
+ "org.eclipse.jgit.lib.Repository#resolve(String)",
|
||||||
|
required = true)
|
||||||
|
public void setSource(String source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Option(name = "--strategy", metaVar = "STRATEGY",
|
||||||
|
usage = "name of the merge strategy, refer to "
|
||||||
|
+ "org.eclipse.jgit.merge.MergeStrategy")
|
||||||
|
public void setStrategy(String strategy) {
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final GitRepositoryManager gitManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CheckMergeability(GitRepositoryManager gitManager,
|
||||||
|
@GerritServerConfig Config cfg,
|
||||||
|
Provider<ReviewDb> db) {
|
||||||
|
this.gitManager = gitManager;
|
||||||
|
this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergeableInfo apply(BranchResource resource)
|
||||||
|
throws IOException, BadRequestException, ResourceNotFoundException {
|
||||||
|
MergeableInfo result = new MergeableInfo();
|
||||||
|
result.strategy = strategy;
|
||||||
|
try (Repository git = gitManager.openRepository(resource.getNameKey());
|
||||||
|
RevWalk rw = new RevWalk(git);
|
||||||
|
ObjectInserter inserter = new InMemoryInserter(git)) {
|
||||||
|
Merger m = MergeUtil.newMerger(git, inserter, strategy);
|
||||||
|
|
||||||
|
Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
|
||||||
|
if (destRef == null) {
|
||||||
|
throw new ResourceNotFoundException(resource.getRef());
|
||||||
|
}
|
||||||
|
|
||||||
|
RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());
|
||||||
|
RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, source);
|
||||||
|
|
||||||
|
if (!resource.getControl().canReadCommit(db.get(), git, sourceCommit)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"do not have read permission for: " + source);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.mergeable = m.merge(targetCommit, sourceCommit);
|
||||||
|
if (m instanceof ResolveMerger) {
|
||||||
|
result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ public class Module extends RestApiModule {
|
|||||||
delete(BRANCH_KIND).to(DeleteBranch.class);
|
delete(BRANCH_KIND).to(DeleteBranch.class);
|
||||||
post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
|
post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
|
||||||
factory(CreateBranch.Factory.class);
|
factory(CreateBranch.Factory.class);
|
||||||
|
get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
|
||||||
get(BRANCH_KIND, "reflog").to(GetReflog.class);
|
get(BRANCH_KIND, "reflog").to(GetReflog.class);
|
||||||
child(BRANCH_KIND, "files").to(FilesCollection.class);
|
child(BRANCH_KIND, "files").to(FilesCollection.class);
|
||||||
get(FILE_KIND, "content").to(GetContent.class);
|
get(FILE_KIND, "content").to(GetContent.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user