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
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 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. 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]] [[content_merge]]
If `Allow content merges` is enabled, Gerrit will try If `Allow content merges` is enabled, Gerrit will try
to do a content merge when a path conflict occurs. 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_ALWAYS;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY; 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_IF_NECESSARY;
import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@@ -123,6 +124,10 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
+ "gerrit:commit_message(M)," + "gerrit:commit_message(M),"
+ "regex_matches('.*REBASE_IF_NECESSARY.*', M)," + "regex_matches('.*REBASE_IF_NECESSARY.*', M),"
+ "!.\n" + "!.\n"
+ "submit_type(rebase_always) :-"
+ "gerrit:commit_message(M),"
+ "regex_matches('.*REBASE_ALWAYS.*', M),"
+ "!.\n"
+ "submit_type(merge_always) :-" + "submit_type(merge_always) :-"
+ "gerrit:commit_message(M)," + "gerrit:commit_message(M),"
+ "regex_matches('.*MERGE_ALWAYS.*', 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 r2 = createChange("master", "FAST_FORWARD_ONLY 2");
PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3"); PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4"); PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4");
PushOneCommit.Result r5 = createChange("master", "MERGE_ALWAYS 5"); PushOneCommit.Result r5 = createChange("master", "REBASE_ALWAYS 5");
PushOneCommit.Result r6 = createChange("master", "CHERRY_PICK 6"); 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, r1.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r2.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, r4.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r7.getChangeId());
setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
@@ -173,8 +180,9 @@ public class SubmitTypeRuleIT extends AbstractDaemonTest {
assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId()); assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId());
assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId()); assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId());
assertSubmitType(MERGE_ALWAYS, r5.getChangeId()); assertSubmitType(REBASE_ALWAYS, r5.getChangeId());
assertSubmitType(CHERRY_PICK, r6.getChangeId()); assertSubmitType(MERGE_ALWAYS, r6.getChangeId());
assertSubmitType(CHERRY_PICK, r7.getChangeId());
} }
@Test @Test

View File

@@ -61,14 +61,21 @@ public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
return cfg; return cfg;
} }
protected static Config submitByCherryPickConifg() { protected static Config submitByCherryPickConfig() {
Config cfg = new Config(); Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true); cfg.setBoolean("change", null, "submitWholeTopic", true);
cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK); cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK);
return cfg; 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(); Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true); cfg.setBoolean("change", null, "submitWholeTopic", true);
cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY); cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY);

View File

