Merge changes from topic 'patch-set-groups-2'
* changes: Convert GetRelated to use patch set groups where present Add patch set group field to secondary index Add a "groups" field to PatchSet Add a helper for assigning commits to groups heuristically Add a helper to sort ChangeDatas in RevWalk order
This commit is contained in:
@@ -31,9 +31,11 @@ import com.google.gerrit.server.change.GetRelated.RelatedInfo;
|
|||||||
import com.google.gerrit.server.edit.ChangeEditModifier;
|
import com.google.gerrit.server.edit.ChangeEditModifier;
|
||||||
import com.google.gerrit.server.edit.ChangeEditUtil;
|
import com.google.gerrit.server.edit.ChangeEditUtil;
|
||||||
import com.google.gerrit.server.query.change.ChangeData;
|
import com.google.gerrit.server.query.change.ChangeData;
|
||||||
|
import com.google.gerrit.testutil.ConfigSuite;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -42,6 +44,20 @@ import java.io.IOException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class GetRelatedIT extends AbstractDaemonTest {
|
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
|
@Inject
|
||||||
private ChangeEditUtil editUtil;
|
private ChangeEditUtil editUtil;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.google.gwtorm.client.Column;
|
|||||||
import com.google.gwtorm.client.IntKey;
|
import com.google.gwtorm.client.IntKey;
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** A single revision of a {@link Change}. */
|
/** A single revision of a {@link Change}. */
|
||||||
public final class PatchSet {
|
public final class PatchSet {
|
||||||
@@ -28,6 +30,41 @@ public final class PatchSet {
|
|||||||
return Id.fromRef(name) != null;
|
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> {
|
public static class Id extends IntKey<Change.Id> {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@@ -140,6 +177,18 @@ public final class PatchSet {
|
|||||||
@Column(id = 5)
|
@Column(id = 5)
|
||||||
protected boolean draft;
|
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() {
|
protected PatchSet() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +236,14 @@ public final class PatchSet {
|
|||||||
draft = draftStatus;
|
draft = draftStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getGroups() {
|
||||||
|
return splitGroups(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroups(Iterable<String> groups) {
|
||||||
|
this.groups = joinGroups(groups);
|
||||||
|
}
|
||||||
|
|
||||||
public String getRefName() {
|
public String getRefName() {
|
||||||
return id.toRefName();
|
return id.toRefName();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
package com.google.gerrit.reviewdb.client;
|
package com.google.gerrit.reviewdb.client;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
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;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -59,6 +63,26 @@ public class PatchSetTest {
|
|||||||
assertNotRef("refs/changes/34/1234foo");
|
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) {
|
private static void assertRef(int changeId, int psId, String refName) {
|
||||||
assertThat(PatchSet.isRef(refName)).isTrue();
|
assertThat(PatchSet.isRef(refName)).isTrue();
|
||||||
assertThat(PatchSet.Id.fromRef(refName))
|
assertThat(PatchSet.Id.fromRef(refName))
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.google.gerrit.server.ChangeMessagesUtil;
|
|||||||
import com.google.gerrit.server.ChangeUtil;
|
import com.google.gerrit.server.ChangeUtil;
|
||||||
import com.google.gerrit.server.account.AccountCache;
|
import com.google.gerrit.server.account.AccountCache;
|
||||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
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.git.WorkQueue;
|
||||||
import com.google.gerrit.server.index.ChangeIndexer;
|
import com.google.gerrit.server.index.ChangeIndexer;
|
||||||
import com.google.gerrit.server.mail.CreateChangeSender;
|
import com.google.gerrit.server.mail.CreateChangeSender;
|
||||||
@@ -163,6 +164,11 @@ public class ChangeInserter {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChangeInserter setGroups(Iterable<String> groups) {
|
||||||
|
patchSet.setGroups(groups);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ChangeInserter setHashtags(Set<String> hashtags) {
|
public ChangeInserter setHashtags(Set<String> hashtags) {
|
||||||
this.hashtags = hashtags;
|
this.hashtags = hashtags;
|
||||||
return this;
|
return this;
|
||||||
@@ -205,6 +211,9 @@ public class ChangeInserter {
|
|||||||
db.changes().beginTransaction(change.getId());
|
db.changes().beginTransaction(change.getId());
|
||||||
try {
|
try {
|
||||||
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
|
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
|
||||||
|
if (patchSet.getGroups() == null) {
|
||||||
|
patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
|
||||||
|
}
|
||||||
db.patchSets().insert(Collections.singleton(patchSet));
|
db.patchSets().insert(Collections.singleton(patchSet));
|
||||||
db.changes().insert(Collections.singleton(change));
|
db.changes().insert(Collections.singleton(change));
|
||||||
LabelTypes labelTypes = projectControl.getLabelTypes();
|
LabelTypes labelTypes = projectControl.getLabelTypes();
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ public class CreateChange implements
|
|||||||
try (Repository git = gitManager.openRepository(project);
|
try (Repository git = gitManager.openRepository(project);
|
||||||
RevWalk rw = new RevWalk(git)) {
|
RevWalk rw = new RevWalk(git)) {
|
||||||
ObjectId parentCommit;
|
ObjectId parentCommit;
|
||||||
|
List<String> groups;
|
||||||
if (input.baseChange != null) {
|
if (input.baseChange != null) {
|
||||||
List<Change> changes = changeUtil.findChanges(input.baseChange);
|
List<Change> changes = changeUtil.findChanges(input.baseChange);
|
||||||
if (changes.size() != 1) {
|
if (changes.size() != 1) {
|
||||||
@@ -172,6 +173,7 @@ public class CreateChange implements
|
|||||||
new PatchSet.Id(change.getId(),
|
new PatchSet.Id(change.getId(),
|
||||||
change.currentPatchSetId().get()));
|
change.currentPatchSetId().get()));
|
||||||
parentCommit = ObjectId.fromString(ps.getRevision().get());
|
parentCommit = ObjectId.fromString(ps.getRevision().get());
|
||||||
|
groups = ps.getGroups();
|
||||||
} else {
|
} else {
|
||||||
Ref destRef = git.getRef(refName);
|
Ref destRef = git.getRef(refName);
|
||||||
if (destRef == null) {
|
if (destRef == null) {
|
||||||
@@ -179,6 +181,7 @@ public class CreateChange implements
|
|||||||
"Branch %s does not exist.", refName));
|
"Branch %s does not exist.", refName));
|
||||||
}
|
}
|
||||||
parentCommit = destRef.getObjectId();
|
parentCommit = destRef.getObjectId();
|
||||||
|
groups = null;
|
||||||
}
|
}
|
||||||
RevCommit mergeTip = rw.parseCommit(parentCommit);
|
RevCommit mergeTip = rw.parseCommit(parentCommit);
|
||||||
|
|
||||||
@@ -208,6 +211,7 @@ public class CreateChange implements
|
|||||||
|
|
||||||
change.setTopic(input.topic);
|
change.setTopic(input.topic);
|
||||||
ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
|
ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
|
||||||
|
ins.setGroups(groups);
|
||||||
ins.insert();
|
ins.insert();
|
||||||
|
|
||||||
return Response.created(json.format(change.getId()));
|
return Response.created(json.format(change.getId()));
|
||||||
|
|||||||
@@ -14,258 +14,166 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.change;
|
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.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.common.Nullable;
|
||||||
import com.google.gerrit.extensions.common.CommitInfo;
|
import com.google.gerrit.extensions.common.CommitInfo;
|
||||||
import com.google.gerrit.extensions.restapi.RestReadView;
|
import com.google.gerrit.extensions.restapi.RestReadView;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
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.reviewdb.server.ReviewDb;
|
||||||
|
import com.google.gerrit.server.ChangeUtil;
|
||||||
import com.google.gerrit.server.CommonConverters;
|
import com.google.gerrit.server.CommonConverters;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
|
||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.project.ProjectControl;
|
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.ChangeData;
|
||||||
import com.google.gerrit.server.query.change.InternalChangeQuery;
|
import com.google.gerrit.server.query.change.InternalChangeQuery;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.gwtorm.server.ResultSet;
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
|
||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
|
||||||
import org.eclipse.jgit.lib.Repository;
|
|
||||||
import org.eclipse.jgit.revwalk.RevCommit;
|
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.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class GetRelated implements RestReadView<RevisionResource> {
|
public class GetRelated implements RestReadView<RevisionResource> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
|
private final Provider<ReviewDb> db;
|
||||||
|
private final GetRelatedByAncestors byAncestors;
|
||||||
private final GitRepositoryManager gitMgr;
|
|
||||||
private final Provider<ReviewDb> dbProvider;
|
|
||||||
private final Provider<InternalChangeQuery> queryProvider;
|
private final Provider<InternalChangeQuery> queryProvider;
|
||||||
|
private final Provider<WalkSorter> sorter;
|
||||||
|
private final IndexCollection indexes;
|
||||||
|
private final boolean byAncestorsOnly;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GetRelated(GitRepositoryManager gitMgr,
|
GetRelated(Provider<ReviewDb> db,
|
||||||
Provider<ReviewDb> db,
|
@GerritServerConfig Config cfg,
|
||||||
Provider<InternalChangeQuery> queryProvider) {
|
GetRelatedByAncestors byAncestors,
|
||||||
this.gitMgr = gitMgr;
|
Provider<InternalChangeQuery> queryProvider,
|
||||||
this.dbProvider = db;
|
Provider<WalkSorter> sorter,
|
||||||
|
IndexCollection indexes) {
|
||||||
|
this.db = db;
|
||||||
|
this.byAncestors = byAncestors;
|
||||||
this.queryProvider = queryProvider;
|
this.queryProvider = queryProvider;
|
||||||
|
this.sorter = sorter;
|
||||||
|
this.indexes = indexes;
|
||||||
|
byAncestorsOnly =
|
||||||
|
cfg.getBoolean("change", null, "getRelatedByAncestors", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RelatedInfo apply(RevisionResource rsrc)
|
public RelatedInfo apply(RevisionResource rsrc)
|
||||||
throws RepositoryNotFoundException, IOException, OrmException {
|
throws RepositoryNotFoundException, IOException, OrmException {
|
||||||
try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
|
List<String> thisPatchSetGroups = GroupCollector.getGroups(rsrc);
|
||||||
RevWalk rw = new RevWalk(git)) {
|
if (byAncestorsOnly
|
||||||
Ref ref = git.getRef(rsrc.getChange().getDest().get());
|
|| thisPatchSetGroups == null
|
||||||
RelatedInfo info = new RelatedInfo();
|
|| !indexes.getSearchIndex().getSchema().hasField(ChangeField.GROUP)) {
|
||||||
info.changes = walk(rsrc, rw, ref);
|
return byAncestors.getRelated(rsrc);
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
RelatedInfo relatedInfo = new RelatedInfo();
|
||||||
|
relatedInfo.changes = getRelated(rsrc, thisPatchSetGroups);
|
||||||
|
return relatedInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
|
private List<ChangeAndCommit> getRelated(RevisionResource rsrc,
|
||||||
throws OrmException, IOException {
|
List<String> thisPatchSetGroups) throws OrmException, IOException {
|
||||||
Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
|
if (thisPatchSetGroups.isEmpty()) {
|
||||||
Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
|
return Collections.emptyList();
|
||||||
|
|
||||||
Map<String, PatchSet> commits = Maps.newHashMap();
|
|
||||||
for (PatchSet p : patchSets.values()) {
|
|
||||||
commits.put(p.getRevision().get(), p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RevCommit rev = rw.parseCommit(ObjectId.fromString(
|
List<ChangeData> cds = queryProvider.get()
|
||||||
rsrc.getPatchSet().getRevision().get()));
|
.enforceVisibility(true)
|
||||||
rw.sort(RevSort.TOPO);
|
.byProjectGroups(
|
||||||
rw.markStart(rev);
|
rsrc.getChange().getProject(),
|
||||||
|
getAllGroups(rsrc.getChange().getId()));
|
||||||
|
List<ChangeAndCommit> result = new ArrayList<>(cds.size());
|
||||||
|
|
||||||
if (ref != null && ref.getObjectId() != null) {
|
PatchSet.Id editBaseId = rsrc.getEdit().isPresent()
|
||||||
try {
|
? rsrc.getEdit().get().getBasePatchSet().getId()
|
||||||
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
|
: null;
|
||||||
} catch (IncorrectObjectTypeException notCommit) {
|
for (PatchSetData d : sorter.get()
|
||||||
// Ignore and treat as new branch.
|
.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();
|
if (result.size() == 1) {
|
||||||
List<ChangeAndCommit> parents = Lists.newArrayList();
|
ChangeAndCommit r = result.get(0);
|
||||||
for (RevCommit c; (c = rw.next()) != null;) {
|
if (r.commit != null
|
||||||
PatchSet p = commits.get(c.name());
|
&& r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
|
||||||
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 Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
|
private Set<String> getAllGroups(Change.Id changeId) throws OrmException {
|
||||||
throws OrmException {
|
Set<String> result = new HashSet<>();
|
||||||
return ChangeData.asMap(
|
for (PatchSet ps : db.get().patchSets().byChange(changeId)) {
|
||||||
queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
|
List<String> groups = ps.getGroups();
|
||||||
|
if (groups != null) {
|
||||||
|
result.addAll(groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
|
private static Set<PatchSet.Id> choosePatchSets(List<String> groups,
|
||||||
Collection<ChangeData> cds) throws OrmException {
|
List<ChangeData> cds) throws OrmException {
|
||||||
Map<PatchSet.Id, PatchSet> r =
|
// Prefer the latest patch set matching at least one group from this
|
||||||
Maps.newHashMapWithExpectedSize(cds.size() * 2);
|
// revision; otherwise, just use the latest patch set overall.
|
||||||
|
Set<PatchSet.Id> result = new HashSet<>();
|
||||||
for (ChangeData cd : cds) {
|
for (ChangeData cd : cds) {
|
||||||
for (PatchSet p : cd.patchSets()) {
|
Collection<PatchSet> patchSets = cd.patchSets();
|
||||||
r.put(p.getId(), p);
|
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());
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
if (rsrc.getEdit().isPresent()) {
|
|
||||||
r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
|
private static boolean hasAnyGroup(PatchSet ps, List<String> groups) {
|
||||||
Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
|
if (ps.getGroups() == null) {
|
||||||
Set<Change.Id> added)
|
return false;
|
||||||
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);
|
// Expected size of each list is 1, so nested linear search is fine.
|
||||||
return graph;
|
for (String g1 : ps.getGroups()) {
|
||||||
}
|
for (String g2 : groups) {
|
||||||
|
if (g1.equals(g2)) {
|
||||||
private boolean isVisible(ProjectControl projectCtl,
|
return true;
|
||||||
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;
|
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 static class RelatedInfo {
|
||||||
public List<ChangeAndCommit> changes;
|
public List<ChangeAndCommit> changes;
|
||||||
}
|
}
|
||||||
@@ -300,5 +208,28 @@ public class GetRelated implements RestReadView<RevisionResource> {
|
|||||||
commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
|
commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
|
||||||
commit.subject = c.getShortMessage();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.events.CommitReceivedEvent;
|
import com.google.gerrit.server.events.CommitReceivedEvent;
|
||||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||||
import com.google.gerrit.server.git.BanCommit;
|
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.CommitValidationException;
|
||||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||||
import com.google.gerrit.server.index.ChangeIndexer;
|
import com.google.gerrit.server.index.ChangeIndexer;
|
||||||
@@ -108,6 +109,7 @@ public class PatchSetInserter {
|
|||||||
private SshInfo sshInfo;
|
private SshInfo sshInfo;
|
||||||
private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
|
private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
|
||||||
private boolean draft;
|
private boolean draft;
|
||||||
|
private Iterable<String> groups;
|
||||||
private boolean runHooks;
|
private boolean runHooks;
|
||||||
private boolean sendMail;
|
private boolean sendMail;
|
||||||
private Account.Id uploader;
|
private Account.Id uploader;
|
||||||
@@ -200,6 +202,11 @@ public class PatchSetInserter {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PatchSetInserter setGroups(Iterable<String> groups) {
|
||||||
|
this.groups = groups;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public PatchSetInserter setRunHooks(boolean runHooks) {
|
public PatchSetInserter setRunHooks(boolean runHooks) {
|
||||||
this.runHooks = runHooks;
|
this.runHooks = runHooks;
|
||||||
return this;
|
return this;
|
||||||
@@ -239,12 +246,18 @@ public class PatchSetInserter {
|
|||||||
|
|
||||||
db.changes().beginTransaction(c.getId());
|
db.changes().beginTransaction(c.getId());
|
||||||
try {
|
try {
|
||||||
if (!db.changes().get(c.getId()).getStatus().isOpen()) {
|
updatedChange = db.changes().get(c.getId());
|
||||||
|
if (!updatedChange.getStatus().isOpen()) {
|
||||||
throw new InvalidChangeOperationException(String.format(
|
throw new InvalidChangeOperationException(String.format(
|
||||||
"Change %s is closed", c.getId()));
|
"Change %s is closed", c.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
|
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));
|
db.patchSets().insert(Collections.singleton(patchSet));
|
||||||
|
|
||||||
SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail
|
SetMultimap<ReviewerState, Account.Id> oldReviewers = sendMail
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright (C) 2015 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Function;
|
||||||
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import com.google.common.collect.Ordering;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
|
import com.google.gerrit.server.query.change.ChangeData;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||||
|
import org.eclipse.jgit.errors.MissingObjectException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevSort;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to sort {@link ChangeData}s based on {@link RevWalk} ordering.
|
||||||
|
* <p>
|
||||||
|
* Split changes by project, and map each change to a single commit based on the
|
||||||
|
* latest patch set. The set of patch sets considered may be limited by calling
|
||||||
|
* {@link #includePatchSets(Set)}. Perform a standard {@link RevWalk} on each
|
||||||
|
* project repository, and record the order in which each change's commit is
|
||||||
|
* seen.
|
||||||
|
* <p>
|
||||||
|
* Once an order within each project is determined, groups of changes are sorted
|
||||||
|
* based on the project name. This is slightly more stable than sorting on
|
||||||
|
* something like the commit or change timestamp, as it will not unexpectedly
|
||||||
|
* reorder large groups of changes on subsequent calls if one of the changes was
|
||||||
|
* updated.
|
||||||
|
*/
|
||||||
|
class WalkSorter {
|
||||||
|
private static final Logger log =
|
||||||
|
LoggerFactory.getLogger(WalkSorter.class);
|
||||||
|
|
||||||
|
private static final Ordering<List<PatchSetData>> PROJECT_LIST_SORTER =
|
||||||
|
Ordering.natural().nullsFirst()
|
||||||
|
.onResultOf(
|
||||||
|
new Function<List<PatchSetData>, Project.NameKey>() {
|
||||||
|
@Override
|
||||||
|
public Project.NameKey apply(List<PatchSetData> in) {
|
||||||
|
if (in == null || in.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return in.get(0).data().change().getProject();
|
||||||
|
} catch (OrmException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private final GitRepositoryManager repoManager;
|
||||||
|
private final Set<PatchSet.Id> includePatchSets;
|
||||||
|
private boolean retainBody;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WalkSorter(GitRepositoryManager repoManager) {
|
||||||
|
this.repoManager = repoManager;
|
||||||
|
includePatchSets = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalkSorter includePatchSets(Iterable<PatchSet.Id> patchSets) {
|
||||||
|
Iterables.addAll(includePatchSets, patchSets);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalkSorter setRetainBody(boolean retainBody) {
|
||||||
|
this.retainBody = retainBody;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<PatchSetData> sort(Iterable<ChangeData> in)
|
||||||
|
throws OrmException, IOException {
|
||||||
|
Multimap<Project.NameKey, ChangeData> byProject =
|
||||||
|
ArrayListMultimap.create();
|
||||||
|
for (ChangeData cd : in) {
|
||||||
|
byProject.put(cd.change().getProject(), cd);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<PatchSetData>> sortedByProject =
|
||||||
|
new ArrayList<>(byProject.keySet().size());
|
||||||
|
for (Map.Entry<Project.NameKey, Collection<ChangeData>> e
|
||||||
|
: byProject.asMap().entrySet()) {
|
||||||
|
sortedByProject.add(sortProject(e.getKey(), e.getValue()));
|
||||||
|
}
|
||||||
|
Collections.sort(sortedByProject, PROJECT_LIST_SORTER);
|
||||||
|
return Iterables.concat(sortedByProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PatchSetData> sortProject(Project.NameKey project,
|
||||||
|
Collection<ChangeData> in) throws OrmException, IOException {
|
||||||
|
try (Repository repo = repoManager.openRepository(project);
|
||||||
|
RevWalk rw = new RevWalk(repo)) {
|
||||||
|
rw.setRetainBody(retainBody);
|
||||||
|
rw.sort(RevSort.TOPO);
|
||||||
|
Multimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in);
|
||||||
|
if (byCommit.isEmpty()) {
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk from all patch set SHA-1s, and terminate as soon as we've found
|
||||||
|
// everything we're looking for. This is equivalent to just sorting the
|
||||||
|
// list of commits by the RevWalk's configured order.
|
||||||
|
for (RevCommit c : byCommit.keySet()) {
|
||||||
|
rw.markStart(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
int expected = byCommit.keySet().size();
|
||||||
|
int found = 0;
|
||||||
|
RevCommit c;
|
||||||
|
List<PatchSetData> result = new ArrayList<>(expected);
|
||||||
|
while (found < expected && (c = rw.next()) != null) {
|
||||||
|
Collection<PatchSetData> psds = byCommit.get(c);
|
||||||
|
if (!psds.isEmpty()) {
|
||||||
|
found++;
|
||||||
|
for (PatchSetData psd : psds) {
|
||||||
|
result.add(psd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Multimap<RevCommit, PatchSetData> byCommit(RevWalk rw,
|
||||||
|
Collection<ChangeData> in) throws OrmException, IOException {
|
||||||
|
Multimap<RevCommit, PatchSetData> byCommit =
|
||||||
|
ArrayListMultimap.create(in.size(), 1);
|
||||||
|
for (ChangeData cd : in) {
|
||||||
|
PatchSet maxPs = null;
|
||||||
|
for (PatchSet ps : cd.patchSets()) {
|
||||||
|
if (shouldInclude(ps)
|
||||||
|
&& (maxPs == null || ps.getId().get() > maxPs.getId().get())) {
|
||||||
|
maxPs = ps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxPs == null) {
|
||||||
|
continue; // No patch sets matched.
|
||||||
|
}
|
||||||
|
ObjectId id = ObjectId.fromString(maxPs.getRevision().get());
|
||||||
|
try {
|
||||||
|
RevCommit c = rw.parseCommit(id);
|
||||||
|
byCommit.put(c, PatchSetData.create(cd, maxPs, c));
|
||||||
|
} catch (MissingObjectException | IncorrectObjectTypeException e) {
|
||||||
|
log.warn(
|
||||||
|
"missing commit " + id.name() + " for patch set " + maxPs.getId(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byCommit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldInclude(PatchSet ps) {
|
||||||
|
return includePatchSets.isEmpty() || includePatchSets.contains(ps.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
static abstract class PatchSetData {
|
||||||
|
@VisibleForTesting
|
||||||
|
static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
|
||||||
|
return new AutoValue_WalkSorter_PatchSetData(cd, ps, commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract ChangeData data();
|
||||||
|
abstract PatchSet patchSet();
|
||||||
|
abstract RevCommit commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import com.google.common.collect.LinkedListMultimap;
|
|||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
import com.google.common.collect.Ordering;
|
import com.google.common.collect.Ordering;
|
||||||
import com.google.common.collect.SetMultimap;
|
import com.google.common.collect.SetMultimap;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
@@ -1467,6 +1468,10 @@ public class ReceiveCommits {
|
|||||||
private void selectNewAndReplacedChangesFromMagicBranch() {
|
private void selectNewAndReplacedChangesFromMagicBranch() {
|
||||||
newChanges = Lists.newArrayList();
|
newChanges = Lists.newArrayList();
|
||||||
final RevWalk walk = rp.getRevWalk();
|
final RevWalk walk = rp.getRevWalk();
|
||||||
|
|
||||||
|
Set<ObjectId> existing = changeRefsById().keySet();
|
||||||
|
GroupCollector groupCollector = new GroupCollector(refsById, db);
|
||||||
|
|
||||||
walk.reset();
|
walk.reset();
|
||||||
walk.sort(RevSort.TOPO);
|
walk.sort(RevSort.TOPO);
|
||||||
walk.sort(RevSort.REVERSE, true);
|
walk.sort(RevSort.REVERSE, true);
|
||||||
@@ -1486,7 +1491,6 @@ public class ReceiveCommits {
|
|||||||
magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
|
magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<ObjectId> existing = changeRefsById().keySet();
|
|
||||||
List<ChangeLookup> pending = Lists.newArrayList();
|
List<ChangeLookup> pending = Lists.newArrayList();
|
||||||
final Set<Change.Key> newChangeIds = new HashSet<>();
|
final Set<Change.Key> newChangeIds = new HashSet<>();
|
||||||
final int maxBatchChanges =
|
final int maxBatchChanges =
|
||||||
@@ -1496,7 +1500,17 @@ public class ReceiveCommits {
|
|||||||
if (c == null) {
|
if (c == null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
groupCollector.visit(c);
|
||||||
if (existing.contains(c)) { // Commit is already tracked.
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1605,8 +1619,20 @@ public class ReceiveCommits {
|
|||||||
reject(magicBranch.cmd, "edit is not supported for new changes");
|
reject(magicBranch.cmd, "edit is not supported for new changes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (CreateRequest create : newChanges) {
|
|
||||||
batch.addCommand(create.cmd);
|
try {
|
||||||
|
Multimap<ObjectId, String> groups = groupCollector.getGroups();
|
||||||
|
for (CreateRequest create : newChanges) {
|
||||||
|
batch.addCommand(create.cmd);
|
||||||
|
create.groups = groups.get(create.commit);
|
||||||
|
}
|
||||||
|
for (ReplaceRequest replace : replaceByChange.values()) {
|
||||||
|
replace.groups = groups.get(replace.newCommit);
|
||||||
|
}
|
||||||
|
} catch (OrmException e) {
|
||||||
|
log.error("Error collecting groups for changes", e);
|
||||||
|
reject(magicBranch.cmd, "internal server error");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1646,6 +1672,7 @@ public class ReceiveCommits {
|
|||||||
final ReceiveCommand cmd;
|
final ReceiveCommand cmd;
|
||||||
final ChangeInserter ins;
|
final ChangeInserter ins;
|
||||||
boolean created;
|
boolean created;
|
||||||
|
Collection<String> groups;
|
||||||
|
|
||||||
CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
|
CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
|
||||||
throws OrmException {
|
throws OrmException {
|
||||||
@@ -1690,7 +1717,7 @@ public class ReceiveCommits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void insertChange(ReviewDb db) throws OrmException, IOException {
|
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 Account.Id me = currentUser.getAccountId();
|
||||||
final List<FooterLine> footerLines = commit.getFooterLines();
|
final List<FooterLine> footerLines = commit.getFooterLines();
|
||||||
final MailRecipients recipients = new MailRecipients();
|
final MailRecipients recipients = new MailRecipients();
|
||||||
@@ -1845,6 +1872,7 @@ public class ReceiveCommits {
|
|||||||
String mergedIntoRef;
|
String mergedIntoRef;
|
||||||
boolean skip;
|
boolean skip;
|
||||||
private PatchSet.Id priorPatchSet;
|
private PatchSet.Id priorPatchSet;
|
||||||
|
Collection<String> groups;
|
||||||
|
|
||||||
ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
|
ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
|
||||||
final ReceiveCommand cmd, final boolean checkMergedInto) {
|
final ReceiveCommand cmd, final boolean checkMergedInto) {
|
||||||
@@ -2020,6 +2048,7 @@ public class ReceiveCommits {
|
|||||||
newPatchSet.setCreatedOn(TimeUtil.nowTs());
|
newPatchSet.setCreatedOn(TimeUtil.nowTs());
|
||||||
newPatchSet.setUploader(currentUser.getAccountId());
|
newPatchSet.setUploader(currentUser.getAccountId());
|
||||||
newPatchSet.setRevision(toRevId(newCommit));
|
newPatchSet.setRevision(toRevId(newCommit));
|
||||||
|
newPatchSet.setGroups(groups);
|
||||||
if (magicBranch != null && magicBranch.draft) {
|
if (magicBranch != null && magicBranch.draft) {
|
||||||
newPatchSet.setDraft(true);
|
newPatchSet.setDraft(true);
|
||||||
}
|
}
|
||||||
@@ -2124,6 +2153,9 @@ public class ReceiveCommits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
|
ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
|
||||||
|
if (newPatchSet.getGroups() == null) {
|
||||||
|
newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
|
||||||
|
}
|
||||||
db.patchSets().insert(Collections.singleton(newPatchSet));
|
db.patchSets().insert(Collections.singleton(newPatchSet));
|
||||||
|
|
||||||
if (checkMergedInto) {
|
if (checkMergedInto) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.google.gerrit.server.IdentifiedUser;
|
|||||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||||
import com.google.gerrit.server.git.CodeReviewCommit;
|
import com.google.gerrit.server.git.CodeReviewCommit;
|
||||||
import com.google.gerrit.server.git.CommitMergeStatus;
|
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.MergeConflictException;
|
||||||
import com.google.gerrit.server.git.MergeException;
|
import com.google.gerrit.server.git.MergeException;
|
||||||
import com.google.gerrit.server.git.MergeIdenticalTreeException;
|
import com.google.gerrit.server.git.MergeIdenticalTreeException;
|
||||||
@@ -186,6 +187,7 @@ public class CherryPick extends SubmitStrategy {
|
|||||||
args.db.changes().beginTransaction(n.change().getId());
|
args.db.changes().beginTransaction(n.change().getId());
|
||||||
try {
|
try {
|
||||||
insertAncestors(args.db, ps.getId(), newCommit);
|
insertAncestors(args.db, ps.getId(), newCommit);
|
||||||
|
ps.setGroups(GroupCollector.getCurrentGroups(args.db, n.change()));
|
||||||
args.db.patchSets().insert(Collections.singleton(ps));
|
args.db.patchSets().insert(Collections.singleton(ps));
|
||||||
n.change()
|
n.change()
|
||||||
.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
|
.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
|
||||||
|
|||||||
@@ -531,6 +531,24 @@ public class ChangeField {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Opaque group identifiers for this change's patch sets. */
|
||||||
|
public static final FieldDef<ChangeData, Iterable<String>> GROUP =
|
||||||
|
new FieldDef.Repeatable<ChangeData, String>(
|
||||||
|
"group", FieldType.EXACT, false) {
|
||||||
|
@Override
|
||||||
|
public Iterable<String> get(ChangeData input, FillArgs args)
|
||||||
|
throws OrmException {
|
||||||
|
Set<String> r = Sets.newHashSetWithExpectedSize(1);
|
||||||
|
for (PatchSet ps : input.patchSets()) {
|
||||||
|
List<String> groups = ps.getGroups();
|
||||||
|
if (groups != null) {
|
||||||
|
r.addAll(groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static class PatchSetProtoField
|
public static class PatchSetProtoField
|
||||||
extends FieldDef.Repeatable<ChangeData, byte[]> {
|
extends FieldDef.Repeatable<ChangeData, byte[]> {
|
||||||
public static final ProtobufCodec<PatchSet> CODEC =
|
public static final ProtobufCodec<PatchSet> CODEC =
|
||||||
|
|||||||
@@ -202,6 +202,36 @@ public class ChangeSchemas {
|
|||||||
ChangeField.COMMENTBY,
|
ChangeField.COMMENTBY,
|
||||||
ChangeField.PATCH_SET);
|
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) {
|
private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
|
||||||
return new Schema<>(ImmutableList.copyOf(fields));
|
return new Schema<>(ImmutableList.copyOf(fields));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,12 +172,13 @@ public class ChangeData {
|
|||||||
/**
|
/**
|
||||||
* Create an instance for testing only.
|
* Create an instance for testing only.
|
||||||
* <p>
|
* <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
|
* @param id change ID
|
||||||
* @return instance for testing.
|
* @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,
|
ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
|
||||||
null, null, null, null, null, null, id);
|
null, null, null, null, null, null, id);
|
||||||
cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
|
cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package com.google.gerrit.server.query.change;
|
package com.google.gerrit.server.query.change;
|
||||||
|
|
||||||
import static com.google.gerrit.server.query.Predicate.and;
|
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 static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
|
||||||
|
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
@@ -32,6 +33,8 @@ import com.google.inject.Inject;
|
|||||||
import org.eclipse.jgit.lib.AbbreviatedObjectId;
|
import org.eclipse.jgit.lib.AbbreviatedObjectId;
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,6 +148,15 @@ public class InternalChangeQuery {
|
|||||||
return query(commit(AbbreviatedObjectId.fromObjectId(id)));
|
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 {
|
private List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
|
||||||
try {
|
try {
|
||||||
return qp.queryChanges(p).changes();
|
return qp.queryChanges(p).changes();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import java.util.List;
|
|||||||
/** A version of the database schema. */
|
/** A version of the database schema. */
|
||||||
public abstract class SchemaVersion {
|
public abstract class SchemaVersion {
|
||||||
/** The current schema version. */
|
/** 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() {
|
public static int getBinaryVersion() {
|
||||||
return guessVersion(C);
|
return guessVersion(C);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
// Copyright (C) 2015 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package com.google.gerrit.server.git;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableListMultimap;
|
||||||
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.reviewdb.client.PatchSet;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
|
||||||
|
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||||||
|
import org.eclipse.jgit.junit.TestRepository;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevSort;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class GroupCollectorTest {
|
||||||
|
private TestRepository<?> tr;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
tr = new TestRepository<>(
|
||||||
|
new InMemoryRepository(new DfsRepositoryDescription("repo")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commitWhoseParentIsUninterestingGetsNewGroup() throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(a, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commitWhoseParentIsNewPatchSetGetsParentsGroup()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(a).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(b, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(b, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commitWhoseParentIsExistingPatchSetGetsParentsGroup()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(a).create();
|
||||||
|
|
||||||
|
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(b, branchTip),
|
||||||
|
patchSets().put(a, psId(1, 1)),
|
||||||
|
groups().put(psId(1, 1), group));
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, group);
|
||||||
|
assertThat(groups).containsEntry(b, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void commitWhoseParentIsExistingPatchSetWithNoGroup()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(a).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(b, branchTip),
|
||||||
|
patchSets().put(a, psId(1, 1)),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(b, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitAndNewParentsAllGetSameGroup() throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(b, a.name());
|
||||||
|
assertThat(groups).containsEntry(m, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWhereOneParentHasExistingGroup() throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||||
|
|
||||||
|
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets().put(b, psId(1, 1)),
|
||||||
|
groups().put(psId(1, 1), group));
|
||||||
|
|
||||||
|
// Merge commit and other parent get the existing group.
|
||||||
|
assertThat(groups).containsEntry(a, group);
|
||||||
|
assertThat(groups).containsEntry(b, group);
|
||||||
|
assertThat(groups).containsEntry(m, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWhereBothParentsHaveDifferentGroups()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||||
|
|
||||||
|
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
String group2 = "1234567812345678123456781234567812345678";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets()
|
||||||
|
.put(a, psId(1, 1))
|
||||||
|
.put(b, psId(2, 1)),
|
||||||
|
groups()
|
||||||
|
.put(psId(1, 1), group1)
|
||||||
|
.put(psId(2, 1), group2));
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, group1);
|
||||||
|
assertThat(groups).containsEntry(b, group2);
|
||||||
|
// Merge commit gets joined group of parents.
|
||||||
|
assertThat(groups.asMap())
|
||||||
|
.containsEntry(m, ImmutableSet.of(group1, group2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitMergesGroupsFromParent() throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(b).create();
|
||||||
|
|
||||||
|
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
String group2a = "1234567812345678123456781234567812345678";
|
||||||
|
String group2b = "ef123456ef123456ef123456ef123456ef123456";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets()
|
||||||
|
.put(a, psId(1, 1))
|
||||||
|
.put(b, psId(2, 1)),
|
||||||
|
groups()
|
||||||
|
.put(psId(1, 1), group1)
|
||||||
|
.put(psId(2, 1), group2a)
|
||||||
|
.put(psId(2, 1), group2b));
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, group1);
|
||||||
|
assertThat(groups.asMap())
|
||||||
|
.containsEntry(b, ImmutableSet.of(group2a, group2b));
|
||||||
|
// Joined parent groups are split and resorted.
|
||||||
|
assertThat(groups.asMap())
|
||||||
|
.containsEntry(m, ImmutableSet.of(group1, group2a, group2b));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWithOneUninterestingParentAndOtherParentIsExisting()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
|
||||||
|
|
||||||
|
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets().put(a, psId(1, 1)),
|
||||||
|
groups().put(psId(1, 1), group));
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, group);
|
||||||
|
assertThat(groups).containsEntry(m, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWithOneUninterestingParentAndOtherParentIsNew()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(m, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleMergeCommitsInHistoryAllResolveToSameGroup()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit c = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m1 = tr.commit().parent(b).parent(c).create();
|
||||||
|
RevCommit m2 = tr.commit().parent(a).parent(m1).create();
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m2, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(b, a.name());
|
||||||
|
assertThat(groups).containsEntry(c, a.name());
|
||||||
|
assertThat(groups).containsEntry(m1, a.name());
|
||||||
|
assertThat(groups).containsEntry(m2, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWithDuplicatedParentGetsParentsGroup()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(a).create();
|
||||||
|
tr.getRevWalk().parseBody(m);
|
||||||
|
assertThat(m.getParentCount()).isEqualTo(2);
|
||||||
|
assertThat(m.getParent(0)).isEqualTo(m.getParent(1));
|
||||||
|
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets(),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(m, a.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mergeCommitWithOneNewParentAndTwoExistingPatchSets()
|
||||||
|
throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit c = tr.commit().parent(b).create();
|
||||||
|
RevCommit m = tr.commit().parent(a).parent(c).create();
|
||||||
|
|
||||||
|
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
||||||
|
String group2 = "1234567812345678123456781234567812345678";
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
newWalk(m, branchTip),
|
||||||
|
patchSets()
|
||||||
|
.put(a, psId(1, 1))
|
||||||
|
.put(b, psId(2, 1)),
|
||||||
|
groups()
|
||||||
|
.put(psId(1, 1), group1)
|
||||||
|
.put(psId(2, 1), group2));
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, group1);
|
||||||
|
assertThat(groups).containsEntry(b, group2);
|
||||||
|
assertThat(groups).containsEntry(c, group2);
|
||||||
|
assertThat(groups.asMap())
|
||||||
|
.containsEntry(m, ImmutableSet.of(group1, group2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectGroupsForMultipleTipsInParallel() throws Exception {
|
||||||
|
RevCommit branchTip = tr.commit().create();
|
||||||
|
RevCommit a = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit b = tr.commit().parent(a).create();
|
||||||
|
RevCommit c = tr.commit().parent(branchTip).create();
|
||||||
|
RevCommit d = tr.commit().parent(c).create();
|
||||||
|
|
||||||
|
RevWalk rw = newWalk(b, branchTip);
|
||||||
|
rw.markStart(rw.parseCommit(d));
|
||||||
|
// Schema upgrade case: all commits are existing patch sets, but none have
|
||||||
|
// groups assigned yet.
|
||||||
|
Multimap<ObjectId, String> groups = collectGroups(
|
||||||
|
rw,
|
||||||
|
patchSets()
|
||||||
|
.put(branchTip, psId(1, 1))
|
||||||
|
.put(a, psId(2, 1))
|
||||||
|
.put(b, psId(3, 1))
|
||||||
|
.put(c, psId(4, 1))
|
||||||
|
.put(d, psId(5, 1)),
|
||||||
|
groups());
|
||||||
|
|
||||||
|
assertThat(groups).containsEntry(a, a.name());
|
||||||
|
assertThat(groups).containsEntry(b, a.name());
|
||||||
|
assertThat(groups).containsEntry(c, c.name());
|
||||||
|
assertThat(groups).containsEntry(d, c.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(dborowitz): Tests for octopus merges.
|
||||||
|
|
||||||
|
private static PatchSet.Id psId(int c, int p) {
|
||||||
|
return new PatchSet.Id(new Change.Id(c), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception {
|
||||||
|
// Match RevWalk conditions from ReceiveCommits.
|
||||||
|
RevWalk rw = new RevWalk(tr.getRepository());
|
||||||
|
rw.sort(RevSort.TOPO);
|
||||||
|
rw.sort(RevSort.REVERSE, true);
|
||||||
|
rw.markStart(rw.parseCommit(start));
|
||||||
|
rw.markUninteresting(rw.parseCommit(branchTip));
|
||||||
|
return rw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Multimap<ObjectId, String> collectGroups(
|
||||||
|
RevWalk rw,
|
||||||
|
ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha,
|
||||||
|
ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup)
|
||||||
|
throws Exception {
|
||||||
|
GroupCollector gc =
|
||||||
|
new GroupCollector(patchSetsBySha.build(), groupLookup.build());
|
||||||
|
RevCommit c;
|
||||||
|
while ((c = rw.next()) != null) {
|
||||||
|
gc.visit(c);
|
||||||
|
}
|
||||||
|
return gc.getGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for constructing various map arguments, to avoid lots of
|
||||||
|
// type specifications.
|
||||||
|
private static ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSets() {
|
||||||
|
return ImmutableMultimap.builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() {
|
||||||
|
return ImmutableListMultimap.builder();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user