Merge "Add new submit strategy "Rebase Always"."

This commit is contained in:
ekempin 2016-10-21 13:34:44 +00:00 committed by Gerrit Code Review
commit 42b7c5155b
22 changed files with 806 additions and 533 deletions

View File

@ -117,6 +117,13 @@ When Gerrit tries to do a merge, by default the merge will only
succeed if there is no path conflict. A path conflict occurs when
the same file has also been changed on the other side of the merge.
[[rebase_always]]
* Rebase Always
+
Basically, the same as Rebase If Necesary, but it creates a new patchset even if
fast forward is possible. In this regard, it's similar to Cherry Pick, but with
the important distinction that Rebase Always does not ignore dependencies.
[[content_merge]]
If `Allow content merges` is enabled, Gerrit will try
to do a content merge when a path conflict occurs.

View File

@ -20,6 +20,7 @@ import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
@ -123,6 +124,10 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
+ "gerrit:commit_message(M),"
+ "regex_matches('.*REBASE_IF_NECESSARY.*', M),"
+ "!.\n"
+ "submit_type(rebase_always) :-"
+ "gerrit:commit_message(M),"
+ "regex_matches('.*REBASE_ALWAYS.*', M),"
+ "!.\n"
+ "submit_type(merge_always) :-"
+ "gerrit:commit_message(M),"
+ "regex_matches('.*MERGE_ALWAYS.*', M),"
@ -157,8 +162,9 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4");
PushOneCommit.Result r5 = createChange("master", "MERGE_ALWAYS 5");
PushOneCommit.Result r6 = createChange("master", "CHERRY_PICK 6");
PushOneCommit.Result r5 = createChange("master", "REBASE_ALWAYS 5");
PushOneCommit.Result r6 = createChange("master", "MERGE_ALWAYS 6");
PushOneCommit.Result r7 = createChange("master", "CHERRY_PICK 7");
assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
@ -166,6 +172,7 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r7.getChangeId());
setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
@ -173,8 +180,9 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId());
assertSubmitType(MERGE_ALWAYS, r5.getChangeId());
assertSubmitType(CHERRY_PICK, r6.getChangeId());
assertSubmitType(REBASE_ALWAYS, r5.getChangeId());
assertSubmitType(MERGE_ALWAYS, r6.getChangeId());
assertSubmitType(CHERRY_PICK, r7.getChangeId());
}
@Test

View File

@ -61,14 +61,21 @@ public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
return cfg;
}
protected static Config submitByCherryPickConifg() {
protected static Config submitByCherryPickConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK);
return cfg;
}
protected static Config submitByRebaseConifg() {
protected static Config submitByRebaseAlwaysConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
cfg.setEnum("project", null, "submitType", SubmitType.REBASE_ALWAYS);
return cfg;
}
protected static Config submitByRebaseIfNecessaryConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY);

View File