@@ -53,12 +53,17 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
@ConfigSuite.Config @ConfigSuite.Config
public static Config cherryPick() { public static Config cherryPick() {
return submitByCherryPickConifg(); return submitByCherryPickConfig();
} }
@ConfigSuite.Config @ConfigSuite.Config
public static Config rebase() { public static Config rebaseAlways() {
return submitByRebaseConifg(); return submitByRebaseAlwaysConfig();
}
@ConfigSuite.Config
public static Config rebaseIfNecessary() {
return submitByRebaseIfNecessaryConfig();
} }
@Test @Test
@@ -129,10 +134,11 @@ public class SubmoduleSubscriptionsWholeTopicMergeIT
assertThat(preview).containsKey( assertThat(preview).containsKey(
new Branch.NameKey(p2, "refs/heads/master")); 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: // each change is updated and the respective target branch is updated:
assertThat(preview).hasSize(5); 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 // 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. // rebasing, or those two stay as is and the first is rebased.
// add in 2 master branches, expect 3 or 4: // 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 = Map<Branch.NameKey, RevTree> actual =
fetchFromBundles(request); fetchFromBundles(request);
if (getSubmitType() == SubmitType.CHERRY_PICK) { if ((getSubmitType() == SubmitType.CHERRY_PICK)
|| (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
// The change is updated as well: // The change is updated as well:
assertThat(actual).hasSize(2); assertThat(actual).hasSize(2);
} else { } else {
@@ -202,7 +203,8 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
assertChangeMergedEvents(change.getChangeId(), assertChangeMergedEvents(change.getChangeId(),
headAfterFirstSubmit.name()); 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() String change2hash = change2.getChange().currentPatchSet()
.getRevision().get(); .getRevision().get();
assertThat(msg).isEqualTo( assertThat(msg).isEqualTo(
@@ -252,8 +254,14 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertThat(actual).containsKey( assertThat(actual).containsKey(
new Branch.NameKey(project, "refs/heads/master")); new Branch.NameKey(project, "refs/heads/master"));
if (getSubmitType() == SubmitType.CHERRY_PICK) { if (getSubmitType() == SubmitType.CHERRY_PICK){
// CherryPick ignores dependencies, thus only change and destination
// branch refs are modified.
assertThat(actual).hasSize(2); 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 { } else {
assertThat(actual).hasSize(1); assertThat(actual).hasSize(1);
} }
@@ -409,6 +417,8 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
if (getSubmitType() == SubmitType.CHERRY_PICK) { if (getSubmitType() == SubmitType.CHERRY_PICK) {
assertThat(last).startsWith( assertThat(last).startsWith(
"Change has been successfully cherry-picked as "); "Change has been successfully cherry-picked as ");
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertThat(last).startsWith("Change has been successfully rebased as");
} else { } else {
assertThat(last).isEqualTo( assertThat(last).isEqualTo(
"Change has been successfully merged by Administrator"); "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 = [ SUBMIT_UTIL_SRCS = [
'AbstractSubmit.java', 'AbstractSubmit.java',
'AbstractSubmitByMerge.java', 'AbstractSubmitByMerge.java',
'AbstractSubmitByRebase.java',
] ]
SUBMIT_TESTS = glob(['Submit*IT.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; package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat; 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.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput; 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.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType; 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.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test; import org.junit.Test;
public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit { public class SubmitByRebaseIfNecessaryIT extends AbstractSubmitByRebase {
@Override @Override
protected SubmitType getSubmitType() { protected SubmitType getSubmitType() {
@@ -65,143 +49,6 @@ public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
assertChangeMergedEvents(change.getChangeId(), head.name()); 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 @Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE) @TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithContentMerge() throws Exception { public void submitWithContentMerge() throws Exception {
@@ -235,160 +82,4 @@ public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
change2.getChangeId(), headAfterSecondSubmit.name(), change2.getChangeId(), headAfterSecondSubmit.name(),
change3.getChangeId(), headAfterThirdSubmit.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, FAST_FORWARD_ONLY,
MERGE_IF_NECESSARY, MERGE_IF_NECESSARY,
REBASE_IF_NECESSARY, REBASE_IF_NECESSARY,
REBASE_ALWAYS,
MERGE_ALWAYS, MERGE_ALWAYS,
CHERRY_PICK CHERRY_PICK
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,7 @@ public class RebaseChangeOp extends BatchUpdate.Op {
private CommitValidators.Policy validate; private CommitValidators.Policy validate;
private boolean forceContentMerge; private boolean forceContentMerge;
private boolean copyApprovals = true; private boolean copyApprovals = true;
private boolean postMessage = true;
private RevCommit rebasedCommit; private RevCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId; private PatchSet.Id rebasedPatchSetId;
@@ -117,6 +118,11 @@ public class RebaseChangeOp extends BatchUpdate.Op {
return this; return this;
} }
public RebaseChangeOp setPostMessage(boolean postMessage) {
this.postMessage = postMessage;
return this;
}
@Override @Override
public void updateRepo(RepoContext ctx) throws MergeConflictException, public void updateRepo(RepoContext ctx) throws MergeConflictException,
InvalidChangeOperationException, RestApiException, IOException, InvalidChangeOperationException, RestApiException, IOException,
@@ -153,10 +159,11 @@ public class RebaseChangeOp extends BatchUpdate.Op {
.setDraft(originalPatchSet.isDraft()) .setDraft(originalPatchSet.isDraft())
.setNotify(NotifyHandling.NONE) .setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(fireRevisionCreated) .setFireRevisionCreated(fireRevisionCreated)
.setCopyApprovals(copyApprovals) .setCopyApprovals(copyApprovals);
.setMessage( if (postMessage) {
"Patch Set " + rebasedPatchSetId.get() patchSetInserter.setMessage("Patch Set " + rebasedPatchSetId.get()
+ ": Patch Set " + originalPatchSet.getId().get() + " was rebased"); + ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
}
if (base != null) { if (base != null) {
patchSetInserter.setGroups(base.patchSet().getGroups()); 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; package com.google.gerrit.server.git.strategy;
import com.google.gerrit.extensions.restapi.MergeConflictException; public class RebaseIfNecessary extends RebaseSubmitStrategy {
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 {
RebaseIfNecessary(SubmitStrategy.Arguments args) { RebaseIfNecessary(SubmitStrategy.Arguments args) {
super(args); super(args, false);
}
@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);
} }
} }

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); return MergeIfNecessary.dryRun(args, tipCommit, toMergeCommit);
case REBASE_IF_NECESSARY: case REBASE_IF_NECESSARY:
return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit); return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit);
case REBASE_ALWAYS:
return RebaseAlways.dryRun(args, tipCommit, toMergeCommit);
default: default:
String errorMsg = "No submit strategy for: " + submitType; String errorMsg = "No submit strategy for: " + submitType;
log.error(errorMsg); log.error(errorMsg);

View File

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

View File

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

View File

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