Merge "Merge branch 'stable-2.11'"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 11acf00..0dcba72 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -31,9 +31,11 @@
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.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;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 48623bd..4ec957e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -20,6 +20,8 @@
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 @@
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 @@
@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 @@
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();
}
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 4bf1d81..87e5b88 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -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 @@
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))
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 9bd6697..a9153d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -33,6 +33,7 @@
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 @@
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 @@
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();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 7320c7a..ac5d3c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -157,6 +157,7 @@
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 @@
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 @@
"Branch %s does not exist.", refName));
}
parentCommit = destRef.getObjectId();
+ groups = null;
}
RevCommit mergeTip = rw.parseCommit(parentCommit);
@@ -208,6 +211,7 @@
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()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 7f49192..df93278e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -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 @@
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();
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java
new file mode 100644
index 0000000..119de7e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java
@@ -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;
+ }
+
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 6716162..db9c6d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -35,6 +35,7 @@
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 @@
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 @@
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 @@
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
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
new file mode 100644
index 0000000..0ccf991
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
@@ -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(Iterable)}. 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();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
new file mode 100644
index 0000000..51125b4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
@@ -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);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 3fb515a..464e267 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -44,6 +44,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
}
ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
+ if (newPatchSet.getGroups() == null) {
+ newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
+ }
db.patchSets().insert(Collections.singleton(newPatchSet));
if (checkMergedInto) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 60af3003..2d229a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -27,6 +27,7 @@
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 @@
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()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 9054ba4..ebbc951 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -531,6 +531,24 @@
}
};
+ /** 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 =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index cff654a..b6b702c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -202,6 +202,36 @@
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));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 6c769f7..52856c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -27,6 +27,7 @@
import com.google.inject.Singleton;
import com.google.inject.name.Named;
+import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -79,7 +80,7 @@
public PatchList get(PatchListKey key) throws PatchListNotAvailableException {
try {
return fileCache.get(key);
- } catch (ExecutionException e) {
+ } catch (ExecutionException | LargeObjectException e) {
PatchListLoader.log.warn("Error computing " + key, e);
throw new PatchListNotAvailableException(e.getCause());
}
@@ -104,7 +105,7 @@
if (computeIntraline) {
try {
return intraCache.get(key);
- } catch (ExecutionException e) {
+ } catch (ExecutionException | LargeObjectException e) {
IntraLineLoader.log.warn("Error computing " + key, e);
return new IntraLineDiff(IntraLineDiff.Status.ERROR);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index d94921e..5c023e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -172,12 +172,13 @@
/**
* 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));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index a0a7a3c..e64b53f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.query.change;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
-import static com.google.gerrit.server.query.change.InternalChangeQuery.schema;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
@@ -46,6 +45,7 @@
import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ListChildProjects;
@@ -151,11 +151,11 @@
final GitRepositoryManager repoManager;
final ProjectCache projectCache;
final Provider<ListChildProjects> listChildProjects;
- final IndexCollection indexes;
final SubmitStrategyFactory submitStrategyFactory;
final ConflictsCache conflictsCache;
final TrackingFooters trackingFooters;
final boolean allowsDrafts;
+ final ChangeIndex index;
private final Provider<CurrentUser> self;
@@ -188,9 +188,10 @@
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
- projectCache, listChildProjects, indexes, submitStrategyFactory,
+ projectCache, listChildProjects, submitStrategyFactory,
conflictsCache, trackingFooters,
- cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
+ cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true),
+ indexes != null ? indexes.getSearchIndex() : null);
}
private Arguments(
@@ -212,11 +213,11 @@
GitRepositoryManager repoManager,
ProjectCache projectCache,
Provider<ListChildProjects> listChildProjects,
- IndexCollection indexes,
SubmitStrategyFactory submitStrategyFactory,
ConflictsCache conflictsCache,
TrackingFooters trackingFooters,
- boolean allowsDrafts) {
+ boolean allowsDrafts,
+ ChangeIndex index) {
this.db = db;
this.queryProvider = queryProvider;
this.rewriter = rewriter;
@@ -235,11 +236,11 @@
this.repoManager = repoManager;
this.projectCache = projectCache;
this.listChildProjects = listChildProjects;
- this.indexes = indexes;
this.submitStrategyFactory = submitStrategyFactory;
this.conflictsCache = conflictsCache;
this.trackingFooters = trackingFooters;
this.allowsDrafts = allowsDrafts;
+ this.index = index;
}
Arguments asUser(CurrentUser otherUser) {
@@ -248,8 +249,8 @@
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
- projectCache, listChildProjects, indexes, submitStrategyFactory,
- conflictsCache, trackingFooters, allowsDrafts);
+ projectCache, listChildProjects, submitStrategyFactory,
+ conflictsCache, trackingFooters, allowsDrafts, index);
}
Arguments asUser(Account.Id otherId) {
@@ -284,6 +285,10 @@
throw new QueryParseException(NotSignedInException.MESSAGE, e);
}
}
+
+ Schema<ChangeData> getSchema() {
+ return index != null ? index.getSchema() : null;
+ }
}
private final Arguments args;
@@ -351,8 +356,7 @@
@Operator
public Predicate<ChangeData> comment(String value) {
- ChangeIndex index = args.indexes.getSearchIndex();
- return new CommentPredicate(index, value);
+ return new CommentPredicate(args.index, value);
}
@Operator
@@ -408,7 +412,7 @@
}
if ("mergeable".equalsIgnoreCase(value)) {
- return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
+ return new IsMergeablePredicate(args.getSchema(), args.fillArgs);
}
try {
@@ -471,9 +475,9 @@
@Operator
public Predicate<ChangeData> topic(String name) {
if (name.startsWith("^")) {
- return new RegexTopicPredicate(schema(args.indexes), name);
+ return new RegexTopicPredicate(args.getSchema(), name);
}
- return new TopicPredicate(schema(args.indexes), name);
+ return new TopicPredicate(args.getSchema(), name);
}
@Operator
@@ -563,8 +567,7 @@
@Operator
public Predicate<ChangeData> message(String text) {
- ChangeIndex index = args.indexes.getSearchIndex();
- return new MessagePredicate(index, text);
+ return new MessagePredicate(args.index, text);
}
@Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index e5fc51d..a21c590 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -24,8 +24,7 @@
static Predicate<ChangeData> create(Arguments args, String value) {
Predicate<ChangeData> eqPath =
new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value);
- if (!args.indexes.getSearchIndex().getSchema().getFields().containsKey(
- ChangeField.FILE_PART.getName())) {
+ if (!args.getSchema().hasField(ChangeField.FILE_PART)) {
return eqPath;
}
return Predicate.or(eqPath, new EqualsFilePredicate(value));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
new file mode 100644
index 0000000..235d64e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -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;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 573bc77..15b1d8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -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 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 @@
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();
@@ -153,7 +165,7 @@
}
}
- static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
+ private static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
return index != null ? index.getSchema() : null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index e7359fd..c1be4f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
/** 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);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
new file mode 100644
index 0000000..8cbf119
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -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;
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
new file mode 100644
index 0000000..7b7983c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
@@ -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();
+ }
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java
new file mode 100644
index 0000000..ba0599d
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -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();
+ }
+}