Add new submit strategy "Rebase Always".

This strategy works the same way as Rebase If Necessary except that it
inserts new patchset to every change even if fast foward if possible,
in which case new patchset's only difference is commit message. Hence,
Rebase Always provides all advantages of Rebase If Necessary.

Adding extra Reviewed-By and Reviewed-On footers as Cherry Pick will
be implemented in the follow up CLs.

Bug: Issue 4452
Change-Id: I8c2514c69c404eebca3469e3f49a51793d98ab95
This commit is contained in:
Andrii Shyshkalov
2016-10-21 15:19:26 +02:00
parent 861123bde0
commit a11e63df5c
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(
@@ -252,8 +254,14 @@ public abstract class AbstractSubmit extends AbstractDaemonTest {
assertThat(actual).containsKey(
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);
} 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