Add a helper for assigning commits to groups heuristically
Implement a GroupCollector helper that observes all commits processed, e.g. during the main ReceiveCommits RevWalk, and assigns them to groups using a set of heuristics. The general idea is that the group for a commit is the SHA-1 of the rootmost commit of an open change reachable from the tip of the push. This rootmost commit may be a new patch set, in which case the group is that commit's SHA-1; or it may be an existing patch set, in which case the group is that patch set's group. (Note that the patch set's group might be the commit of a merged change--that is ok, and can in fact prove useful to link related changes together even when some have been merged.) When merge commits are involved, we try to choose a single group and use it for all ancestors along all parent paths, such that all not-yet-seen ancestors of a merge end up in the same group. This doesn't always work out, and there are some corner cases where we assign multiple groups to a merge commit as a result. The actual rules for assigning commits to groups are subtle and explained in detail in the GroupCollector javadoc. The tests are intended to be readable and may be more intuitive and illuminating than an explanation of the rules. Change-Id: Ib00dbf1eecaf61a53facb3a77ff1f8b5c0577b26
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
// Copyright (C) 2015 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||
|
||||
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
|
||||
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||||
import org.eclipse.jgit.junit.TestRepository;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevSort;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class GroupCollectorTest {
|
||||
private TestRepository<?> tr;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
tr = new TestRepository<>(
|
||||
new InMemoryRepository(new DfsRepositoryDescription("repo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitWhoseParentIsUninterestingGetsNewGroup() throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(a, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitWhoseParentIsNewPatchSetGetsParentsGroup()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(a).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(b, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(b, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitWhoseParentIsExistingPatchSetGetsParentsGroup()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(a).create();
|
||||
|
||||
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(b, branchTip),
|
||||
patchSets().put(a, psId(1, 1)),
|
||||
groups().put(psId(1, 1), group));
|
||||
|
||||
assertThat(groups).containsEntry(a, group);
|
||||
assertThat(groups).containsEntry(b, group);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commitWhoseParentIsExistingPatchSetWithNoGroup()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(a).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(b, branchTip),
|
||||
patchSets().put(a, psId(1, 1)),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(b, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitAndNewParentsAllGetSameGroup() throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(b, a.name());
|
||||
assertThat(groups).containsEntry(m, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWhereOneParentHasExistingGroup() throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||
|
||||
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets().put(b, psId(1, 1)),
|
||||
groups().put(psId(1, 1), group));
|
||||
|
||||
// Merge commit and other parent get the existing group.
|
||||
assertThat(groups).containsEntry(a, group);
|
||||
assertThat(groups).containsEntry(b, group);
|
||||
assertThat(groups).containsEntry(m, group);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWhereBothParentsHaveDifferentGroups()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||
|
||||
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
String group2 = "1234567812345678123456781234567812345678";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets()
|
||||
.put(a, psId(1, 1))
|
||||
.put(b, psId(2, 1)),
|
||||
groups()
|
||||
.put(psId(1, 1), group1)
|
||||
.put(psId(2, 1), group2));
|
||||
|
||||
assertThat(groups).containsEntry(a, group1);
|
||||
assertThat(groups).containsEntry(b, group2);
|
||||
// Merge commit gets joined group of parents.
|
||||
assertThat(groups.asMap())
|
||||
.containsEntry(m, ImmutableSet.of(group1, group2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitMergesGroupsFromParent() throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||
|
||||
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
String group2a = "1234567812345678123456781234567812345678";
|
||||
String group2b = "ef123456ef123456ef123456ef123456ef123456";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets()
|
||||
.put(a, psId(1, 1))
|
||||
.put(b, psId(2, 1)),
|
||||
groups()
|
||||
.put(psId(1, 1), group1)
|
||||
.put(psId(2, 1), group2a)
|
||||
.put(psId(2, 1), group2b));
|
||||
|
||||
assertThat(groups).containsEntry(a, group1);
|
||||
assertThat(groups.asMap())
|
||||
.containsEntry(b, ImmutableSet.of(group2a, group2b));
|
||||
// Joined parent groups are split and resorted.
|
||||
assertThat(groups.asMap())
|
||||
.containsEntry(m, ImmutableSet.of(group1, group2a, group2b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWithOneUninterestingParentAndOtherParentIsExisting()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
|
||||
|
||||
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets().put(a, psId(1, 1)),
|
||||
groups().put(psId(1, 1), group));
|
||||
|
||||
assertThat(groups).containsEntry(a, group);
|
||||
assertThat(groups).containsEntry(m, group);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWithOneUninterestingParentAndOtherParentIsNew()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(m, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleMergeCommitsInHistoryAllResolveToSameGroup()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit c = tr.commit().parent(branchTip).create();
|
||||
RevCommit m1 = tr.commit().parent(b).parent(c).create();
|
||||
RevCommit m2 = tr.commit().parent(a).parent(m1).create();
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m2, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(b, a.name());
|
||||
assertThat(groups).containsEntry(c, a.name());
|
||||
assertThat(groups).containsEntry(m1, a.name());
|
||||
assertThat(groups).containsEntry(m2, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWithDuplicatedParentGetsParentsGroup()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(a).create();
|
||||
tr.getRevWalk().parseBody(m);
|
||||
assertThat(m.getParentCount()).isEqualTo(2);
|
||||
assertThat(m.getParent(0)).isEqualTo(m.getParent(1));
|
||||
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets(),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(m, a.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeCommitWithOneNewParentAndTwoExistingPatchSets()
|
||||
throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(branchTip).create();
|
||||
RevCommit c = tr.commit().parent(b).create();
|
||||
RevCommit m = tr.commit().parent(a).parent(c).create();
|
||||
|
||||
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||
String group2 = "1234567812345678123456781234567812345678";
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
newWalk(m, branchTip),
|
||||
patchSets()
|
||||
.put(a, psId(1, 1))
|
||||
.put(b, psId(2, 1)),
|
||||
groups()
|
||||
.put(psId(1, 1), group1)
|
||||
.put(psId(2, 1), group2));
|
||||
|
||||
assertThat(groups).containsEntry(a, group1);
|
||||
assertThat(groups).containsEntry(b, group2);
|
||||
assertThat(groups).containsEntry(c, group2);
|
||||
assertThat(groups.asMap())
|
||||
.containsEntry(m, ImmutableSet.of(group1, group2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectGroupsForMultipleTipsInParallel() throws Exception {
|
||||
RevCommit branchTip = tr.commit().create();
|
||||
RevCommit a = tr.commit().parent(branchTip).create();
|
||||
RevCommit b = tr.commit().parent(a).create();
|
||||
RevCommit c = tr.commit().parent(branchTip).create();
|
||||
RevCommit d = tr.commit().parent(c).create();
|
||||
|
||||
RevWalk rw = newWalk(b, branchTip);
|
||||
rw.markStart(rw.parseCommit(d));
|
||||
// Schema upgrade case: all commits are existing patch sets, but none have
|
||||
// groups assigned yet.
|
||||
Multimap<ObjectId, String> groups = collectGroups(
|
||||
rw,
|
||||
patchSets()
|
||||
.put(branchTip, psId(1, 1))
|
||||
.put(a, psId(2, 1))
|
||||
.put(b, psId(3, 1))
|
||||
.put(c, psId(4, 1))
|
||||
.put(d, psId(5, 1)),
|
||||
groups());
|
||||
|
||||
assertThat(groups).containsEntry(a, a.name());
|
||||
assertThat(groups).containsEntry(b, a.name());
|
||||
assertThat(groups).containsEntry(c, c.name());
|
||||
assertThat(groups).containsEntry(d, c.name());
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Tests for octopus merges.
|
||||
|
||||
private static PatchSet.Id psId(int c, int p) {
|
||||
return new PatchSet.Id(new Change.Id(c), p);
|
||||
}
|
||||
|
||||
private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception {
|
||||
// Match RevWalk conditions from ReceiveCommits.
|
||||
RevWalk rw = new RevWalk(tr.getRepository());
|
||||
rw.sort(RevSort.TOPO);
|
||||
rw.sort(RevSort.REVERSE, true);
|
||||
rw.markStart(rw.parseCommit(start));
|
||||
rw.markUninteresting(rw.parseCommit(branchTip));
|
||||
return rw;
|
||||
}
|
||||
|
||||
private static Multimap<ObjectId, String> collectGroups(
|
||||
RevWalk rw,
|
||||
ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha,
|
||||
ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup)
|
||||
throws Exception {
|
||||
GroupCollector gc =
|
||||
new GroupCollector(patchSetsBySha.build(), groupLookup.build());
|
||||
RevCommit c;
|
||||
while ((c = rw.next()) != null) {
|
||||
gc.visit(c);
|
||||
}
|
||||
return gc.getGroups();
|
||||
}
|
||||
|
||||
// Helper methods for constructing various map arguments, to avoid lots of
|
||||
// type specifications.
|
||||
private static ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSets() {
|
||||
return ImmutableMultimap.builder();
|
||||
}
|
||||
|
||||
private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() {
|
||||
return ImmutableListMultimap.builder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user