Merge changes from topic 'patch-set-groups-2'

* changes:
  Convert GetRelated to use patch set groups where present
  Add patch set group field to secondary index
  Add a "groups" field to PatchSet
  Add a helper for assigning commits to groups heuristically
  Add a helper to sort ChangeDatas in RevWalk order
This commit is contained in:
Dave Borowitz
2015-05-26 17:41:56 +00:00
committed by Gerrit Code Review
21 changed files with 1957 additions and 199 deletions

View File

@@ -31,9 +31,11 @@ import com.google.gerrit.server.change.GetRelated.RelatedInfo;
import com.google.gerrit.server.edit.ChangeEditModifier;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
@@ -42,6 +44,20 @@ import java.io.IOException;
import java.util.List;
public class GetRelatedIT extends AbstractDaemonTest {
@ConfigSuite.Default
public static Config byGroup() {
Config cfg = new Config();
cfg.setBoolean("change", null, "getRelatedByAncestors", false);
return cfg;
}
@ConfigSuite.Config
public static Config byAncestors() {
Config cfg = new Config();
cfg.setBoolean("change", null, "getRelatedByAncestors", true);
return cfg;
}
@Inject
private ChangeEditUtil editUtil;

View File

@@ -20,6 +20,8 @@ import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
/** A single revision of a {@link Change}. */
public final class PatchSet {
@@ -28,6 +30,41 @@ public final class PatchSet {
return Id.fromRef(name) != null;
}
public static String joinGroups(Iterable<String> groups) {
if (groups == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String g : groups) {
if (!first) {
sb.append(',');
} else {
first = false;
}
sb.append(g);
}
return sb.toString();
}
public static List<String> splitGroups(String joinedGroups) {
if (joinedGroups == null) {
return null;
}
List<String> groups = new ArrayList<>();
int i = 0;
while (true) {
int idx = joinedGroups.indexOf(',', i);
if (idx < 0) {
groups.add(joinedGroups.substring(i, joinedGroups.length()));
break;
}
groups.add(joinedGroups.substring(i, idx));
i = idx + 1;
}
return groups;
}
public static class Id extends IntKey<Change.Id> {
private static final long serialVersionUID = 1L;
@@ -140,6 +177,18 @@ public final class PatchSet {
@Column(id = 5)
protected boolean draft;
/**
* Opaque group identifier, usually assigned during creation.
* <p>
* This field is actually a comma-separated list of values, as in rare cases
* involving merge commits a patch set may belong to multiple groups.
* <p>
* Changes on the same branch having patch sets with intersecting groups are
* considered related, as in the "Related Changes" tab.
*/
@Column(id = 6, notNull = false)
protected String groups;
protected PatchSet() {
}
@@ -187,6 +236,14 @@ public final class PatchSet {
draft = draftStatus;
}
public List<String> getGroups() {
return splitGroups(groups);
}
public void setGroups(Iterable<String> groups) {
this.groups = joinGroups(groups);
}
public String getRefName() {
return id.toRefName();
}

View File

@@ -15,6 +15,10 @@
package com.google.gerrit.reviewdb.client;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.reviewdb.client.PatchSet.joinGroups;
import static com.google.gerrit.reviewdb.client.PatchSet.splitGroups;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
@@ -59,6 +63,26 @@ public class PatchSetTest {
assertNotRef("refs/changes/34/1234foo");
}
@Test
public void testSplitGroups() {
assertThat(splitGroups(null)).isNull();
assertThat(splitGroups("")).containsExactly("");
assertThat(splitGroups("abcd")).containsExactly("abcd");
assertThat(splitGroups("ab,cd")).containsExactly("ab", "cd").inOrder();
assertThat(splitGroups("ab,")).containsExactly("ab", "").inOrder();
assertThat(splitGroups(",cd")).containsExactly("", "cd").inOrder();
}
@Test
public void testJoinGroups() {
assertThat(joinGroups(null)).isNull();
assertThat(joinGroups(ImmutableList.of(""))).isEqualTo("");
assertThat(joinGroups(ImmutableList.of("abcd"))).isEqualTo("abcd");
assertThat(joinGroups(ImmutableList.of("ab", "cd"))).isEqualTo("ab,cd");
assertThat(joinGroups(ImmutableList.of("ab", ""))).isEqualTo("ab,");
assertThat(joinGroups(ImmutableList.of("", "cd"))).isEqualTo(",cd");
}
private static void assertRef(int changeId, int psId, String refName) {
assertThat(PatchSet.isRef(refName)).isTrue();
assertThat(PatchSet.Id.fromRef(refName))

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.CreateChangeSender;
@@ -163,6 +164,11 @@ public class ChangeInserter {
return this;
}
public ChangeInserter setGroups(Iterable<String> groups) {
patchSet.setGroups(groups);
return this;
}
public ChangeInserter setHashtags(Set<String> hashtags) {
this.hashtags = hashtags;
return this;
@@ -205,6 +211,9 @@ public class ChangeInserter {
db.changes().beginTransaction(change.getId());
try {
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
if (patchSet.getGroups() == null) {
patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
}
db.patchSets().insert(Collections.singleton(patchSet));
db.changes().insert(Collections.singleton(change));
LabelTypes labelTypes = projectControl.getLabelTypes();

View File

@@ -157,6 +157,7 @@ public class CreateChange implements
try (Repository git = gitManager.openRepository(project);
RevWalk rw = new RevWalk(git)) {
ObjectId parentCommit;
List<String> groups;
if (input.baseChange != null) {
List<Change> changes = changeUtil.findChanges(input.baseChange);
if (changes.size() != 1) {
@@ -172,6 +173,7 @@ public class CreateChange implements
new PatchSet.Id(change.getId(),
change.currentPatchSetId().get()));
parentCommit = ObjectId.fromString(ps.getRevision().get());
groups = ps.getGroups();
} else {
Ref destRef = git.getRef(refName);
if (destRef == null) {
@@ -179,6 +181,7 @@ public class CreateChange implements
"Branch %s does not exist.", refName));
}
parentCommit = destRef.getObjectId();
groups = null;
}
RevCommit mergeTip = rw.parseCommit(parentCommit);
@@ -208,6 +211,7 @@ public class CreateChange implements
change.setTopic(input.topic);
ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
ins.setGroups(groups);
ins.insert();
return Response.created(json.format(change.getId()));

View File

@@ -14,258 +14,166 @@
package com.google.gerrit.server.change;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommonConverters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Singleton
public class GetRelated implements RestReadView<RevisionResource> {
private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
private final GitRepositoryManager gitMgr;
private final Provider<ReviewDb> dbProvider;
private final Provider<ReviewDb> db;
private final GetRelatedByAncestors byAncestors;
private final Provider<InternalChangeQuery> queryProvider;
private final Provider<WalkSorter> sorter;
private final IndexCollection indexes;
private final boolean byAncestorsOnly;
@Inject
GetRelated(GitRepositoryManager gitMgr,
Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider) {
this.gitMgr = gitMgr;
this.dbProvider = db;
GetRelated(Provider<ReviewDb> db,
@GerritServerConfig Config cfg,
GetRelatedByAncestors byAncestors,
Provider<InternalChangeQuery> queryProvider,
Provider<WalkSorter> sorter,
IndexCollection indexes) {
this.db = db;
this.byAncestors = byAncestors;
this.queryProvider = queryProvider;
this.sorter = sorter;
this.indexes = indexes;
byAncestorsOnly =
cfg.getBoolean("change", null, "getRelatedByAncestors", false);
}
@Override
public RelatedInfo apply(RevisionResource rsrc)
throws RepositoryNotFoundException, IOException, OrmException {
try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
RevWalk rw = new RevWalk(git)) {
Ref ref = git.getRef(rsrc.getChange().getDest().get());
RelatedInfo info = new RelatedInfo();
info.changes = walk(rsrc, rw, ref);
return info;
List<String> thisPatchSetGroups = GroupCollector.getGroups(rsrc);
if (byAncestorsOnly
|| thisPatchSetGroups == null
|| !indexes.getSearchIndex().getSchema().hasField(ChangeField.GROUP)) {
return byAncestors.getRelated(rsrc);
}
RelatedInfo relatedInfo = new RelatedInfo();
relatedInfo.changes = getRelated(rsrc, thisPatchSetGroups);
return relatedInfo;
}
private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
throws OrmException, IOException {
Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
Map<String, PatchSet> commits = Maps.newHashMap();
for (PatchSet p : patchSets.values()) {
commits.put(p.getRevision().get(), p);
private List<ChangeAndCommit> getRelated(RevisionResource rsrc,
List<String> thisPatchSetGroups) throws OrmException, IOException {
if (thisPatchSetGroups.isEmpty()) {
return Collections.emptyList();
}
RevCommit rev = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
rw.sort(RevSort.TOPO);
rw.markStart(rev);
List<ChangeData> cds = queryProvider.get()
.enforceVisibility(true)
.byProjectGroups(
rsrc.getChange().getProject(),
getAllGroups(rsrc.getChange().getId()));
List<ChangeAndCommit> result = new ArrayList<>(cds.size());
if (ref != null && ref.getObjectId() != null) {
try {
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
} catch (IncorrectObjectTypeException notCommit) {
// Ignore and treat as new branch.
PatchSet.Id editBaseId = rsrc.getEdit().isPresent()
? rsrc.getEdit().get().getBasePatchSet().getId()
: null;
for (PatchSetData d : sorter.get()
.includePatchSets(choosePatchSets(thisPatchSetGroups, cds))
.setRetainBody(true)
.sort(cds)) {
PatchSet ps = d.patchSet();
RevCommit commit;
if (ps.getId().equals(editBaseId)) {
// Replace base of an edit with the edit itself.
ps = rsrc.getPatchSet();
commit = rsrc.getEdit().get().getEditCommit();
} else {
commit = d.commit();
}
result.add(new ChangeAndCommit(d.data().change(), ps, commit));
}
Set<Change.Id> added = Sets.newHashSet();
List<ChangeAndCommit> parents = Lists.newArrayList();
for (RevCommit c; (c = rw.next()) != null;) {
PatchSet p = commits.get(c.name());
Change g = null;
if (p != null) {
g = changes.get(p.getId().getParentKey()).change();
added.add(p.getId().getParentKey());
}
parents.add(new ChangeAndCommit(g, p, c));
}
List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets, added);
list.addAll(parents);
if (list.size() == 1) {
ChangeAndCommit r = list.get(0);
if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
if (result.size() == 1) {
ChangeAndCommit r = result.get(0);
if (r.commit != null
&& r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
return Collections.emptyList();
}
}
return list;
return result;
}
private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
throws OrmException {
return ChangeData.asMap(
queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
private Set<String> getAllGroups(Change.Id changeId) throws OrmException {
Set<String> result = new HashSet<>();
for (PatchSet ps : db.get().patchSets().byChange(changeId)) {
List<String> groups = ps.getGroups();
if (groups != null) {
result.addAll(groups);
}
}
return result;
}
private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
Collection<ChangeData> cds) throws OrmException {
Map<PatchSet.Id, PatchSet> r =
Maps.newHashMapWithExpectedSize(cds.size() * 2);
private static Set<PatchSet.Id> choosePatchSets(List<String> groups,
List<ChangeData> cds) throws OrmException {
// Prefer the latest patch set matching at least one group from this
// revision; otherwise, just use the latest patch set overall.
Set<PatchSet.Id> result = new HashSet<>();
for (ChangeData cd : cds) {
for (PatchSet p : cd.patchSets()) {
r.put(p.getId(), p);
Collection<PatchSet> patchSets = cd.patchSets();
List<PatchSet> sameGroup = new ArrayList<>(patchSets.size());
for (PatchSet ps : patchSets) {
if (hasAnyGroup(ps, groups)) {
sameGroup.add(ps);
}
}
result.add(ChangeUtil.PS_ID_ORDER.max(
!sameGroup.isEmpty() ? sameGroup : patchSets).getId());
}
if (rsrc.getEdit().isPresent()) {
r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
}
return r;
return result;
}
private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
Set<Change.Id> added)
throws OrmException, IOException {
// children is a map of parent commit name to PatchSet built on it.
Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
RevFlag seenCommit = rw.newFlag("seenCommit");
LinkedList<String> q = Lists.newLinkedList();
seedQueue(rsrc, rw, seenCommit, patchSets, q);
ProjectControl projectCtl = rsrc.getControl().getProjectControl();
Set<Change.Id> seenChange = Sets.newHashSet();
List<ChangeAndCommit> graph = Lists.newArrayList();
while (!q.isEmpty()) {
String id = q.remove();
// For every matching change find the most recent patch set.
Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
for (PatchSet.Id psId : children.get(id)) {
PatchSet.Id e = matches.get(psId.getParentKey());
if ((e == null || e.get() < psId.get())
&& isVisible(projectCtl, changes, patchSets, psId)) {
matches.put(psId.getParentKey(), psId);
}
}
for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
ChangeData cd = changes.get(e.getKey());
PatchSet ps = patchSets.get(e.getValue());
if (cd == null || ps == null || !seenChange.add(e.getKey())) {
continue;
}
RevCommit c = rw.parseCommit(ObjectId.fromString(
ps.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.addFirst(ps.getRevision().get());
if (added.add(ps.getId().getParentKey())) {
rw.parseBody(c);
graph.add(new ChangeAndCommit(cd.change(), ps, c));
}
}
}
private static boolean hasAnyGroup(PatchSet ps, List<String> groups) {
if (ps.getGroups() == null) {
return false;
}
Collections.reverse(graph);
return graph;
}
private boolean isVisible(ProjectControl projectCtl,
Map<Change.Id, ChangeData> changes,
Map<PatchSet.Id, PatchSet> patchSets,
PatchSet.Id psId) throws OrmException {
ChangeData cd = changes.get(psId.getParentKey());
PatchSet ps = patchSets.get(psId);
if (cd != null && ps != null) {
// Related changes are in the same project, so reuse the existing
// ProjectControl.
ChangeControl ctl = projectCtl.controlFor(cd.change());
return ctl.isVisible(dbProvider.get())
&& ctl.isPatchVisible(ps, dbProvider.get());
// Expected size of each list is 1, so nested linear search is fine.
for (String g1 : ps.getGroups()) {
for (String g2 : groups) {
if (g1.equals(g2)) {
return true;
}
}
}
return false;
}
private void seedQueue(RevisionResource rsrc, RevWalk rw,
RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
LinkedList<String> q) throws IOException {
RevCommit tip = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
tip.add(seenCommit);
q.add(tip.name());
Change.Id cId = rsrc.getChange().getId();
for (PatchSet p : patchSets.values()) {
if (cId.equals(p.getId().getParentKey())) {
try {
RevCommit c = rw.parseCommit(ObjectId.fromString(
p.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.add(c.name());
}
} catch (IOException e) {
log.warn(String.format(
"Cannot read patch set %d of %d",
p.getPatchSetId(), cId.get()), e);
}
}
}
}
private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
throws OrmException {
ReviewDb db = dbProvider.get();
List<ResultSet<PatchSetAncestor>> t =
Lists.newArrayListWithCapacity(ids.size());
for (Change.Id id : ids) {
t.add(db.patchSetAncestors().byChange(id));
}
Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
for (ResultSet<PatchSetAncestor> rs : t) {
for (PatchSetAncestor a : rs) {
r.put(a.getAncestorRevision().get(), a.getPatchSet());
}
}
return r;
}
public static class RelatedInfo {
public List<ChangeAndCommit> changes;
}
@@ -300,5 +208,28 @@ public class GetRelated implements RestReadView<RevisionResource> {
commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
commit.subject = c.getShortMessage();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("changeId", changeId)
.add("commit", toString(commit))
.add("_changeNumber", _changeNumber)
.add("_revisionNumber", _revisionNumber)
.add("_currentRevisionNumber", _currentRevisionNumber)
.toString();
}
private static String toString(CommitInfo commit) {
return MoreObjects.toStringHelper(commit)
.add("commit", commit.commit)
.add("parent", commit.parents)
.add("author", commit.author)
.add("committer", commit.committer)
.add("subject", commit.subject)
.add("message", commit.message)
.add("webLinks", commit.webLinks)
.toString();
}
}
}

View File

@@ -0,0 +1,266 @@
// 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.change;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
import com.google.gerrit.server.change.GetRelated.RelatedInfo;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Implementation of {@link GetRelated} using {@link PatchSetAncestor}s. */
class GetRelatedByAncestors {
private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
private final GitRepositoryManager gitMgr;
private final Provider<ReviewDb> dbProvider;
private final Provider<InternalChangeQuery> queryProvider;
@Inject
GetRelatedByAncestors(GitRepositoryManager gitMgr,
Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider) {
this.gitMgr = gitMgr;
this.dbProvider = db;
this.queryProvider = queryProvider;
}
public RelatedInfo getRelated(RevisionResource rsrc)
throws RepositoryNotFoundException, IOException, OrmException {
try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
RevWalk rw = new RevWalk(git)) {
Ref ref = git.getRef(rsrc.getChange().getDest().get());
RelatedInfo info = new RelatedInfo();
info.changes = walk(rsrc, rw, ref);
return info;
}
}
private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
throws OrmException, IOException {
Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
Map<String, PatchSet> commits = Maps.newHashMap();
for (PatchSet p : patchSets.values()) {
commits.put(p.getRevision().get(), p);
}
RevCommit rev = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
rw.sort(RevSort.TOPO);
rw.markStart(rev);
if (ref != null && ref.getObjectId() != null) {
try {
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
} catch (IncorrectObjectTypeException notCommit) {
// Ignore and treat as new branch.
}
}
Set<Change.Id> added = Sets.newHashSet();
List<ChangeAndCommit> parents = Lists.newArrayList();
for (RevCommit c; (c = rw.next()) != null;) {
PatchSet p = commits.get(c.name());
Change g = null;
if (p != null) {
g = changes.get(p.getId().getParentKey()).change();
added.add(p.getId().getParentKey());
}
parents.add(new ChangeAndCommit(g, p, c));
}
List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets, added);
list.addAll(parents);
if (list.size() == 1) {
ChangeAndCommit r = list.get(0);
if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
return Collections.emptyList();
}
}
return list;
}
private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
throws OrmException {
return ChangeData.asMap(
queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
}
private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
Collection<ChangeData> cds) throws OrmException {
Map<PatchSet.Id, PatchSet> r =
Maps.newHashMapWithExpectedSize(cds.size() * 2);
for (ChangeData cd : cds) {
for (PatchSet p : cd.patchSets()) {
r.put(p.getId(), p);
}
}
if (rsrc.getEdit().isPresent()) {
r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
}
return r;
}
private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
Set<Change.Id> added)
throws OrmException, IOException {
// children is a map of parent commit name to PatchSet built on it.
Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
RevFlag seenCommit = rw.newFlag("seenCommit");
LinkedList<String> q = Lists.newLinkedList();
seedQueue(rsrc, rw, seenCommit, patchSets, q);
ProjectControl projectCtl = rsrc.getControl().getProjectControl();
Set<Change.Id> seenChange = Sets.newHashSet();
List<ChangeAndCommit> graph = Lists.newArrayList();
while (!q.isEmpty()) {
String id = q.remove();
// For every matching change find the most recent patch set.
Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
for (PatchSet.Id psId : children.get(id)) {
PatchSet.Id e = matches.get(psId.getParentKey());
if ((e == null || e.get() < psId.get())
&& isVisible(projectCtl, changes, patchSets, psId)) {
matches.put(psId.getParentKey(), psId);
}
}
for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
ChangeData cd = changes.get(e.getKey());
PatchSet ps = patchSets.get(e.getValue());
if (cd == null || ps == null || !seenChange.add(e.getKey())) {
continue;
}
RevCommit c = rw.parseCommit(ObjectId.fromString(
ps.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.addFirst(ps.getRevision().get());
if (added.add(ps.getId().getParentKey())) {
rw.parseBody(c);
graph.add(new ChangeAndCommit(cd.change(), ps, c));
}
}
}
}
Collections.reverse(graph);
return graph;
}
private boolean isVisible(ProjectControl projectCtl,
Map<Change.Id, ChangeData> changes,
Map<PatchSet.Id, PatchSet> patchSets,
PatchSet.Id psId) throws OrmException {
ChangeData cd = changes.get(psId.getParentKey());
PatchSet ps = patchSets.get(psId);
if (cd != null && ps != null) {
// Related changes are in the same project, so reuse the existing
// ProjectControl.
ChangeControl ctl = projectCtl.controlFor(cd.change());
return ctl.isVisible(dbProvider.get())
&& ctl.isPatchVisible(ps, dbProvider.get());
}
return false;
}
private void seedQueue(RevisionResource rsrc, RevWalk rw,
RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
LinkedList<String> q) throws IOException {
RevCommit tip = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
tip.add(seenCommit);
q.add(tip.name());
Change.Id cId = rsrc.getChange().getId();
for (PatchSet p : patchSets.values()) {
if (cId.equals(p.getId().getParentKey())) {
try {
RevCommit c = rw.parseCommit(ObjectId.fromString(
p.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.add(c.name());
}
} catch (IOException e) {
log.warn(String.format(
"Cannot read patch set %d of %d",
p.getPatchSetId(), cId.get()), e);
}
}
}
}
private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
throws OrmException {
ReviewDb db = dbProvider.get();
List<ResultSet<PatchSetAncestor>> t =
Lists.newArrayListWithCapacity(ids.size());
for (Change.Id id : ids) {
t.add(db.patchSetAncestors().byChange(id));
}
Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
for (ResultSet<PatchSetAncestor> rs : t) {
for (PatchSetAncestor a : rs) {
r.put(a.getAncestorRevision().get(), a.getPatchSet());
}
}
return r;
}
}

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.index.ChangeIndexer;
@@ -108,6 +109,7 @@ public class PatchSetInserter {
private SshInfo sshInfo;
private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
private boolean draft;
private Iterable<String> groups;
private boolean runHooks;
private boolean sendMail;
private Account.Id uploader;
@@ -200,6 +202,11 @@ public class PatchSetInserter {
return this;
}
public PatchSetInserter setGroups(Iterable<String> groups) {
this.groups = groups;
return this;
}
public PatchSetInserter setRunHooks(boolean runHooks) {
this.runHooks = runHooks;
return this;
@@ -239,12 +246,18 @@ public class PatchSetInserter {
db.changes().beginTransaction(c.getId());
try {
if (!db.changes().get(c.getId()).getStatus().isOpen()) {
updatedChange = db.changes().get(c.getId());
if (!updatedChange.getStatus().isOpen()) {
throw new InvalidChangeOperationException(String.format(
"Change %s is closed", c.getId()));
}
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
if (groups != null) {
patchSet.setGroups(groups);
} else {
patchSet.setGroups(GroupCollector.getCurrentGroups(db, c));
}
db.patchSets().insert(Collections.singleton(patchSet));
SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail

View File

@@ -0,0 +1,203 @@
// 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.change;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Helper to sort {@link ChangeData}s based on {@link RevWalk} ordering.
* <p>
* Split changes by project, and map each change to a single commit based on the
* latest patch set. The set of patch sets considered may be limited by calling
* {@link #includePatchSets(Set)}. Perform a standard {@link RevWalk} on each
* project repository, and record the order in which each change's commit is
* seen.
* <p>
* Once an order within each project is determined, groups of changes are sorted
* based on the project name. This is slightly more stable than sorting on
* something like the commit or change timestamp, as it will not unexpectedly
* reorder large groups of changes on subsequent calls if one of the changes was
* updated.
*/
class WalkSorter {
private static final Logger log =
LoggerFactory.getLogger(WalkSorter.class);
private static final Ordering<List<PatchSetData>> PROJECT_LIST_SORTER =
Ordering.natural().nullsFirst()
.onResultOf(
new Function<List<PatchSetData>, Project.NameKey>() {
@Override
public Project.NameKey apply(List<PatchSetData> in) {
if (in == null || in.isEmpty()) {
return null;
}
try {
return in.get(0).data().change().getProject();
} catch (OrmException e) {
throw new IllegalStateException(e);
}
}
});
private final GitRepositoryManager repoManager;
private final Set<PatchSet.Id> includePatchSets;
private boolean retainBody;
@Inject
WalkSorter(GitRepositoryManager repoManager) {
this.repoManager = repoManager;
includePatchSets = new HashSet<>();
}
public WalkSorter includePatchSets(Iterable<PatchSet.Id> patchSets) {
Iterables.addAll(includePatchSets, patchSets);
return this;
}
public WalkSorter setRetainBody(boolean retainBody) {
this.retainBody = retainBody;
return this;
}
public Iterable<PatchSetData> sort(Iterable<ChangeData> in)
throws OrmException, IOException {
Multimap<Project.NameKey, ChangeData> byProject =
ArrayListMultimap.create();
for (ChangeData cd : in) {
byProject.put(cd.change().getProject(), cd);
}
List<List<PatchSetData>> sortedByProject =
new ArrayList<>(byProject.keySet().size());
for (Map.Entry<Project.NameKey, Collection<ChangeData>> e
: byProject.asMap().entrySet()) {
sortedByProject.add(sortProject(e.getKey(), e.getValue()));
}
Collections.sort(sortedByProject, PROJECT_LIST_SORTER);
return Iterables.concat(sortedByProject);
}
private List<PatchSetData> sortProject(Project.NameKey project,
Collection<ChangeData> in) throws OrmException, IOException {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
rw.setRetainBody(retainBody);
rw.sort(RevSort.TOPO);
Multimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in);
if (byCommit.isEmpty()) {
return ImmutableList.of();
}
// Walk from all patch set SHA-1s, and terminate as soon as we've found
// everything we're looking for. This is equivalent to just sorting the
// list of commits by the RevWalk's configured order.
for (RevCommit c : byCommit.keySet()) {
rw.markStart(c);
}
int expected = byCommit.keySet().size();
int found = 0;
RevCommit c;
List<PatchSetData> result = new ArrayList<>(expected);
while (found < expected && (c = rw.next()) != null) {
Collection<PatchSetData> psds = byCommit.get(c);
if (!psds.isEmpty()) {
found++;
for (PatchSetData psd : psds) {
result.add(psd);
}
}
}
return result;
}
}
private Multimap<RevCommit, PatchSetData> byCommit(RevWalk rw,
Collection<ChangeData> in) throws OrmException, IOException {
Multimap<RevCommit, PatchSetData> byCommit =
ArrayListMultimap.create(in.size(), 1);
for (ChangeData cd : in) {
PatchSet maxPs = null;
for (PatchSet ps : cd.patchSets()) {
if (shouldInclude(ps)
&& (maxPs == null || ps.getId().get() > maxPs.getId().get())) {
maxPs = ps;
}
}
if (maxPs == null) {
continue; // No patch sets matched.
}
ObjectId id = ObjectId.fromString(maxPs.getRevision().get());
try {
RevCommit c = rw.parseCommit(id);
byCommit.put(c, PatchSetData.create(cd, maxPs, c));
} catch (MissingObjectException | IncorrectObjectTypeException e) {
log.warn(
"missing commit " + id.name() + " for patch set " + maxPs.getId(),
e);
}
}
return byCommit;
}
private boolean shouldInclude(PatchSet ps) {
return includePatchSets.isEmpty() || includePatchSets.contains(ps.getId());
}
@AutoValue
static abstract class PatchSetData {
@VisibleForTesting
static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
return new AutoValue_WalkSorter_PatchSetData(cd, ps, commit);
}
abstract ChangeData data();
abstract PatchSet patchSet();
abstract RevCommit commit();
}
}

View File

@@ -0,0 +1,303 @@
// 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.base.Preconditions.checkState;
import static org.eclipse.jgit.revwalk.RevFlag.UNINTERESTING;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Helper for assigning groups to commits during {@link ReceiveCommits}.
* <p>
* For each commit encountered along a walk between the branch tip and the tip
* of the push, the group of a commit is defined as follows:
* <ul>
* <li>If the commit is an existing patch set of a change, the group is read
* from the group field in the corresponding {@link PatchSet} record.</li>
* <li>If all of a commit's parents are merged into the branch, then its group
* is its own SHA-1.</li>
* <li>If the commit has a single parent that is not yet merged into the
* branch, then its group is the same as the parent's group.<li>
* <li>For a merge commit, choose a parent and use that parent's group. If one
* of the parents has a group from a patch set, use that group, otherwise, use
* the group from the first parent. In addition to setting this merge commit's
* group, use the chosen group for all commits that would otherwise use a
* group from the parents that were not chosen.</li>
* <li>If a merge commit has multiple parents whose group comes from separate
* patch sets, concatenate the groups from those parents together. This
* indicates two side branches were pushed separately, followed by the merge.
* <li>
* </ul>
* <p>
* Callers must call {@link #visit(RevCommit)} on all commits between the
* current branch tip and the tip of a push, in reverse topo order (parents
* before children). Once all commits have been visited, call {@link
* #getGroups()} for the result.
*/
public class GroupCollector {
private static final Logger log =
LoggerFactory.getLogger(GroupCollector.class);
public static List<String> getCurrentGroups(ReviewDb db, Change c)
throws OrmException {
PatchSet ps = db.patchSets().get(c.currentPatchSetId());
return ps != null ? ps.getGroups() : null;
}
public static List<String> getDefaultGroups(PatchSet ps) {
return ImmutableList.of(ps.getRevision().get());
}
public static List<String> getGroups(RevisionResource rsrc) {
if (rsrc.getEdit().isPresent()) {
// Groups for an edit are just the base revision's groups, since they have
// the same parent.
return rsrc.getEdit().get().getBasePatchSet().getGroups();
}
return rsrc.getPatchSet().getGroups();
}
private static interface Lookup {
List<String> lookup(PatchSet.Id psId) throws OrmException;
}
private final Multimap<ObjectId, PatchSet.Id> patchSetsBySha;
private final Multimap<ObjectId, String> groups;
private final SetMultimap<String, String> groupAliases;
private final Lookup groupLookup;
private boolean done;
private GroupCollector(
Multimap<ObjectId, PatchSet.Id> patchSetsBySha,
Lookup groupLookup) {
this.patchSetsBySha = patchSetsBySha;
this.groupLookup = groupLookup;
groups = ArrayListMultimap.create();
groupAliases = HashMultimap.create();
}
public GroupCollector(
Multimap<ObjectId, Ref> changeRefsById,
final ReviewDb db) {
this(
Multimaps.transformValues(
changeRefsById,
new Function<Ref, PatchSet.Id>() {
@Override
public PatchSet.Id apply(Ref in) {
return PatchSet.Id.fromRef(in.getName());
}
}),
new Lookup() {
@Override
public List<String> lookup(PatchSet.Id psId) throws OrmException {
PatchSet ps = db.patchSets().get(psId);
return ps != null ? ps.getGroups() : null;
}
});
}
@VisibleForTesting
GroupCollector(
Multimap<ObjectId, PatchSet.Id> patchSetsBySha,
final ListMultimap<PatchSet.Id, String> groupLookup) {
this(
patchSetsBySha,
new Lookup() {
@Override
public List<String> lookup(PatchSet.Id psId) {
List<String> groups = groupLookup.get(psId);
return !groups.isEmpty() ? groups : null;
}
});
}
public void visit(RevCommit c) {
checkState(!done, "visit() called after getGroups()");
Set<RevCommit> interestingParents = getInterestingParents(c);
if (interestingParents.size() == 0) {
// All parents are uninteresting: treat this commit as the root of a new
// group of related changes.
groups.put(c, c.name());
return;
} else if (interestingParents.size() == 1) {
// Only one parent is new in this push. If it is the only parent, just use
// that parent's group. If there are multiple parents, perhaps this commit
// is a merge of a side branch. This commit belongs in that parent's group
// in that case.
groups.putAll(c, groups.get(interestingParents.iterator().next()));
return;
}
// Multiple parents, merging at least two branches containing new commits in
// this push.
Set<String> thisCommitGroups = new TreeSet<>();
Set<String> parentGroupsNewInThisPush =
Sets.newLinkedHashSetWithExpectedSize(interestingParents.size());
for (RevCommit p : interestingParents) {
Collection<String> parentGroups = groups.get(p);
if (parentGroups.isEmpty()) {
throw new IllegalStateException(String.format(
"no group assigned to parent %s of commit %s", p.name(), c.name()));
}
for (String parentGroup : parentGroups) {
if (isGroupFromExistingPatchSet(p, parentGroup)) {
// This parent's group is from an existing patch set, i.e. the parent
// not new in this push. Use this group for the commit.
thisCommitGroups.add(parentGroup);
} else {
// This parent's group is new in this push.
parentGroupsNewInThisPush.add(parentGroup);
}
}
}
Iterable<String> toAlias;
if (thisCommitGroups.isEmpty()) {
// All parent groups were new in this push. Pick the first one and alias
// other parents' groups to this first parent.
String firstParentGroup = parentGroupsNewInThisPush.iterator().next();
thisCommitGroups = ImmutableSet.of(firstParentGroup);
toAlias = Iterables.skip(parentGroupsNewInThisPush, 1);
} else {
// For each parent group that was new in this push, alias it to the actual
// computed group(s) for this commit.
toAlias = parentGroupsNewInThisPush;
}
groups.putAll(c, thisCommitGroups);
for (String pg : toAlias) {
groupAliases.putAll(pg, thisCommitGroups);
}
}
public SetMultimap<ObjectId, String> getGroups() throws OrmException {
done = true;
SetMultimap<ObjectId, String> result = MultimapBuilder
.hashKeys(groups.keySet().size())
.treeSetValues()
.build();
for (Map.Entry<ObjectId, Collection<String>> e
: groups.asMap().entrySet()) {
ObjectId id = e.getKey();
result.putAll(id.copy(), resolveGroups(id, e.getValue()));
}
return result;
}
private Set<RevCommit> getInterestingParents(RevCommit commit) {
Set<RevCommit> result =
Sets.newLinkedHashSetWithExpectedSize(commit.getParentCount());
for (RevCommit p : commit.getParents()) {
if (!p.has(UNINTERESTING)) {
result.add(p);
}
}
return result;
}
private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) {
ObjectId id = parseGroup(commit, group);
return id != null && patchSetsBySha.containsKey(id);
}
private Set<String> resolveGroups(ObjectId forCommit,
Collection<String> candidates) throws OrmException {
Set<String> actual = Sets.newTreeSet();
Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size());
Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size());
Deque<String> todo = new ArrayDeque<>(candidates);
// BFS through all aliases to find groups that are not aliased to anything
// else.
while (!todo.isEmpty()) {
String g = todo.removeFirst();
if (!seen.add(g)) {
continue;
}
Set<String> aliases = groupAliases.get(g);
if (aliases.isEmpty()) {
if (!done.contains(g)) {
Iterables.addAll(actual, resolveGroup(forCommit, g));
done.add(g);
}
} else {
todo.addAll(aliases);
}
}
return actual;
}
private ObjectId parseGroup(ObjectId forCommit, String group) {
try {
return ObjectId.fromString(group);
} catch (IllegalArgumentException e) {
// Shouldn't happen; some sort of corruption or manual tinkering?
log.warn("group for commit {} is not a SHA-1: {}",
forCommit.name(), group);
return null;
}
}
private Iterable<String> resolveGroup(ObjectId forCommit, String group)
throws OrmException {
ObjectId id = parseGroup(forCommit, group);
if (id != null) {
PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
if (psId != null) {
List<String> groups = groupLookup.lookup(psId);
// Group for existing patch set may be missing, e.g. if group has not
// been migrated yet.
if (groups != null && !groups.isEmpty()) {
return groups;
}
}
}
return ImmutableList.of(group);
}
}

View File

@@ -44,6 +44,7 @@ import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
@@ -1467,6 +1468,10 @@ public class ReceiveCommits {
private void selectNewAndReplacedChangesFromMagicBranch() {
newChanges = Lists.newArrayList();
final RevWalk walk = rp.getRevWalk();
Set<ObjectId> existing = changeRefsById().keySet();
GroupCollector groupCollector = new GroupCollector(refsById, db);
walk.reset();
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
@@ -1486,7 +1491,6 @@ public class ReceiveCommits {
magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
}
Set<ObjectId> existing = changeRefsById().keySet();
List<ChangeLookup> pending = Lists.newArrayList();
final Set<Change.Key> newChangeIds = new HashSet<>();
final int maxBatchChanges =
@@ -1496,7 +1500,17 @@ public class ReceiveCommits {
if (c == null) {
break;
}
groupCollector.visit(c);
if (existing.contains(c)) { // Commit is already tracked.
// TODO(dborowitz): Corner case where an existing commit might need a
// new group:
// Let A<-B<-C, then:
// 1. Push A to refs/heads/master
// 2. Push B to refs/for/master
// 3. Force push A~ to refs/heads/master
// 4. Push C to refs/for/master.
// B will be in existing so we aren't replacing the patch set. It used
// to have its own group, but now needs to to be changed to A's group.
continue;
}
@@ -1605,8 +1619,20 @@ public class ReceiveCommits {
reject(magicBranch.cmd, "edit is not supported for new changes");
return;
}
for (CreateRequest create : newChanges) {
batch.addCommand(create.cmd);
try {
Multimap<ObjectId, String> groups = groupCollector.getGroups();
for (CreateRequest create : newChanges) {
batch.addCommand(create.cmd);
create.groups = groups.get(create.commit);
}
for (ReplaceRequest replace : replaceByChange.values()) {
replace.groups = groups.get(replace.newCommit);
}
} catch (OrmException e) {
log.error("Error collecting groups for changes", e);
reject(magicBranch.cmd, "internal server error");
return;
}
}
@@ -1646,6 +1672,7 @@ public class ReceiveCommits {
final ReceiveCommand cmd;
final ChangeInserter ins;
boolean created;
Collection<String> groups;
CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
throws OrmException {
@@ -1690,7 +1717,7 @@ public class ReceiveCommits {
}
private void insertChange(ReviewDb db) throws OrmException, IOException {
final PatchSet ps = ins.getPatchSet();
final PatchSet ps = ins.setGroups(groups).getPatchSet();
final Account.Id me = currentUser.getAccountId();
final List<FooterLine> footerLines = commit.getFooterLines();
final MailRecipients recipients = new MailRecipients();
@@ -1845,6 +1872,7 @@ public class ReceiveCommits {
String mergedIntoRef;
boolean skip;
private PatchSet.Id priorPatchSet;
Collection<String> groups;
ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
final ReceiveCommand cmd, final boolean checkMergedInto) {
@@ -2020,6 +2048,7 @@ public class ReceiveCommits {
newPatchSet.setCreatedOn(TimeUtil.nowTs());
newPatchSet.setUploader(currentUser.getAccountId());
newPatchSet.setRevision(toRevId(newCommit));
newPatchSet.setGroups(groups);
if (magicBranch != null && magicBranch.draft) {
newPatchSet.setDraft(true);
}
@@ -2124,6 +2153,9 @@ public class ReceiveCommits {
}
ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
if (newPatchSet.getGroups() == null) {
newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
}
db.patchSets().insert(Collections.singleton(newPatchSet));
if (checkMergedInto) {

View File

@@ -27,6 +27,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeConflictException;
import com.google.gerrit.server.git.MergeException;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
@@ -186,6 +187,7 @@ public class CherryPick extends SubmitStrategy {
args.db.changes().beginTransaction(n.change().getId());
try {
insertAncestors(args.db, ps.getId(), newCommit);
ps.setGroups(GroupCollector.getCurrentGroups(args.db, n.change()));
args.db.patchSets().insert(Collections.singleton(ps));
n.change()
.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));

View File

@@ -531,6 +531,24 @@ public class ChangeField {
}
};
/** Opaque group identifiers for this change's patch sets. */
public static final FieldDef<ChangeData, Iterable<String>> GROUP =
new FieldDef.Repeatable<ChangeData, String>(
"group", FieldType.EXACT, false) {
@Override
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
Set<String> r = Sets.newHashSetWithExpectedSize(1);
for (PatchSet ps : input.patchSets()) {
List<String> groups = ps.getGroups();
if (groups != null) {
r.addAll(groups);
}
}
return r;
}
};
public static class PatchSetProtoField
extends FieldDef.Repeatable<ChangeData, byte[]> {
public static final ProtobufCodec<PatchSet> CODEC =

View File

@@ -202,6 +202,36 @@ public class ChangeSchemas {
ChangeField.COMMENTBY,
ChangeField.PATCH_SET);
static final Schema<ChangeData> V18 = schema(
ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
ChangeField.PROJECT,
ChangeField.PROJECTS,
ChangeField.REF,
ChangeField.TOPIC,
ChangeField.UPDATED,
ChangeField.FILE_PART,
ChangeField.PATH,
ChangeField.OWNER,
ChangeField.REVIEWER,
ChangeField.COMMIT,
ChangeField.TR,
ChangeField.LABEL,
ChangeField.REVIEWED,
ChangeField.COMMIT_MESSAGE,
ChangeField.COMMENT,
ChangeField.CHANGE,
ChangeField.APPROVAL,
ChangeField.MERGEABLE,
ChangeField.ADDED,
ChangeField.DELETED,
ChangeField.DELTA,
ChangeField.HASHTAG,
ChangeField.COMMENTBY,
ChangeField.PATCH_SET,
ChangeField.GROUP);
private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
return new Schema<>(ImmutableList.copyOf(fields));
}

View File

@@ -172,12 +172,13 @@ public class ChangeData {
/**
* Create an instance for testing only.
* <p>
* Attempting to lazy load data will fail with NPEs.
* Attempting to lazy load data will fail with NPEs. Callers may consider
* manually setting fields that can be set.
*
* @param id change ID
* @return instance for testing.
*/
static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
public static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
null, null, null, null, null, null, id);
cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));

View File

@@ -0,0 +1,44 @@
// 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.query.change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import java.util.List;
class GroupPredicate extends IndexPredicate<ChangeData> {
GroupPredicate(String group) {
super(ChangeField.GROUP, group);
}
@Override
public boolean match(ChangeData cd) throws OrmException {
for (PatchSet ps : cd.patchSets()) {
List<String> groups = ps.getGroups();
if (groups != null && groups.contains(getValue())) {
return true;
}
}
return false;
}
@Override
public int getCost() {
return 1;
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.query.change;
import static com.google.gerrit.server.query.Predicate.and;
import static com.google.gerrit.server.query.Predicate.or;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
import com.google.gerrit.common.Nullable;
@@ -32,6 +33,8 @@ import com.google.inject.Inject;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.ObjectId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -145,6 +148,15 @@ public class InternalChangeQuery {
return query(commit(AbbreviatedObjectId.fromObjectId(id)));
}
public List<ChangeData> byProjectGroups(Project.NameKey project,
Collection<String> groups) throws OrmException {
List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
for (String g : groups) {
groupPredicates.add(new GroupPredicate(g));
}
return query(and(project(project), or(groupPredicates)));
}
private List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
try {
return qp.queryChanges(p).changes();

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_107> C = Schema_107.class;
public static final Class<Schema_108> C = Schema_108.class;
public static int getBinaryVersion() {
return guessVersion(C);

View File

@@ -0,0 +1,161 @@
// 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.schema;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
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.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class Schema_108 extends SchemaVersion {
private final GitRepositoryManager repoManager;
@Inject
Schema_108(Provider<Schema_107> prior,
GitRepositoryManager repoManager) {
super(prior);
this.repoManager = repoManager;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
ui.message("Listing all changes ...");
SetMultimap<Project.NameKey, Change.Id> openByProject =
getOpenChangesByProject(db);
ui.message("done");
ui.message("Updating groups for open changes ...");
int i = 0;
for (Map.Entry<Project.NameKey, Collection<Change.Id>> e
: openByProject.asMap().entrySet()) {
try (Repository repo = repoManager.openRepository(e.getKey());
RevWalk rw = new RevWalk(repo)) {
updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue());
} catch (IOException err) {
throw new OrmException(err);
}
if (++i % 100 == 0) {
ui.message(" done " + i + " projects ...");
}
}
ui.message("done");
}
private static void updateProjectGroups(ReviewDb db, Repository repo,
RevWalk rw, Set<Change.Id> changes) throws OrmException, IOException {
// Match sorting in ReceiveCommits.
rw.reset();
rw.sort(RevSort.TOPO);
rw.sort(RevSort.REVERSE, true);
RefDatabase refdb = repo.getRefDatabase();
for (Ref ref : refdb.getRefs(Constants.R_HEADS).values()) {
RevCommit c = maybeParseCommit(rw, ref.getObjectId());
if (c != null) {
rw.markUninteresting(c);
}
}
Multimap<ObjectId, Ref> changeRefsBySha = ArrayListMultimap.create();
Multimap<ObjectId, PatchSet.Id> patchSetsBySha = ArrayListMultimap.create();
for (Ref ref : refdb.getRefs(RefNames.REFS_CHANGES).values()) {
ObjectId id = ref.getObjectId();
if (ref.getObjectId() == null) {
continue;
}
id = id.copy();
changeRefsBySha.put(id, ref);
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
if (psId != null && changes.contains(psId.getParentKey())) {
patchSetsBySha.put(id, psId);
RevCommit c = maybeParseCommit(rw, id);
if (c != null) {
rw.markStart(c);
}
}
}
GroupCollector collector = new GroupCollector(changeRefsBySha, db);
RevCommit c;
while ((c = rw.next()) != null) {
collector.visit(c);
}
updateGroups(db, collector, patchSetsBySha);
}
private static void updateGroups(ReviewDb db, GroupCollector collector,
Multimap<ObjectId, PatchSet.Id> patchSetsBySha) throws OrmException {
Map<PatchSet.Id, PatchSet> patchSets =
db.patchSets().toMap(db.patchSets().get(patchSetsBySha.values()));
for (Map.Entry<ObjectId, Collection<String>> e
: collector.getGroups().asMap().entrySet()) {
for (PatchSet.Id psId : patchSetsBySha.get(e.getKey())) {
PatchSet ps = patchSets.get(psId);
if (ps != null) {
ps.setGroups(e.getValue());
}
}
}
db.patchSets().update(patchSets.values());
}
private SetMultimap<Project.NameKey, Change.Id> getOpenChangesByProject(
ReviewDb db) throws OrmException {
SetMultimap<Project.NameKey, Change.Id> openByProject =
HashMultimap.create();
for (Change c : db.changes().all()) {
if (c.getStatus().isOpen()) {
openByProject.put(c.getProject(), c.getId());
}
}
return openByProject;
}
private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id)
throws IOException {
if (id == null) {
return null;
}
RevObject obj = rw.parseAny(id);
return (obj instanceof RevCommit) ? (RevCommit) obj : null;
}
}

View File

@@ -0,0 +1,267 @@
// 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.change;
import static com.google.common.collect.Collections2.permutations;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Account;
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.reviewdb.client.RevId;
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
import com.google.gerrit.testutil.TestChanges;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.StandardKeyEncoder;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class WalkSorterTest {
static {
KeyUtil.setEncoderImpl(new StandardKeyEncoder());
}
private Account.Id userId;
private InMemoryRepositoryManager repoManager;
@Before
public void setUp() {
userId = new Account.Id(1);
repoManager = new InMemoryRepositoryManager();
}
@Test
public void seriesOfChanges() throws Exception {
TestRepository<Repo> p = newRepo("p");
RevCommit c1_1 = p.commit().create();
RevCommit c2_1 = p.commit().parent(c1_1).create();
RevCommit c3_1 = p.commit().parent(c2_1).create();
ChangeData cd1 = newChange(p, c1_1);
ChangeData cd2 = newChange(p, c2_1);
ChangeData cd3 = newChange(p, c3_1);
List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(sorter, changes, ImmutableList.of(
patchSetData(cd3, c3_1),
patchSetData(cd2, c2_1),
patchSetData(cd1, c1_1)));
// Add new patch sets whose commits are in reverse order, so output is in
// reverse order.
RevCommit c3_2 = p.commit().create();
RevCommit c2_2 = p.commit().parent(c3_2).create();
RevCommit c1_2 = p.commit().parent(c2_2).create();
addPatchSet(cd1, c1_2);
addPatchSet(cd2, c2_2);
addPatchSet(cd3, c3_2);
assertSorted(sorter, changes, ImmutableList.of(
patchSetData(cd1, c1_2),
patchSetData(cd2, c2_2),
patchSetData(cd3, c3_2)));
}
@Test
public void seriesOfChangesAtSameTimestamp() throws Exception {
TestRepository<Repo> p = newRepo("p");
RevCommit c1 = p.commit().tick(0).create();
RevCommit c2 = p.commit().tick(0).parent(c1).create();
RevCommit c3 = p.commit().tick(0).parent(c2).create();
RevCommit c4 = p.commit().tick(0).parent(c3).create();
RevWalk rw = p.getRevWalk();
rw.parseCommit(c1);
assertThat(rw.parseCommit(c2).getCommitTime())
.isEqualTo(c1.getCommitTime());
assertThat(rw.parseCommit(c3).getCommitTime())
.isEqualTo(c1.getCommitTime());
assertThat(rw.parseCommit(c4).getCommitTime())
.isEqualTo(c1.getCommitTime());
ChangeData cd1 = newChange(p, c1);
ChangeData cd2 = newChange(p, c2);
ChangeData cd3 = newChange(p, c3);
ChangeData cd4 = newChange(p, c4);
List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(sorter, changes, ImmutableList.of(
patchSetData(cd4, c4),
patchSetData(cd3, c3),
patchSetData(cd2, c2),
patchSetData(cd1, c1)));
}
@Test
public void projectsSortedByName() throws Exception {
TestRepository<Repo> pa = newRepo("a");
TestRepository<Repo> pb = newRepo("b");
RevCommit c1 = pa.commit().create();
RevCommit c2 = pb.commit().create();
RevCommit c3 = pa.commit().parent(c1).create();
RevCommit c4 = pb.commit().parent(c2).create();
ChangeData cd1 = newChange(pa, c1);
ChangeData cd2 = newChange(pb, c2);
ChangeData cd3 = newChange(pa, c3);
ChangeData cd4 = newChange(pb, c4);
assertSorted(
new WalkSorter(repoManager),
ImmutableList.of(cd1, cd2, cd3, cd4),
ImmutableList.of(
patchSetData(cd3, c3),
patchSetData(cd1, c1),
patchSetData(cd4, c4),
patchSetData(cd2, c2)));
}
@Test
public void restrictToPatchSets() throws Exception {
TestRepository<Repo> p = newRepo("p");
RevCommit c1_1 = p.commit().create();
RevCommit c2_1 = p.commit().parent(c1_1).create();
ChangeData cd1 = newChange(p, c1_1);
ChangeData cd2 = newChange(p, c2_1);
// Add new patch sets whose commits are in reverse order.
RevCommit c2_2 = p.commit().create();
RevCommit c1_2 = p.commit().parent(c2_2).create();
addPatchSet(cd1, c1_2);
addPatchSet(cd2, c2_2);
List<ChangeData> changes = ImmutableList.of(cd1, cd2);
WalkSorter sorter = new WalkSorter(repoManager);
assertSorted(sorter, changes, ImmutableList.of(
patchSetData(cd1, c1_2),
patchSetData(cd2, c2_2)));
// If we restrict to PS1 of each change, the sorter uses that commit.
sorter.includePatchSets(ImmutableSet.of(
new PatchSet.Id(cd1.getId(), 1),
new PatchSet.Id(cd2.getId(), 1)));
assertSorted(sorter, changes, ImmutableList.of(
patchSetData(cd2, 1, c2_1),
patchSetData(cd1, 1, c1_1)));
}
@Test
public void restrictToPatchSetsOmittingWholeProject() throws Exception {
TestRepository<Repo> pa = newRepo("a");
TestRepository<Repo> pb = newRepo("b");
RevCommit c1 = pa.commit().create();
RevCommit c2 = pa.commit().create();
ChangeData cd1 = newChange(pa, c1);
ChangeData cd2 = newChange(pb, c2);
List<ChangeData> changes = ImmutableList.of(cd1, cd2);
WalkSorter sorter = new WalkSorter(repoManager)
.includePatchSets(ImmutableSet.of(cd1.currentPatchSet().getId()));
assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd1, c1)));
}
@Test
public void retainBody() throws Exception {
TestRepository<Repo> p = newRepo("p");
RevCommit c = p.commit().message("message").create();
ChangeData cd = newChange(p, c);
List<ChangeData> changes = ImmutableList.of(cd);
RevCommit actual = new WalkSorter(repoManager)
.setRetainBody(true)
.sort(changes)
.iterator().next()
.commit();
assertThat(actual.getRawBuffer()).isNotNull();
assertThat(actual.getShortMessage()).isEqualTo("message");
actual = new WalkSorter(repoManager)
.setRetainBody(false)
.sort(changes)
.iterator().next()
.commit();
assertThat(actual.getRawBuffer()).isNull();
}
private ChangeData newChange(TestRepository<Repo> tr, ObjectId id)
throws Exception {
Project.NameKey project = tr.getRepository().getDescription().getProject();
Change c = TestChanges.newChange(project, userId);
ChangeData cd = ChangeData.createForTest(c.getId(), 1);
cd.setChange(c);
cd.currentPatchSet().setRevision(new RevId(id.name()));
cd.setPatchSets(ImmutableList.of(cd.currentPatchSet()));
return cd;
}
private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
TestChanges.incrementPatchSet(cd.change());
PatchSet ps = new PatchSet(cd.change().currentPatchSetId());
ps.setRevision(new RevId(id.name()));
List<PatchSet> patchSets = new ArrayList<>(cd.patchSets());
patchSets.add(ps);
cd.setPatchSets(patchSets);
return ps;
}
private TestRepository<Repo> newRepo(String name)
throws Exception {
return new TestRepository<>(
repoManager.createRepository(new Project.NameKey(name)));
}
private static PatchSetData patchSetData(ChangeData cd, RevCommit commit)
throws Exception {
return PatchSetData.create(cd, cd.currentPatchSet(), commit);
}
private static PatchSetData patchSetData(ChangeData cd, int psId,
RevCommit commit) throws Exception {
return PatchSetData.create(
cd, cd.patchSet(new PatchSet.Id(cd.getId(), psId)), commit);
}
private static void assertSorted(WalkSorter sorter, List<ChangeData> changes,
List<PatchSetData> expected) throws Exception {
for (List<ChangeData> list : permutations(changes)) {
assertThat(sorter.sort(list))
.containsExactlyElementsIn(expected).inOrder();
}
}
}

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