@ -53,12 +53,17 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
@ConfigSuite.Config
public static Config cherryPick() {
return submitByCherryPickConifg();
return submitByCherryPickConfig();
}
@ConfigSuite.Config
public static Config rebase() {
return submitByRebaseConifg();
public static Config rebaseAlways() {
return submitByRebaseAlwaysConfig();
}
@ConfigSuite.Config
public static Config rebaseIfNecessary() {
return submitByRebaseIfNecessaryConfig();
}
@Test
@ -129,10 +134,11 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
assertThat(preview).containsKey(
new Branch.NameKey(p2, "refs/heads/master"));
if (getSubmitType() == SubmitType.CHERRY_PICK) {
if ((getSubmitType() == SubmitType.CHERRY_PICK)
|| (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
// each change is updated and the respective target branch is updated:
assertThat(preview).hasSize(5);
} else if (getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
} else if ((getSubmitType() == SubmitType.REBASE_IF_NECESSARY)) {
// Either the first is used first as is, then the second and third need
// rebasing, or those two stay as is and the first is rebased.
// add in 2 master branches, expect 3 or 4:

View File

@ -146,7 +146,8 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
Map<Branch.NameKey, RevTree> actual =
fetchFromBundles(request);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
if ((getSubmitType() == SubmitType.CHERRY_PICK)
|| (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
// The change is updated as well:
assertThat(actual).hasSize(2);
} else {
@ -202,7 +203,8 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(),
headAfterFirstSubmit.name());
} else if(getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
} else if ((getSubmitType() == SubmitType.REBASE_IF_NECESSARY)
|| (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
String change2hash = change2.getChange().currentPatchSet()
.getRevision().get();
assertThat(msg).isEqualTo(
@ -253,7 +255,13 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertThat(actual).containsKey(
new Branch.NameKey(project, "refs/heads/master"));
if (getSubmitType() == SubmitType.CHERRY_PICK){
// CherryPick ignores dependencies, thus only change and destination
// branch refs are modified.
assertThat(actual).hasSize(2);
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
// RebaseAlways takes care of dependencies, therefore Change{2,3,4} and
// destination branch will be modified.
assertThat(actual).hasSize(4);
} else {
assertThat(actual).hasSize(1);
}
@ -409,6 +417,8 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
if (getSubmitType() == SubmitType.CHERRY_PICK) {
assertThat(last).startsWith(
"Change has been successfully cherry-picked as ");
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertThat(last).startsWith("Change has been successfully rebased as");
} else {
assertThat(last).isEqualTo(
"Change has been successfully merged by Administrator");

View File

@ -0,0 +1,353 @@
// Copyright (C) 2013 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.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.getChangeId;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.Submit.TestSubmitInput;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
public abstract class AbstractSubmitByRebase extends AbstractSubmit {
@Override
protected abstract SubmitType getSubmitType();
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithRebase() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
submit(change2.getChangeId());
assertRebase(testRepo, false);
RevCommit headAfterSecondSubmit = getRemoteHead();
assertThat(headAfterSecondSubmit.getParent(0))
.isEqualTo(headAfterFirstSubmit);
assertApproved(change2.getChangeId());
assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
assertSubmitter(change2.getChangeId(), 1);
assertSubmitter(change2.getChangeId(), 2);
assertPersonEquals(admin.getIdent(),
headAfterSecondSubmit.getAuthorIdent());
assertPersonEquals(admin.getIdent(),
headAfterSecondSubmit.getCommitterIdent());
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name());
}
@Test
public void submitWithRebaseMultipleChanges() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change1 =
createChange("Change 1", "a.txt", "content");
submit(change1.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertCurrentRevision(change1.getChangeId(), 2, headAfterFirstSubmit);
} else {
assertThat(headAfterFirstSubmit.name())
.isEqualTo(change1.getCommit().name());
}
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
assertThat(change2.getCommit().getParent(0))
.isNotEqualTo(change1.getCommit());
PushOneCommit.Result change3 =
createChange("Change 3", "c.txt", "third content");
PushOneCommit.Result change4 =
createChange("Change 4", "d.txt", "fourth content");
approve(change2.getChangeId());
approve(change3.getChangeId());
submit(change4.getChangeId());
assertRebase(testRepo, false);
assertApproved(change2.getChangeId());
assertApproved(change3.getChangeId());
assertApproved(change4.getChangeId());
RevCommit headAfterSecondSubmit = parse(getRemoteHead());
assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4");
assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit());
assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit);
RevCommit parent = parse(headAfterSecondSubmit.getParent(0));
assertThat(parent.getShortMessage()).isEqualTo("Change 3");
assertThat(parent).isNotEqualTo(change3.getCommit());
assertCurrentRevision(change3.getChangeId(), 2, parent);
RevCommit grandparent = parse(parent.getParent(0));
assertThat(grandparent).isNotEqualTo(change2.getCommit());
assertCurrentRevision(change2.getChangeId(), 2, grandparent);
RevCommit greatgrandparent = parse(grandparent.getParent(0));
assertThat(greatgrandparent).isEqualTo(headAfterFirstSubmit);
if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertCurrentRevision(change1.getChangeId(), 2, greatgrandparent);
} else {
assertCurrentRevision(change1.getChangeId(), 1, greatgrandparent);
}
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name(),
change3.getChangeId(), headAfterSecondSubmit.name(),
change4.getChangeId(), headAfterSecondSubmit.name());
}
@Test
public void submitWithRebaseMergeCommit() throws Exception {
/*
* (HEAD, origin/master, origin/HEAD) Merge changes X,Y
|\
| * Merge branch 'master' into origin/master
| |\
| | * SHA Added a
| |/
* | Before
|/
* Initial empty repository
*/
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo,
"Merge to master", "m.txt", "");
change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
PushOneCommit.Result change2 = change2Push.to("refs/for/master");
testRepo.reset(initialHead);
PushOneCommit.Result change3 = createChange("Before", "b.txt", "");
approve(change3.getChangeId());
submit(change3.getChangeId());
approve(change1.getChangeId());
approve(change2.getChangeId());
submit(change2.getChangeId());
RevCommit newHead = getRemoteHead();
assertThat(newHead.getParentCount()).isEqualTo(2);
RevCommit headParent1 = parse(newHead.getParent(0).getId());
RevCommit headParent2 = parse(newHead.getParent(1).getId());
if (getSubmitType() == SubmitType.REBASE_ALWAYS){
assertCurrentRevision(change3.getChangeId(), 2, headParent1.getId());
} else {
assertThat(change3.getCommit().getId()).isEqualTo(headParent1.getId());
}
assertThat(headParent1.getParentCount()).isEqualTo(1);
assertThat(headParent1.getParent(0)).isEqualTo(initialHead);
assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId());
assertThat(headParent2.getParentCount()).isEqualTo(2);
RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId());
RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId());
assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId());
assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
}
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithContentMerge_Conflict() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "a.txt", "other content");
submitWithConflict(change2.getChangeId(),
"Cannot rebase " + change2.getCommit().name()
+ ": The change could not be rebased due to a conflict during merge.");
RevCommit head = getRemoteHead();
assertThat(head).isEqualTo(headAfterFirstSubmit);
assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
assertNoSubmitter(change2.getChangeId(), 1);
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
}
@Test
public void repairChangeStateAfterFailure() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
Change.Id id2 = change2.getChange().getId();
SubmitInput failAfterRefUpdates =
new TestSubmitInput(new SubmitInput(), true);
submit(change2.getChangeId(), failAfterRefUpdates,
ResourceConflictException.class, "Failing after ref updates");
RevCommit headAfterFailedSubmit = getRemoteHead();
// Bad: ref advanced but change wasn't updated.
PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
ChangeInfo info = gApi.changes().id(id2.get()).get();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
assertThat(getPatchSet(psId2)).isNull();
ObjectId rev2;
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
assertThat(rev1).isNotNull();
rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
assertThat(rev2).isNotNull();
assertThat(rev2).isNotEqualTo(rev1);
assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit);
assertThat(repo.exactRef("refs/heads/master").getObjectId())
.isEqualTo(rev2);
}
submit(change2.getChangeId());
RevCommit headAfterSecondSubmit = getRemoteHead();
assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
// Change status and patch set entities were updated, and branch tip stayed
// the same.
info = gApi.changes().id(id2.get()).get();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
PatchSet ps2 = getPatchSet(psId2);
assertThat(ps2).isNotNull();
assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
assertThat(Iterables.getLast(info.messages).message)
.isEqualTo("Change has been successfully rebased as "
+ rev2.name() + " by Administrator");
try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef("refs/heads/master").getObjectId())
.isEqualTo(rev2);
}
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name());
}
protected RevCommit parse(ObjectId id) throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit c = rw.parseCommit(id);
rw.parseBody(c);
return c;
}
}
@Test
public void submitAfterReorderOfCommits() throws Exception {
RevCommit initialHead = getRemoteHead();
// Create two commits and push.
RevCommit c1 = commitBuilder()
.add("a.txt", "1")
.message("subject: 1")
.create();
RevCommit c2 = commitBuilder()
.add("b.txt", "2")
.message("subject: 2")
.create();
pushHead(testRepo, "refs/for/master", false);
String id1 = getChangeId(testRepo, c1).get();
String id2 = getChangeId(testRepo, c2).get();
// Swap the order of commits and push again.
testRepo.reset("HEAD~2");
testRepo.cherryPick(c2);
testRepo.cherryPick(c1);
pushHead(testRepo, "refs/for/master", false);
approve(id1);
approve(id2);
submit(id1);
RevCommit headAfterSubmit = getRemoteHead();
assertRefUpdatedEvents(initialHead, headAfterSubmit);
assertChangeMergedEvents(id2, headAfterSubmit.name(),
id1, headAfterSubmit.name());
}
@Test
public void submitChangesAfterBranchOnSecond() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change = createChange();
approve(change.getChangeId());
PushOneCommit.Result change2 = createChange();
approve(change2.getChangeId());
Project.NameKey project = change2.getChange().change().getProject();
Branch.NameKey branch = new Branch.NameKey(project, "branch");
createBranchWithRevision(branch, change2.getCommit().getName());
gApi.changes().id(change2.getChangeId()).current().submit();
assertMerged(change2.getChangeId());
assertMerged(change.getChangeId());
RevCommit newHead = getRemoteHead();
assertRefUpdatedEvents(initialHead, newHead);
assertChangeMergedEvents(change.getChangeId(), newHead.name(),
change2.getChangeId(), newHead.name());
}
}

