Add more information to MergeableInfo to indicate a merged commit

Add two fields contentMerged and commitMerged into MergeableInfo.
contentMerged is true if the content of the commit is merged,
commitMerged is true if the commit itself is merged.

Also add more tests for CheckMergeability class.

Change-Id: Ia756644411f217b8d8bae06f44f14a24597544bd
This commit is contained in:
Zhen Chen
2016-07-26 16:54:59 -07:00
parent 93a4c8e42d
commit 8f00d55d11
10 changed files with 522 additions and 75 deletions

View File

@@ -4915,6 +4915,10 @@ The strategy of the merge, can be `recursive`, `resolve`,
`simple-two-way-in-core`, `ours` or `theirs`. `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
|`commit_merged` |optional|
`true` if this change is already merged, `false` otherwise
|`content_merged` |optional|
`true` if the content of this change is already merged, `false` otherwise
|`conflicts`|optional| |`conflicts`|optional|
A list of paths with conflicts A list of paths with conflicts
|`mergeable_into`|optional| |`mergeable_into`|optional|

View File

@@ -1363,8 +1363,9 @@ The content is returned as base64 encoded string.
Gets whether the source is mergeable with the target branch. Gets whether the source is mergeable with the target branch.
The `source` query parameter is required, which can be anything that could be resolved The `source` query parameter is required, which can be anything that could be
to a commit, see examples in link:rest-api-changes.html#merge-input[MergeInput]. resolved to a commit, see examples of the `source` attribute in
link:rest-api-changes.html#merge-input[MergeInput].
Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`, Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`,
`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings. `simple-two-way-in-core`, `ours` or `theirs`, default will use project settings.
@@ -1374,7 +1375,7 @@ Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`
GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0 GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0
---- ----
As response a link:#mergeable-info[MergeableInfo] entity is returned. As response a link:rest-api-changes.html#mergeable-info[MergeableInfo] entity is returned.
.Response .Response
---- ----
@@ -1384,9 +1385,67 @@ As response a link:#mergeable-info[MergeableInfo] entity is returned.
)]}' )]}'
{ {
submit_type: "MERGE_IF_NECESSARY", "submit_type": "MERGE_IF_NECESSARY",
strategy: "recursive", "strategy": "recursive",
mergeable: true "mergeable": true,
"commit_merged": false,
"content_merged": false
}
----
or when there were conflicts.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"submit_type": "MERGE_IF_NECESSARY",
"strategy": "recursive",
"mergeable": false,
"conflicts": [
"common.txt",
"shared.txt"
]
}
----
or when the 'testbranch' has been already merged.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"submit_type": "MERGE_IF_NECESSARY",
"strategy": "recursive",
"mergeable": true,
"commit_merged": true,
"content_merged": true
}
----
or when only the content of 'testbranch' has been merged.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"submit_type": "MERGE_IF_NECESSARY",
"strategy": "recursive",
"mergeable": true,
"commit_merged": false,
"content_merged": true
} }
---- ----

View File

@@ -20,31 +20,37 @@ import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
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.Strings;
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;
import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
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.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.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.server.git.ChangeAlreadyMergedException;
import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.TestTimeUtil; import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@@ -142,6 +148,99 @@ public class CreateChangeIT extends AbstractDaemonTest {
} }
} }
@Test
public void createMergeChange() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in =
newMergeChangeInput("branchA", "branchB", "");
assertCreateSucceeds(in);
}
@Test
public void createMergeChange_Conflicts() throws Exception {
changeInTwoBranches("branchA", "shared.txt", "branchB", "shared.txt");
ChangeInput in =
newMergeChangeInput("branchA", "branchB", "");
assertCreateFails(in, RestApiException.class, "merge conflict");
}
@Test
public void createMergeChange_Conflicts_Ours() throws Exception {
changeInTwoBranches("branchA", "shared.txt", "branchB", "shared.txt");
ChangeInput in =
newMergeChangeInput("branchA", "branchB", "ours");
assertCreateSucceeds(in);
}
@Test
public void invalidSource() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in =
newMergeChangeInput("branchA", "invalid", "");
assertCreateFails(in, BadRequestException.class,
"Cannot resolve 'invalid' to a commit");
}
@Test
public void invalidStrategy() throws Exception {
changeInTwoBranches("branchA", "a.txt", "branchB", "b.txt");
ChangeInput in =
newMergeChangeInput("branchA", "branchB", "octopus");
assertCreateFails(in, BadRequestException.class,
"invalid merge strategy: octopus");
}
@Test
public void alreadyMerged() throws Exception {
ObjectId c0 = testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.branch("HEAD").commit().insertChangeId()
.message("second commit")
.add("b.txt", "b contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
ChangeInput in =
newMergeChangeInput("master", c0.getName(), "");
assertCreateFails(in, ChangeAlreadyMergedException.class,
"'" + c0.getName() + "' has already been merged");
}
@Test
public void onlyContentMerged() throws Exception {
testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
// create a change, and cherrypick into master
PushOneCommit.Result cId = createChange();
RevCommit commitId = cId.getCommit();
CherryPickInput cpi = new CherryPickInput();
cpi.destination = "master";
cpi.message = "cherry pick the commit";
ChangeApi orig = gApi.changes()
.id(cId.getChangeId());
ChangeApi cherry = orig.current().cherryPick(cpi);
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
ObjectId remoteId = getRemoteHead();
assertThat(remoteId).isNotEqualTo(commitId);
ChangeInput in =
newMergeChangeInput("master", commitId.getName(), "");
assertCreateSucceeds(in);
}
private ChangeInput newChangeInput(ChangeStatus status) { private ChangeInput newChangeInput(ChangeStatus status) {
ChangeInput in = new ChangeInput(); ChangeInput in = new ChangeInput();
in.project = project.get(); in.project = project.get();
@@ -197,58 +296,25 @@ public class CreateChangeIT extends AbstractDaemonTest {
assertThat(o.signedOffBy).isTrue(); assertThat(o.signedOffBy).isTrue();
} }
private ChangeInput newMergeChangeInput(String targetBranch, String sourceRef,
@Test String strategy) {
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 // create a merge change from branchA to master in gerrit
ChangeInput in = new ChangeInput(); ChangeInput in = new ChangeInput();
in.project = project.get(); in.project = project.get();
in.branch = targetBranch; in.branch = targetBranch;
in.subject = "merge " + sourceRef + " to " + targetBranch; in.subject = "merge " + sourceRef + " to " + targetBranch;
in.status = status; in.status = ChangeStatus.NEW;
MergeInput mergeInput = new MergeInput(); MergeInput mergeInput = new MergeInput();
mergeInput.source = sourceRef; mergeInput.source = sourceRef;
in.merge = mergeInput; in.merge = mergeInput;
if (!Strings.isNullOrEmpty(strategy)) {
in.merge.strategy = strategy;
}
return in; return in;
} }
private void changeInTwoBranches(String fileInMaster, String fileInBranchA) private void changeInTwoBranches(String branchA, String fileA, String branchB,
throws Exception { String fileB) throws Exception {
// create a initial commit in master // create a initial commit in master
Result initialCommit = pushFactory Result initialCommit = pushFactory
.create(db, user.getIdent(), testRepo, "initial commit", "readme.txt", .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt",
@@ -256,21 +322,21 @@ public class CreateChangeIT extends AbstractDaemonTest {
.to("refs/heads/master"); .to("refs/heads/master");
initialCommit.assertOkStatus(); initialCommit.assertOkStatus();
// create a new branch branchA // create two new branches
createBranch(new Branch.NameKey(project, "branchA")); createBranch(new Branch.NameKey(project, branchA));
createBranch(new Branch.NameKey(project, branchB));
// 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 // create a commit in branchA
PushOneCommit commit = pushFactory Result changeA = pushFactory
.create(db, user.getIdent(), testRepo, "change to branchA", .create(db, user.getIdent(), testRepo, "change A", fileA, "A content")
fileInBranchA, "branchA content"); .to("refs/heads/" + branchA);
commit.setParent(initialCommit.getCommit()); changeA.assertOkStatus();
commit.to("refs/heads/branchA");
// create a commit in branchB
PushOneCommit commitB = pushFactory
.create(db, user.getIdent(), testRepo, "change B", fileB, "B content");
commitB.setParent(initialCommit.getCommit());
Result changeB = commitB.to("refs/heads/" + branchB);
changeB.assertOkStatus();
} }
} }

View File

@@ -0,0 +1,248 @@
// 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.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Strings;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.reviewdb.client.Branch;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Before;
import org.junit.Test;
public class CheckMergeabilityIT extends AbstractDaemonTest {
private Branch.NameKey branch;
@Before
public void setUp() throws Exception {
branch = new Branch.NameKey(project, "test");
gApi.projects()
.name(branch.getParentKey().get())
.branch(branch.get()).create(new BranchInput());
}
@Test
public void checkMergeableCommit() throws Exception {
RevCommit initialHead = getRemoteHead();
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.reset(initialHead);
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in b")
.add("b.txt", "b contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/test")).call();
assertMergeable("master", "test", "recursive");
}
@Test
public void checkUnMergeableCommit() throws Exception {
RevCommit initialHead = getRemoteHead();
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.reset(initialHead);
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a too")
.add("a.txt", "a contents too")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/test")).call();
assertUnMergeable("master", "test", "recursive", "a.txt");
}
@Test
public void checkOursMergeStrategy() throws Exception {
RevCommit initialHead = getRemoteHead();
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.reset(initialHead);
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a too")
.add("a.txt", "a contents too")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/test")).call();
assertMergeable("master", "test", "ours");
}
@Test
public void checkAlreadyMergedCommit() throws Exception {
ObjectId c0 = testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.branch("HEAD").commit().insertChangeId()
.message("second commit")
.add("b.txt", "b contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
assertCommitMerged("master", c0.getName(), "");
}
@Test
public void checkContentMergedCommit() throws Exception {
testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
// create a change, and cherrypick into master
PushOneCommit.Result cId = createChange();
RevCommit commitId = cId.getCommit();
CherryPickInput cpi = new CherryPickInput();
cpi.destination = "master";
cpi.message = "cherry pick the commit";
ChangeApi orig = gApi.changes()
.id(cId.getChangeId());
ChangeApi cherry = orig.current().cherryPick(cpi);
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
ObjectId remoteId = getRemoteHead();
assertThat(remoteId).isNotEqualTo(commitId);
assertContentMerged("master", commitId.getName(), "recursive");
}
@Test
public void checkInvalidSource() throws Exception {
testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
assertBadRequest("master", "fdsafsdf", "recursive",
"Cannot resolve 'fdsafsdf' to a commit");
}
@Test
public void checkInvalidStrategy() throws Exception {
RevCommit initialHead = getRemoteHead();
testRepo.branch("HEAD").commit().insertChangeId()
.message("first commit")
.add("a.txt", "a contents ")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/master")).call();
testRepo.reset(initialHead);
testRepo.branch("HEAD").commit().insertChangeId()
.message("some change in a too")
.add("a.txt", "a contents too")
.create();
testRepo.git().push().setRemote("origin").setRefSpecs(
new RefSpec("HEAD:refs/heads/test")).call();
assertBadRequest("master", "test", "octopus",
"invalid merge strategy: octopus");
}
private void assertMergeable(String targetBranch, String source,
String strategy) throws Exception {
MergeableInfo
mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
assertThat(mergeableInfo.mergeable).isTrue();
}
private void assertUnMergeable(String targetBranch, String source,
String strategy, String... conflicts) throws Exception {
MergeableInfo mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
assertThat(mergeableInfo.mergeable).isFalse();
assertThat(mergeableInfo.conflicts).containsExactly((Object[]) conflicts);
}
private void assertCommitMerged(String targetBranch, String source,
String strategy) throws Exception {
MergeableInfo
mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
assertThat(mergeableInfo.mergeable).isTrue();
assertThat(mergeableInfo.commitMerged).isTrue();
}
private void assertContentMerged(String targetBranch, String source,
String strategy) throws Exception {
MergeableInfo
mergeableInfo = getMergeableInfo(targetBranch, source, strategy);
assertThat(mergeableInfo.mergeable).isTrue();
assertThat(mergeableInfo.contentMerged).isTrue();
}
private void assertBadRequest(String targetBranch, String source,
String strategy, String errMsg) throws Exception {
String url = "/projects/" + project.get() + "/branches/" + targetBranch;
url += "/mergeable?source=" + source;
if (!Strings.isNullOrEmpty(strategy)) {
url += "&strategy=" + strategy;
}
RestResponse r = userRestSession.get(url);
r.assertBadRequest();
assertThat(r.getEntityContent()).isEqualTo(errMsg);
}
private MergeableInfo getMergeableInfo(String targetBranch, String source,
String strategy) throws Exception {
String url = "/projects/" + project.get() + "/branches/" + targetBranch;
url += "/mergeable?source=" + source;
if (!Strings.isNullOrEmpty(strategy)) {
url += "&strategy=" + strategy;
}
RestResponse r = userRestSession.get(url);
r.assertOK();
MergeableInfo result = newGson().fromJson(r.getReader(), MergeableInfo.class);
r.consume();
return result;
}
}

View File

@@ -22,6 +22,8 @@ public class MergeableInfo {
public SubmitType submitType; public SubmitType submitType;
public String strategy; public String strategy;
public boolean mergeable; public boolean mergeable;
public boolean commitMerged;
public boolean contentMerged;
public List<String> conflicts; public List<String> conflicts;
public List<String> mergeableInto; public List<String> mergeableInto;
} }

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.Capable; import com.google.gerrit.common.data.Capable;
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.client.SubmitType;
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.MergeInput;
@@ -104,6 +105,7 @@ public class CreateChange implements
private final PatchSetUtil psUtil; private final PatchSetUtil psUtil;
private final boolean allowDrafts; private final boolean allowDrafts;
private final MergeUtil.Factory mergeUtilFactory; private final MergeUtil.Factory mergeUtilFactory;
private final SubmitType submitType;
@Inject @Inject
CreateChange(@AnonymousCowardName String anonymousCowardName, CreateChange(@AnonymousCowardName String anonymousCowardName,
@@ -135,6 +137,8 @@ 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.submitType = config
.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
this.mergeUtilFactory = mergeUtilFactory; this.mergeUtilFactory = mergeUtilFactory;
} }
@@ -241,6 +245,11 @@ public class CreateChange implements
RevCommit c; RevCommit c;
if (input.merge != null) { if (input.merge != null) {
// create a merge commit // create a merge commit
if (!(submitType.equals(SubmitType.MERGE_ALWAYS) ||
submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
throw new BadRequestException(
"Submit type: " + submitType + " is not supported");
}
c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge, c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge,
author, commitMessage); author, commitMessage);
} else { } else {
@@ -268,6 +277,8 @@ 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()));
} catch (IllegalArgumentException e) {
throw new BadRequestException(e.getMessage());
} }
} }
@@ -308,9 +319,8 @@ public class CreateChange implements
Strings.emptyToNull(merge.strategy), Strings.emptyToNull(merge.strategy),
mergeUtil.mergeStrategyName()); mergeUtil.mergeStrategyName());
return rw.parseCommit(MergeUtil return MergeUtil.createMergeCommit(repo, oi, mergeTip, sourceCommit,
.createMergeCommit(repo, oi, mergeTip, sourceCommit, mergeStrategy, mergeStrategy, authorIdent, commitMessage, rw);
authorIdent, commitMessage, rw));
} }
private static ObjectId insert(ObjectInserter inserter, private static ObjectId insert(ObjectInserter inserter,

View File

@@ -0,0 +1,27 @@
// 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.git;
/**
* Indicates that the change or commit is already in the source tree.
*/
public class ChangeAlreadyMergedException extends MergeIdenticalTreeException {
private static final long serialVersionUID = 1L;
/** @param msg message to return to the client describing the error. */
public ChangeAlreadyMergedException(String msg) {
super(msg);
}
}

View File

@@ -18,13 +18,13 @@ import com.google.gerrit.extensions.restapi.RestApiException;
/** /**
* Indicates that the commit is already contained in destination branch. * Indicates that the commit is already contained in destination branch.
* * Either the commit itself is in the source tree, or the content is merged
*/ */
public class MergeIdenticalTreeException extends RestApiException { 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. */ /** @param msg message to return to the client describing the error. */
public MergeIdenticalTreeException(String msg) { public MergeIdenticalTreeException(String msg) {
super(msg, null); super(msg);
} }
} }

View File

@@ -208,14 +208,14 @@ public class MergeUtil {
throw new MergeConflictException("merge conflict"); throw new MergeConflictException("merge conflict");
} }
public static ObjectId createMergeCommit(Repository repo, ObjectInserter inserter, public static RevCommit createMergeCommit(Repository repo, ObjectInserter inserter,
RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy,
PersonIdent committerIndent, String commitMsg, RevWalk rw) PersonIdent committerIndent, String commitMsg, RevWalk rw)
throws IOException, MergeIdenticalTreeException, MergeConflictException { throws IOException, MergeIdenticalTreeException, MergeConflictException {
if (rw.isMergedInto(originalCommit, mergeTip)) { if (rw.isMergedInto(originalCommit, mergeTip)) {
throw new MergeIdenticalTreeException( throw new ChangeAlreadyMergedException(
"merge identical tree: change(s) has been already merged!"); "'" + originalCommit.getName() + "' has already been merged");
} }
Merger m = newMerger(repo, inserter, mergeStrategy); Merger m = newMerger(repo, inserter, mergeStrategy);
@@ -228,7 +228,7 @@ public class MergeUtil {
mergeCommit.setAuthor(committerIndent); mergeCommit.setAuthor(committerIndent);
mergeCommit.setCommitter(committerIndent); mergeCommit.setCommitter(committerIndent);
mergeCommit.setMessage(commitMsg); mergeCommit.setMessage(commitMsg);
return inserter.insert(mergeCommit); return rw.parseCommit(inserter.insert(mergeCommit));
} }
List<String> conflicts = ImmutableList.of(); List<String> conflicts = ImmutableList.of();
if (m instanceof ResolveMerger) { if (m instanceof ResolveMerger) {
@@ -746,7 +746,12 @@ public class MergeUtil {
public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str) public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str)
throws BadRequestException, ResourceNotFoundException, IOException { throws BadRequestException, ResourceNotFoundException, IOException {
try { try {
return rw.parseCommit(repo.resolve(str)); ObjectId commitId = repo.resolve(str);
if (commitId == null) {
throw new BadRequestException(
"Cannot resolve '" + str + "' to a commit");
}
return rw.parseCommit(commitId);
} catch (AmbiguousObjectException | IncorrectObjectTypeException | } catch (AmbiguousObjectException | IncorrectObjectTypeException |
RevisionSyntaxException e) { RevisionSyntaxException e) {
throw new BadRequestException(e.getMessage()); throw new BadRequestException(e.getMessage());

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.project; package com.google.gerrit.server.project;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.MergeableInfo; 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.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -45,6 +46,7 @@ public class CheckMergeability implements RestReadView<BranchResource> {
private String source; private String source;
private String strategy; private String strategy;
private SubmitType submitType;
private final Provider<ReviewDb> db; private final Provider<ReviewDb> db;
@Option(name = "--source", metaVar = "COMMIT", @Option(name = "--source", metaVar = "COMMIT",
@@ -71,13 +73,22 @@ public class CheckMergeability implements RestReadView<BranchResource> {
Provider<ReviewDb> db) { Provider<ReviewDb> db) {
this.gitManager = gitManager; this.gitManager = gitManager;
this.strategy = MergeUtil.getMergeStrategy(cfg).getName(); this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
this.submitType = cfg.getEnum("project", null, "submitType",
SubmitType.MERGE_IF_NECESSARY);
this.db = db; this.db = db;
} }
@Override @Override
public MergeableInfo apply(BranchResource resource) public MergeableInfo apply(BranchResource resource)
throws IOException, BadRequestException, ResourceNotFoundException { throws IOException, BadRequestException, ResourceNotFoundException {
if (!(submitType.equals(SubmitType.MERGE_ALWAYS) ||
submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
throw new BadRequestException(
"Submit type: " + submitType + " is not supported");
}
MergeableInfo result = new MergeableInfo(); MergeableInfo result = new MergeableInfo();
result.submitType = submitType;
result.strategy = strategy; result.strategy = strategy;
try (Repository git = gitManager.openRepository(resource.getNameKey()); try (Repository git = gitManager.openRepository(resource.getNameKey());
RevWalk rw = new RevWalk(git); RevWalk rw = new RevWalk(git);
@@ -97,11 +108,26 @@ public class CheckMergeability implements RestReadView<BranchResource> {
"do not have read permission for: " + source); "do not have read permission for: " + source);
} }
result.mergeable = m.merge(targetCommit, sourceCommit); if (rw.isMergedInto(sourceCommit, targetCommit)) {
result.mergeable = true;
result.commitMerged = true;
result.contentMerged = true;
return result;
}
if (m.merge(false, targetCommit, sourceCommit)) {
result.mergeable = true;
result.commitMerged = false;
result.contentMerged = m.getResultTreeId().equals(targetCommit.getTree());
} else {
result.mergeable = false;
if (m instanceof ResolveMerger) { if (m instanceof ResolveMerger) {
result.conflicts = ((ResolveMerger) m).getUnmergedPaths(); result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
} }
} }
} catch (IllegalArgumentException e) {
throw new BadRequestException(e.getMessage());
}
return result; return result;
} }
} }