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:
Dave Borowitz
2015-05-12 11:59:40 -07:00
parent a1246e41a4
commit c6853e158b
2 changed files with 622 additions and 0 deletions

View File

@@ -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();
}
}