View File

@ -3,6 +3,7 @@ include_defs('//gerrit-acceptance-tests/tests.defs')
SUBMIT_UTIL_SRCS = [
'AbstractSubmit.java',
'AbstractSubmitByMerge.java',
'AbstractSubmitByRebase.java',
]
SUBMIT_TESTS = glob(['Submit*IT.java'])

View File

@ -0,0 +1,53 @@
// 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.change;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
public class SubmitByRebaseAlwaysIT extends AbstractSubmitByRebase {
@Override
protected SubmitType getSubmitType() {
return SubmitType.REBASE_ALWAYS;
}
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithPossibleFastForward() throws Exception {
RevCommit oldHead = getRemoteHead();
PushOneCommit.Result change = createChange();
submit(change.getChangeId());
RevCommit head = getRemoteHead();
assertThat(head.getId()).isNotEqualTo(change.getCommit());
assertThat(head.getParent(0)).isEqualTo(oldHead);
assertApproved(change.getChangeId());
assertCurrentRevision(change.getChangeId(), 2, head);
assertSubmitter(change.getChangeId(), 1);
assertSubmitter(change.getChangeId(), 2);
assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
assertRefUpdatedEvents(oldHead, head);
assertChangeMergedEvents(change.getChangeId(), head.name());
}
}

View File

