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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,266 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.change;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
import com.google.gerrit.server.change.GetRelated.RelatedInfo;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Implementation of {@link GetRelated} using {@link PatchSetAncestor}s. */
class GetRelatedByAncestors {
private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
private final GitRepositoryManager gitMgr;
private final Provider<ReviewDb> dbProvider;
private final Provider<InternalChangeQuery> queryProvider;
@Inject
GetRelatedByAncestors(GitRepositoryManager gitMgr,
Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider) {
this.gitMgr = gitMgr;
this.dbProvider = db;
this.queryProvider = queryProvider;
}
public RelatedInfo getRelated(RevisionResource rsrc)
throws RepositoryNotFoundException, IOException, OrmException {
try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
RevWalk rw = new RevWalk(git)) {
Ref ref = git.getRef(rsrc.getChange().getDest().get());
RelatedInfo info = new RelatedInfo();
info.changes = walk(rsrc, rw, ref);
return info;
}
}
private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
throws OrmException, IOException {
Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
Map<String, PatchSet> commits = Maps.newHashMap();
for (PatchSet p : patchSets.values()) {
commits.put(p.getRevision().get(), p);
}
RevCommit rev = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
rw.sort(RevSort.TOPO);
rw.markStart(rev);
if (ref != null && ref.getObjectId() != null) {
try {
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
} catch (IncorrectObjectTypeException notCommit) {
// Ignore and treat as new branch.
}
}
Set<Change.Id> added = Sets.newHashSet();
List<ChangeAndCommit> parents = Lists.newArrayList();
for (RevCommit c; (c = rw.next()) != null;) {
PatchSet p = commits.get(c.name());
Change g = null;
if (p != null) {
g = changes.get(p.getId().getParentKey()).change();
added.add(p.getId().getParentKey());
}
parents.add(new ChangeAndCommit(g, p, c));
}
List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets, added);
list.addAll(parents);
if (list.size() == 1) {
ChangeAndCommit r = list.get(0);
if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
return Collections.emptyList();
}
}
return list;
}
private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
throws OrmException {
return ChangeData.asMap(
queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
}
private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
Collection<ChangeData> cds) throws OrmException {
Map<PatchSet.Id, PatchSet> r =
Maps.newHashMapWithExpectedSize(cds.size() * 2);
for (ChangeData cd : cds) {
for (PatchSet p : cd.patchSets()) {
r.put(p.getId(), p);
}
}
if (rsrc.getEdit().isPresent()) {
r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
}
return r;
}
private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
Set<Change.Id> added)
throws OrmException, IOException {
// children is a map of parent commit name to PatchSet built on it.
Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
RevFlag seenCommit = rw.newFlag("seenCommit");
LinkedList<String> q = Lists.newLinkedList();
seedQueue(rsrc, rw, seenCommit, patchSets, q);
ProjectControl projectCtl = rsrc.getControl().getProjectControl();
Set<Change.Id> seenChange = Sets.newHashSet();
List<ChangeAndCommit> graph = Lists.newArrayList();
while (!q.isEmpty()) {
String id = q.remove();
// For every matching change find the most recent patch set.
Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
for (PatchSet.Id psId : children.get(id)) {
PatchSet.Id e = matches.get(psId.getParentKey());
if ((e == null || e.get() < psId.get())
&& isVisible(projectCtl, changes, patchSets, psId)) {
matches.put(psId.getParentKey(), psId);
}
}
for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
ChangeData cd = changes.get(e.getKey());
PatchSet ps = patchSets.get(e.getValue());
if (cd == null || ps == null || !seenChange.add(e.getKey())) {
continue;
}
RevCommit c = rw.parseCommit(ObjectId.fromString(
ps.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.addFirst(ps.getRevision().get());
if (added.add(ps.getId().getParentKey())) {
rw.parseBody(c);
graph.add(new ChangeAndCommit(cd.change(), ps, c));
}
}
}
}
Collections.reverse(graph);
return graph;
}
private boolean isVisible(ProjectControl projectCtl,
Map<Change.Id, ChangeData> changes,
Map<PatchSet.Id, PatchSet> patchSets,
PatchSet.Id psId) throws OrmException {
ChangeData cd = changes.get(psId.getParentKey());
PatchSet ps = patchSets.get(psId);
if (cd != null && ps != null) {
// Related changes are in the same project, so reuse the existing
// ProjectControl.
ChangeControl ctl = projectCtl.controlFor(cd.change());
return ctl.isVisible(dbProvider.get())
&& ctl.isPatchVisible(ps, dbProvider.get());
}
return false;
}
private void seedQueue(RevisionResource rsrc, RevWalk rw,
RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
LinkedList<String> q) throws IOException {
RevCommit tip = rw.parseCommit(ObjectId.fromString(
rsrc.getPatchSet().getRevision().get()));
tip.add(seenCommit);
q.add(tip.name());
Change.Id cId = rsrc.getChange().getId();
for (PatchSet p : patchSets.values()) {
if (cId.equals(p.getId().getParentKey())) {
try {
RevCommit c = rw.parseCommit(ObjectId.fromString(
p.getRevision().get()));
if (!c.has(seenCommit)) {
c.add(seenCommit);
q.add(c.name());
}
} catch (IOException e) {
log.warn(String.format(
"Cannot read patch set %d of %d",
p.getPatchSetId(), cId.get()), e);
}
}
}
}
private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
throws OrmException {
ReviewDb db = dbProvider.get();
List<ResultSet<PatchSetAncestor>> t =
Lists.newArrayListWithCapacity(ids.size());
for (Change.Id id : ids) {
t.add(db.patchSetAncestors().byChange(id));
}
Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
for (ResultSet<PatchSetAncestor> rs : t) {
for (PatchSetAncestor a : rs) {
r.put(a.getAncestorRevision().get(), a.getPatchSet());
}
}
return r;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import java.util.List;
class GroupPredicate extends IndexPredicate<ChangeData> {
GroupPredicate(String group) {
super(ChangeField.GROUP, group);
}
@Override
public boolean match(ChangeData cd) throws OrmException {
for (PatchSet ps : cd.patchSets()) {
List<String> groups = ps.getGroups();
if (groups != null && groups.contains(getValue())) {
return true;
}
}
return false;
}
@Override
public int getCost() {
return 1;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,161 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.schema;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class Schema_108 extends SchemaVersion {
private final GitRepositoryManager repoManager;
@Inject
Schema_108(Provider<Schema_107> prior,
GitRepositoryManager repoManager) {
super(prior);
this.repoManager = repoManager;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
ui.message("Listing all changes ...");
SetMultimap<Project.NameKey, Change.Id> openByProject =
getOpenChangesByProject(db);
ui.message("done");
ui.message("Updating groups for open changes ...");
int i = 0;
for (Map.Entry<Project.NameKey, Collection<Change.Id>> e
: openByProject.asMap().entrySet()) {
try (Repository repo = repoManager.openRepository(e.getKey());
RevWalk rw = new RevWalk(repo)) {
updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue());
} catch (IOException err) {
throw new OrmException(err);
}
if (++i % 100 == 0) {
ui.message(" done " + i + " projects ...");
}
}
ui.message("done");
}
private static void updateProjectGroups(ReviewDb db, Repository repo,
RevWalk rw, Set<Change.Id> changes) throws OrmException, IOException {
// Match sorting in ReceiveCommits.
rw.reset();
rw.sort(RevSort.TOPO);
rw.sort(RevSort.REVERSE, true);
RefDatabase refdb = repo.getRefDatabase();
for (Ref ref : refdb.getRefs(Constants.R_HEADS).values()) {
RevCommit c = maybeParseCommit(rw, ref.getObjectId());
if (c != null) {
rw.markUninteresting(c);
}
}
Multimap<ObjectId, Ref> changeRefsBySha = ArrayListMultimap.create();
Multimap<ObjectId, PatchSet.Id> patchSetsBySha = ArrayListMultimap.create();
for (Ref ref : refdb.getRefs(RefNames.REFS_CHANGES).values()) {
ObjectId id = ref.getObjectId();
if (ref.getObjectId() == null) {
continue;
}
id = id.copy();
changeRefsBySha.put(id, ref);
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
if (psId != null && changes.contains(psId.getParentKey())) {
patchSetsBySha.put(id, psId);
RevCommit c = maybeParseCommit(rw, id);
if (c != null) {
rw.markStart(c);
}
}
}
GroupCollector collector = new GroupCollector(changeRefsBySha, db);
RevCommit c;
while ((c = rw.next()) != null) {
collector.visit(c);
}
updateGroups(db, collector, patchSetsBySha);
}
private static void updateGroups(ReviewDb db, GroupCollector collector,
Multimap<ObjectId, PatchSet.Id> patchSetsBySha) throws OrmException {
Map<PatchSet.Id, PatchSet> patchSets =
db.patchSets().toMap(db.patchSets().get(patchSetsBySha.values()));
for (Map.Entry<ObjectId, Collection<String>> e
: collector.getGroups().asMap().entrySet()) {
for (PatchSet.Id psId : patchSetsBySha.get(e.getKey())) {
PatchSet ps = patchSets.get(psId);
if (ps != null) {
ps.setGroups(e.getValue());
}
}
}
db.patchSets().update(patchSets.values());
}
private SetMultimap<Project.NameKey, Change.Id> getOpenChangesByProject(
ReviewDb db) throws OrmException {
SetMultimap<Project.NameKey, Change.Id> openByProject =
HashMultimap.create();
for (Change c : db.changes().all()) {
if (c.getStatus().isOpen()) {
openByProject.put(c.getProject(), c.getId());
}
}
return openByProject;
}
private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id)
throws IOException {
if (id == null) {
return null;
}
RevObject obj = rw.parseAny(id);
return (obj instanceof RevCommit) ? (RevCommit) obj : null;
}
}