@ -15,32 +15,16 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.getChangeId;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.Submit.TestSubmitInput;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
public class SubmitByRebaseIfNecessaryIT extends AbstractSubmitByRebase {
@Override
protected SubmitType getSubmitType() {
@ -65,143 +49,6 @@ public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
assertChangeMergedEvents(change.getChangeId(), head.name());
}
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithRebase() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
submit(change2.getChangeId());
assertRebase(testRepo, false);
RevCommit headAfterSecondSubmit = getRemoteHead();
assertThat(headAfterSecondSubmit.getParent(0))
.isEqualTo(headAfterFirstSubmit);
assertApproved(change2.getChangeId());
assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit);
assertSubmitter(change2.getChangeId(), 1);
assertSubmitter(change2.getChangeId(), 2);
assertPersonEquals(admin.getIdent(),
headAfterSecondSubmit.getAuthorIdent());
assertPersonEquals(admin.getIdent(),
headAfterSecondSubmit.getCommitterIdent());
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name());
}
@Test
public void submitWithRebaseMultipleChanges() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change1 =
createChange("Change 1", "a.txt", "content");
submit(change1.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
assertThat(headAfterFirstSubmit.name())
.isEqualTo(change1.getCommit().name());
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
assertThat(change2.getCommit().getParent(0))
.isNotEqualTo(change1.getCommit());
PushOneCommit.Result change3 =
createChange("Change 3", "c.txt", "third content");
PushOneCommit.Result change4 =
createChange("Change 4", "d.txt", "fourth content");
approve(change2.getChangeId());
approve(change3.getChangeId());
submit(change4.getChangeId());
assertRebase(testRepo, false);
assertApproved(change2.getChangeId());
assertApproved(change3.getChangeId());
assertApproved(change4.getChangeId());
RevCommit headAfterSecondSubmit = parse(getRemoteHead());
assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4");
assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit());
assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit);
RevCommit parent = parse(headAfterSecondSubmit.getParent(0));
assertThat(parent.getShortMessage()).isEqualTo("Change 3");
assertThat(parent).isNotEqualTo(change3.getCommit());
assertCurrentRevision(change3.getChangeId(), 2, parent);
RevCommit grandparent = parse(parent.getParent(0));
assertThat(grandparent).isNotEqualTo(change2.getCommit());
assertCurrentRevision(change2.getChangeId(), 2, grandparent);
RevCommit greatgrandparent = parse(grandparent.getParent(0));
assertThat(greatgrandparent).isEqualTo(change1.getCommit());
assertCurrentRevision(change1.getChangeId(), 1, greatgrandparent);
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit,
headAfterFirstSubmit, headAfterSecondSubmit);
assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name(),
change3.getChangeId(), headAfterSecondSubmit.name(),
change4.getChangeId(), headAfterSecondSubmit.name());
}
@Test
public void submitWithRebaseMergeCommit() throws Exception {
/*
* (HEAD, origin/master, origin/HEAD) Merge changes X,Y
|\
| * Merge branch 'master' into origin/master
| |\
| | * SHA Added a
| |/
* | Before
|/
* Initial empty repository
*/
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change1 = createChange("Added a", "a.txt", "");
PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo,
"Merge to master", "m.txt", "");
change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit()));
PushOneCommit.Result change2 = change2Push.to("refs/for/master");
testRepo.reset(initialHead);
PushOneCommit.Result change3 = createChange("Before", "b.txt", "");
approve(change3.getChangeId());
submit(change3.getChangeId());
approve(change1.getChangeId());
approve(change2.getChangeId());
submit(change2.getChangeId());
RevCommit newHead = getRemoteHead();
assertThat(newHead.getParentCount()).isEqualTo(2);
RevCommit headParent1 = parse(newHead.getParent(0).getId());
RevCommit headParent2 = parse(newHead.getParent(1).getId());
assertThat(headParent1.getId()).isEqualTo(change3.getCommit().getId());
assertThat(headParent1.getParentCount()).isEqualTo(1);
assertThat(headParent1.getParent(0)).isEqualTo(initialHead);
assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId());
assertThat(headParent2.getParentCount()).isEqualTo(2);
RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId());
RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId());
assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId());
assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId());
}
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithContentMerge() throws Exception {
@ -235,160 +82,4 @@ public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
change2.getChangeId(), headAfterSecondSubmit.name(),
change3.getChangeId(), headAfterThirdSubmit.name());
}
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithContentMerge_Conflict() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "a.txt", "other content");
submitWithConflict(change2.getChangeId(),
"Cannot rebase " + change2.getCommit().name()
+ ": The change could not be rebased due to a conflict during merge.");
RevCommit head = getRemoteHead();
assertThat(head).isEqualTo(headAfterFirstSubmit);
assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
assertNoSubmitter(change2.getChangeId(), 1);
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
}
@Test
public void repairChangeStateAfterFailure() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change =
createChange("Change 1", "a.txt", "content");
submit(change.getChangeId());
RevCommit headAfterFirstSubmit = getRemoteHead();
testRepo.reset(initialHead);
PushOneCommit.Result change2 =
createChange("Change 2", "b.txt", "other content");
Change.Id id2 = change2.getChange().getId();
SubmitInput failAfterRefUpdates =
new TestSubmitInput(new SubmitInput(), true);
submit(change2.getChangeId(), failAfterRefUpdates,
ResourceConflictException.class, "Failing after ref updates");
RevCommit headAfterFailedSubmit = getRemoteHead();
// Bad: ref advanced but change wasn't updated.
PatchSet.Id psId1 = new PatchSet.Id(id2, 1);
PatchSet.Id psId2 = new PatchSet.Id(id2, 2);
ChangeInfo info = gApi.changes().id(id2.get()).get();
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1);
assertThat(getPatchSet(psId2)).isNull();
ObjectId rev2;
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId();
assertThat(rev1).isNotNull();
rev2 = repo.exactRef(psId2.toRefName()).getObjectId();
assertThat(rev2).isNotNull();
assertThat(rev2).isNotEqualTo(rev1);
assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit);
assertThat(repo.exactRef("refs/heads/master").getObjectId())
.isEqualTo(rev2);
}
submit(change2.getChangeId());
RevCommit headAfterSecondSubmit = getRemoteHead();
assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit);
// Change status and patch set entities were updated, and branch tip stayed
// the same.
info = gApi.changes().id(id2.get()).get();
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2);
PatchSet ps2 = getPatchSet(psId2);
assertThat(ps2).isNotNull();
assertThat(ps2.getRevision().get()).isEqualTo(rev2.name());
assertThat(Iterables.getLast(info.messages).message)
.isEqualTo("Change has been successfully rebased as "
+ rev2.name() + " by Administrator");
try (Repository repo = repoManager.openRepository(project)) {
assertThat(repo.exactRef("refs/heads/master").getObjectId())
.isEqualTo(rev2);
}
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(),
change2.getChangeId(), headAfterSecondSubmit.name());
}
private RevCommit parse(ObjectId id) throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit c = rw.parseCommit(id);
rw.parseBody(c);
return c;
}
}
@Test
public void submitAfterReorderOfCommits() throws Exception {
RevCommit initialHead = getRemoteHead();
// Create two commits and push.
RevCommit c1 = commitBuilder()
.add("a.txt", "1")
.message("subject: 1")
.create();
RevCommit c2 = commitBuilder()
.add("b.txt", "2")
.message("subject: 2")
.create();
pushHead(testRepo, "refs/for/master", false);
String id1 = getChangeId(testRepo, c1).get();
String id2 = getChangeId(testRepo, c2).get();
// Swap the order of commits and push again.
testRepo.reset("HEAD~2");
testRepo.cherryPick(c2);
testRepo.cherryPick(c1);
pushHead(testRepo, "refs/for/master", false);
approve(id1);
approve(id2);
submit(id1);
RevCommit headAfterSubmit = getRemoteHead();
assertRefUpdatedEvents(initialHead, headAfterSubmit);
assertChangeMergedEvents(id2, headAfterSubmit.name(),
id1, headAfterSubmit.name());
}
@Test
public void submitChangesAfterBranchOnSecond() throws Exception {
RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change = createChange();
approve(change.getChangeId());
PushOneCommit.Result change2 = createChange();
approve(change2.getChangeId());
Project.NameKey project = change2.getChange().change().getProject();
Branch.NameKey branch = new Branch.NameKey(project, "branch");
createBranchWithRevision(branch, change2.getCommit().getName());
gApi.changes().id(change2.getChangeId()).current().submit();
assertMerged(change2.getChangeId());
assertMerged(change.getChangeId());
RevCommit newHead = getRemoteHead();
assertRefUpdatedEvents(initialHead, newHead);
assertChangeMergedEvents(change.getChangeId(), newHead.name(),
change2.getChangeId(), newHead.name());
}
}

View File

@ -18,6 +18,7 @@ public enum SubmitType {
FAST_FORWARD_ONLY,
MERGE_IF_NECESSARY,
REBASE_IF_NECESSARY,
REBASE_ALWAYS,
MERGE_ALWAYS,
CHERRY_PICK
}

View File

@ -77,6 +77,7 @@ public interface AdminConstants extends Constants {
String projectSubmitType_MERGE_ALWAYS();
String projectSubmitType_MERGE_IF_NECESSARY();
String projectSubmitType_REBASE_IF_NECESSARY();
String projectSubmitType_REBASE_ALWAYS();
String projectSubmitType_CHERRY_PICK();
String headingProjectState();

View File

@ -54,6 +54,7 @@ headingAuditLog = Audit Log
headingProjectSubmitType = Submit Type
projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
projectSubmitType_MERGE_IF_NECESSARY = Merge if Necessary
projectSubmitType_REBASE_ALWAYS = Rebase Always
projectSubmitType_REBASE_IF_NECESSARY = Rebase if Necessary
projectSubmitType_MERGE_ALWAYS = Always Merge
projectSubmitType_CHERRY_PICK = Cherry Pick

View File

@ -43,6 +43,8 @@ public class Util {
return C.projectSubmitType_MERGE_IF_NECESSARY();
case REBASE_IF_NECESSARY:
return C.projectSubmitType_REBASE_IF_NECESSARY();
case REBASE_ALWAYS:
return C.projectSubmitType_REBASE_ALWAYS();
case MERGE_ALWAYS:
return C.projectSubmitType_MERGE_ALWAYS();
case CHERRY_PICK:

View File

@ -64,12 +64,15 @@ public class MergeabilityCacheImpl implements MergeabilityCache {
private static final String CACHE_NAME = "mergeability";
public static final BiMap<SubmitType, Character> SUBMIT_TYPES = ImmutableBiMap.of(
SubmitType.FAST_FORWARD_ONLY, 'F',
SubmitType.MERGE_IF_NECESSARY, 'M',
SubmitType.REBASE_IF_NECESSARY, 'R',
SubmitType.MERGE_ALWAYS, 'A',
SubmitType.CHERRY_PICK, 'C');
public static final BiMap<SubmitType, Character> SUBMIT_TYPES =
new ImmutableBiMap.Builder<SubmitType, Character>()
.put(SubmitType.FAST_FORWARD_ONLY, 'F')
.put(SubmitType.MERGE_IF_NECESSARY, 'M')
.put(SubmitType.REBASE_ALWAYS, 'P')
.put(SubmitType.REBASE_IF_NECESSARY, 'R')
.put(SubmitType.MERGE_ALWAYS, 'A')
.put(SubmitType.CHERRY_PICK, 'C')
.build();
static {
checkState(SUBMIT_TYPES.size() == SubmitType.values().length,

View File

@ -68,6 +68,7 @@ public class RebaseChangeOp extends BatchUpdate.Op {
private CommitValidators.Policy validate;
private boolean forceContentMerge;
private boolean copyApprovals = true;
private boolean postMessage = true;
private RevCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId;
@ -117,6 +118,11 @@ public class RebaseChangeOp extends BatchUpdate.Op {
return this;
}
public RebaseChangeOp setPostMessage(boolean postMessage) {
this.postMessage = postMessage;
return this;
}
@Override
public void updateRepo(RepoContext ctx) throws MergeConflictException,
InvalidChangeOperationException, RestApiException, IOException,
@ -153,10 +159,11 @@ public class RebaseChangeOp extends BatchUpdate.Op {
.setDraft(originalPatchSet.isDraft())
.setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(fireRevisionCreated)
.setCopyApprovals(copyApprovals)
.setMessage(
"Patch Set " + rebasedPatchSetId.get()
.setCopyApprovals(copyApprovals);
if (postMessage) {
patchSetInserter.setMessage("Patch Set " + rebasedPatchSetId.get()
+ ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
}
if (base != null) {
patchSetInserter.setGroups(base.patchSet().getGroups());

View File

@ -0,0 +1,22 @@
// 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.strategy;
public class RebaseAlways extends RebaseSubmitStrategy {
RebaseAlways(SubmitStrategy.Arguments args) {
super(args, true);
}
}

View File

@ -14,207 +14,9 @@
package com.google.gerrit.server.git.strategy;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.RebaseSorter;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class RebaseIfNecessary extends SubmitStrategy {
public class RebaseIfNecessary extends RebaseSubmitStrategy {
RebaseIfNecessary(SubmitStrategy.Arguments args) {
super(args);
}
@Override
public List<SubmitStrategyOp> buildOps(
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
List<CodeReviewCommit> sorted = sort(toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
boolean first = true;
for (CodeReviewCommit c : sorted) {
if (c.getParentCount() > 1) {
// Since there is a merge commit, sort and prune again using
// MERGE_IF_NECESSARY semantics to avoid creating duplicate
// commits.
//
sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
break;
}
}
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
if (first && args.mergeTip.getInitialTip() == null) {
ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new RebaseRootOp(n));
} else if (n.getParentCount() == 1) {
ops.add(new RebaseOneOp(n));
} else {
ops.add(new RebaseMultipleParentsOp(n));
}
first = false;
}
return ops;
}
private class RebaseRootOp extends SubmitStrategyOp {
private RebaseRootOp(CodeReviewCommit toMerge) {
super(RebaseIfNecessary.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx) {
// Refuse to merge a root commit into an existing branch, we cannot obtain
// a delta for the cherry-pick to apply.
toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
}
}
private class RebaseOneOp extends SubmitStrategyOp {
private RebaseChangeOp rebaseOp;
private CodeReviewCommit newCommit;
private RebaseOneOp(CodeReviewCommit toMerge) {
super(RebaseIfNecessary.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, InvalidChangeOperationException,
RestApiException, IOException, OrmException {
// TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
// When hoisting BatchUpdate into MergeOp, we will need to teach
// BatchUpdate how to produce CodeReviewRevWalks.
if (args.mergeUtil
.canFastForward(args.mergeSorter, args.mergeTip.getCurrentTip(),
args.rw, toMerge)) {
args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
acceptMergeTip(args.mergeTip);
return;
}
// Stale read of patch set is ok; see comments in RebaseChangeOp.
PatchSet origPs = args.psUtil.get(
ctx.getDb(), toMerge.getControl().getNotes(), toMerge.getPatchsetId());
rebaseOp = args.rebaseFactory.create(
toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name())
.setFireRevisionCreated(false)
// Bypass approval copier since SubmitStrategyOp copy all approvals
// later anyway.
.setCopyApprovals(false)
.setValidatePolicy(CommitValidators.Policy.NONE);
try {
rebaseOp.updateRepo(ctx);
} catch (MergeConflictException | NoSuchChangeException e) {
toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
throw new IntegrationException(
"Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
}
newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
newCommit = amendGitlink(newCommit);
newCommit.copyFrom(toMerge);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
newCommit.setPatchsetId(rebaseOp.getPatchSetId());
args.mergeTip.moveTipTo(newCommit, newCommit);
args.commits.put(args.mergeTip.getCurrentTip());
acceptMergeTip(args.mergeTip);
}
@Override
public PatchSet updateChangeImpl(ChangeContext ctx)
throws NoSuchChangeException, ResourceConflictException,
OrmException, IOException {
if (rebaseOp == null) {
// Took the fast-forward option, nothing to do.
return null;
}
rebaseOp.updateChange(ctx);
ctx.getChange().setCurrentPatchSet(
args.patchSetInfoFactory.get(
args.rw, newCommit, rebaseOp.getPatchSetId()));
newCommit.setControl(ctx.getControl());
return rebaseOp.getPatchSet();
}
@Override
public void postUpdateImpl(Context ctx) throws OrmException {
if (rebaseOp != null) {
rebaseOp.postUpdate(ctx);
}
}
}
private class RebaseMultipleParentsOp extends SubmitStrategyOp {
private RebaseMultipleParentsOp(CodeReviewCommit toMerge) {
super(RebaseIfNecessary.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, IOException {
// There are multiple parents, so this is a merge commit. We don't want
// to rebase the merge as clients can't easily rebase their history with
// that merge present and replaced by an equivalent merge with a different
// first parent. So instead behave as though MERGE_IF_NECESSARY was
// configured.
MergeTip mergeTip = args.mergeTip;
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge) &&
!args.submoduleOp.hasSubscription(args.destBranch)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
args.serverIdent, args.serverIdent, args.repo, args.rw,
args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge);
mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
acceptMergeTip(mergeTip);
}
}
private void acceptMergeTip(MergeTip mergeTip) {
args.alreadyAccepted.add(mergeTip.getCurrentTip());
}
private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
throws IntegrationException {
try {
return new RebaseSorter(
args.rw, args.alreadyAccepted, args.canMergeFlag).sort(toSort);
} catch (IOException e) {
throw new IntegrationException("Commit sorting failed", e);
}
}
static boolean dryRun(SubmitDryRun.Arguments args,
CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
// Test for merge instead of cherry pick to avoid false negatives
// on commit chains.
return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
&& args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
toMerge);
super(args, false);
}
}

View File

@ -0,0 +1,289 @@
// Copyright (C) 2012 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.strategy;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.git.strategy.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.RebaseSorter;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* This strategy covers RebaseAlways and RebaseIfNecessary ones.
*/
public class RebaseSubmitStrategy extends SubmitStrategy {
private final boolean rebaseAlways;
RebaseSubmitStrategy(SubmitStrategy.Arguments args, boolean rebaseAlways) {
super(args);
this.rebaseAlways = rebaseAlways;
}
@Override
public List<SubmitStrategyOp> buildOps(
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
List<CodeReviewCommit> sorted = sort(toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
boolean first = true;
for (CodeReviewCommit c : sorted) {
if (c.getParentCount() > 1) {
// Since there is a merge commit, sort and prune again using
// MERGE_IF_NECESSARY semantics to avoid creating duplicate
// commits.
//
sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
break;
}
}
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
if (first && args.mergeTip.getInitialTip() == null) {
// TODO(tandrii): Cherry-Pick strategy does this too, but it's wrong
// and can be fixed.
ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new RebaseRootOp(n));
} else if (n.getParentCount() == 1) {
ops.add(new RebaseOneOp(n));
} else {
ops.add(new RebaseMultipleParentsOp(n));
}
first = false;
}
return ops;
}
private class RebaseRootOp extends SubmitStrategyOp {
private RebaseRootOp(CodeReviewCommit toMerge) {
super(RebaseSubmitStrategy.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx) {
// Refuse to merge a root commit into an existing branch, we cannot obtain
// a delta for the cherry-pick to apply.
toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
}
}
private class RebaseOneOp extends SubmitStrategyOp {
private RebaseChangeOp rebaseOp;
private CodeReviewCommit newCommit;
private PatchSet.Id newPatchSetId;
private RebaseOneOp(CodeReviewCommit toMerge) {
super(RebaseSubmitStrategy.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, InvalidChangeOperationException,
RestApiException, IOException, OrmException {
// TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
// When hoisting BatchUpdate into MergeOp, we will need to teach
// BatchUpdate how to produce CodeReviewRevWalks.
if (args.mergeUtil
.canFastForward(args.mergeSorter, args.mergeTip.getCurrentTip(),
args.rw, toMerge)) {
if (!rebaseAlways){
args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
acceptMergeTip(args.mergeTip);
return;
}
// RebaseAlways means we modify commit message.
args.rw.parseBody(toMerge);
newPatchSetId = ChangeUtil.nextPatchSetId(
args.repo, toMerge.change().currentPatchSetId());
// TODO(tandrii): add extension point to customize this commit message.
String cherryPickCmtMsg =
args.mergeUtil.createCherryPickCommitMessage(toMerge);
PersonIdent committer = args.caller.newCommitterIdent(ctx.getWhen(),
args.serverIdent.getTimeZone());
try {
newCommit = args.mergeUtil.createCherryPickFromCommit(args.repo,
args.inserter, args.mergeTip.getCurrentTip(), toMerge, committer,
cherryPickCmtMsg, args.rw);
} catch (MergeConflictException mce) {
// Unlike in Cherry-pick case, this should never happen.
toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
throw new IllegalStateException(
"MergeConflictException on message edit must not happen");
} catch (MergeIdenticalTreeException mie) {
toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE);
return;
}
ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), newCommit,
newPatchSetId.toRefName()));
} else {
// Stale read of patch set is ok; see comments in RebaseChangeOp.
PatchSet origPs = args.psUtil.get(ctx.getDb(),
toMerge.getControl().getNotes(), toMerge.getPatchsetId());
// TODO(tandrii): add extension point to customize commit message while
// rebasing.
rebaseOp = args.rebaseFactory.create(
toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name())
.setFireRevisionCreated(false)
// Bypass approval copier since SubmitStrategyOp copy all approvals
// later anyway.
.setCopyApprovals(false)
.setValidatePolicy(CommitValidators.Policy.NONE)
// Do not post message after inserting new patchset because there
// will be one about change being merged already.
.setPostMessage(false);
try {
rebaseOp.updateRepo(ctx);
} catch (MergeConflictException | NoSuchChangeException e) {
toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
throw new IntegrationException(
"Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
}
newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
newPatchSetId = rebaseOp.getPatchSetId();
}
newCommit = amendGitlink(newCommit);
newCommit.copyFrom(toMerge);
newCommit.setPatchsetId(newPatchSetId);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
args.mergeTip.moveTipTo(newCommit, newCommit);
args.commits.put(args.mergeTip.getCurrentTip());
acceptMergeTip(args.mergeTip);
}
@Override
public PatchSet updateChangeImpl(ChangeContext ctx)
throws NoSuchChangeException, ResourceConflictException,
OrmException, IOException {
if (newCommit == null) {
checkState(!rebaseAlways, "RebaseAlways must never fast forward");
// Took the fast-forward option, nothing to do.
return null;
}
PatchSet newPs;
if (rebaseOp != null) {
rebaseOp.updateChange(ctx);
newPs = rebaseOp.getPatchSet();
} else {
// CherryPick
PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
newPs = args.psUtil.insert(ctx.getDb(), ctx.getRevWalk(),
ctx.getUpdate(newPatchSetId), newPatchSetId, newCommit, false,
prevPs != null ? prevPs.getGroups() : ImmutableList.<String> of(),
null);
}
ctx.getChange().setCurrentPatchSet(args.patchSetInfoFactory
.get(ctx.getRevWalk(), newCommit, newPatchSetId));
newCommit.setControl(ctx.getControl());
return newPs;
}
@Override
public void postUpdateImpl(Context ctx) throws OrmException {
if (rebaseOp != null) {
rebaseOp.postUpdate(ctx);
}
}
}
private class RebaseMultipleParentsOp extends SubmitStrategyOp {
private RebaseMultipleParentsOp(CodeReviewCommit toMerge) {
super(RebaseSubmitStrategy.this.args, toMerge);
}
@Override
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, IOException {
// There are multiple parents, so this is a merge commit. We don't want
// to rebase the merge as clients can't easily rebase their history with
// that merge present and replaced by an equivalent merge with a different
// first parent. So instead behave as though MERGE_IF_NECESSARY was
// configured.
// TODO(tandrii): this is not in spirit of RebaseAlways strategy because
// the commit messages can not be modified in the process. It's also
// possible to implement rebasing of merge commits. E.g., the Cherry Pick
// REST endpoint already supports cherry-picking of merge commits.
// For now, users of RebaseAlways strategy for whom changed commit footers
// are important would be well advised to prohibit uploading patches with
// merge commits.
MergeTip mergeTip = args.mergeTip;
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge) &&
!args.submoduleOp.hasSubscription(args.destBranch)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
args.serverIdent, args.serverIdent, args.repo, args.rw,
args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge);
mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
acceptMergeTip(mergeTip);
}
}
private void acceptMergeTip(MergeTip mergeTip) {
args.alreadyAccepted.add(mergeTip.getCurrentTip());
}
private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
throws IntegrationException {
try {
return new RebaseSorter(
args.rw, args.alreadyAccepted, args.canMergeFlag).sort(toSort);
} catch (IOException e) {
throw new IntegrationException("Commit sorting failed", e);
}
}
static boolean dryRun(SubmitDryRun.Arguments args,
CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
// Test for merge instead of cherry pick to avoid false negatives
// on commit chains.
return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
&& args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
toMerge);
}
}

View File

@ -122,6 +122,8 @@ public class SubmitDryRun {
return MergeIfNecessary.dryRun(args, tipCommit, toMergeCommit);
case REBASE_IF_NECESSARY:
return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit);
case REBASE_ALWAYS:
return RebaseAlways.dryRun(args, tipCommit, toMergeCommit);
default:
String errorMsg = "No submit strategy for: " + submitType;
log.error(errorMsg);

View File

@ -71,6 +71,8 @@ public class SubmitStrategyFactory {
return new MergeIfNecessary(args);
case REBASE_IF_NECESSARY:
return new RebaseIfNecessary(args);
case REBASE_ALWAYS:
return new RebaseAlways(args);
default:
String errorMsg = "No submit strategy for: " + submitType;
log.error(errorMsg);

View File

@ -432,6 +432,7 @@ abstract class SubmitStrategyOp extends BatchUpdate.Op {
case CHERRY_PICK:
return message(ctx, commit, CommitMergeStatus.CLEAN_PICK);
case REBASE_IF_NECESSARY:
case REBASE_ALWAYS:
return message(ctx, commit, CommitMergeStatus.CLEAN_REBASE);
default:
throw new IllegalStateException("unexpected submit type "

View File

@ -58,6 +58,10 @@ public class RepositoryConfigTest {
configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY);
assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
.isEqualTo(SubmitType.REBASE_IF_NECESSARY);
configureDefaultSubmitType("*", SubmitType.REBASE_ALWAYS);
assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject")))
.isEqualTo(SubmitType.REBASE_ALWAYS);
}
@Test