Merge "Add extension point to allow custom merge super set computations"
This commit is contained in:
@@ -483,6 +483,15 @@ plugins implementing this can modify commit message of the change being
|
||||
submitted by Rebase Always and Cherry Pick submit strategies as well as
|
||||
change being queried with COMMIT_FOOTERS option.
|
||||
|
||||
[[merge-super-set-computation]]
|
||||
== Merge Super Set Computation
|
||||
|
||||
The algorithm to compute the merge super set to detect changes that
|
||||
should be submitted together can be customized by implementing
|
||||
`com.google.gerrit.server.git.MergeSuperSetComputation`.
|
||||
MergeSuperSetComputation is a DynamicItem, so Gerrit may only have one
|
||||
implementation.
|
||||
|
||||
[[receive-pack]]
|
||||
== Receive Pack Initializers
|
||||
|
||||
|
@@ -66,6 +66,7 @@ import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.config.RestCacheAdminModule;
|
||||
import com.google.gerrit.server.events.StreamEventsApiListener;
|
||||
import com.google.gerrit.server.git.GarbageCollectionModule;
|
||||
import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
|
||||
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
||||
import com.google.gerrit.server.git.WorkQueue;
|
||||
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
|
||||
@@ -472,6 +473,7 @@ public class Daemon extends SiteProgram {
|
||||
if (testSysModule != null) {
|
||||
modules.add(testSysModule);
|
||||
}
|
||||
modules.add(new LocalMergeSuperSetComputation.Module());
|
||||
return cfgInjector.createChildInjector(modules);
|
||||
}
|
||||
|
||||
|
@@ -113,6 +113,7 @@ import com.google.gerrit.server.git.ChangeMessageModifier;
|
||||
import com.google.gerrit.server.git.EmailMerge;
|
||||
import com.google.gerrit.server.git.GitModule;
|
||||
import com.google.gerrit.server.git.GitModules;
|
||||
import com.google.gerrit.server.git.MergeSuperSetComputation;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.git.MergedByPushOp;
|
||||
import com.google.gerrit.server.git.NotesBranchUtil;
|
||||
@@ -375,6 +376,7 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
|
||||
DynamicSet.setOf(binder(), AssigneeValidationListener.class);
|
||||
DynamicSet.setOf(binder(), ActionVisitor.class);
|
||||
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
|
||||
|
||||
DynamicMap.mapOf(binder(), MailFilter.class);
|
||||
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
|
||||
|
@@ -0,0 +1,260 @@
|
||||
// Copyright (C) 2017 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 com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gerrit.common.data.SubmitTypeRecord;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
|
||||
import com.google.gerrit.server.permissions.ChangePermission;
|
||||
import com.google.gerrit.server.permissions.PermissionBackend;
|
||||
import com.google.gerrit.server.permissions.PermissionBackendException;
|
||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||
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.inject.AbstractModule;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevSort;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of MergeSuperSet that does the computation of the merge super set
|
||||
* sequentially on the local Gerrit instance.
|
||||
*/
|
||||
public class LocalMergeSuperSetComputation implements MergeSuperSetComputation {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalMergeSuperSetComputation.class);
|
||||
|
||||
public static class Module extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
DynamicItem.bind(binder(), MergeSuperSetComputation.class)
|
||||
.to(LocalMergeSuperSetComputation.class);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class QueryKey {
|
||||
private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
|
||||
return new AutoValue_LocalMergeSuperSetComputation_QueryKey(
|
||||
branch, ImmutableSet.copyOf(hashes));
|
||||
}
|
||||
|
||||
abstract Branch.NameKey branch();
|
||||
|
||||
abstract ImmutableSet<String> hashes();
|
||||
}
|
||||
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final Provider<InternalChangeQuery> queryProvider;
|
||||
private final Map<QueryKey, List<ChangeData>> queryCache;
|
||||
private final Map<Branch.NameKey, Optional<RevCommit>> heads;
|
||||
|
||||
@Inject
|
||||
LocalMergeSuperSetComputation(
|
||||
PermissionBackend permissionBackend, Provider<InternalChangeQuery> queryProvider) {
|
||||
this.permissionBackend = permissionBackend;
|
||||
this.queryProvider = queryProvider;
|
||||
this.queryCache = new HashMap<>();
|
||||
this.heads = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangeSet completeWithoutTopic(
|
||||
ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
|
||||
throws OrmException, IOException, PermissionBackendException {
|
||||
Collection<ChangeData> visibleChanges = new ArrayList<>();
|
||||
Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
|
||||
|
||||
// For each target branch we run a separate rev walk to find open changes
|
||||
// reachable from changes already in the merge super set.
|
||||
ImmutableListMultimap<Branch.NameKey, ChangeData> bc =
|
||||
byBranch(Iterables.concat(changeSet.changes(), changeSet.nonVisibleChanges()));
|
||||
for (Branch.NameKey b : bc.keySet()) {
|
||||
OpenRepo or = getRepo(orm, b.getParentKey());
|
||||
List<RevCommit> visibleCommits = new ArrayList<>();
|
||||
List<RevCommit> nonVisibleCommits = new ArrayList<>();
|
||||
for (ChangeData cd : bc.get(b)) {
|
||||
boolean visible = isVisible(db, changeSet, cd, user);
|
||||
|
||||
if (submitType(cd) == SubmitType.CHERRY_PICK) {
|
||||
if (visible) {
|
||||
visibleChanges.add(cd);
|
||||
} else {
|
||||
nonVisibleChanges.add(cd);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the underlying git commit object
|
||||
String objIdStr = cd.currentPatchSet().getRevision().get();
|
||||
RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
|
||||
|
||||
// Always include the input, even if merged. This allows
|
||||
// SubmitStrategyOp to correct the situation later, assuming it gets
|
||||
// returned by byCommitsOnBranchNotMerged below.
|
||||
if (visible) {
|
||||
visibleCommits.add(commit);
|
||||
} else {
|
||||
nonVisibleCommits.add(commit);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visibleHashes =
|
||||
walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
|
||||
Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
|
||||
|
||||
Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
|
||||
Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
|
||||
}
|
||||
|
||||
return new ChangeSet(visibleChanges, nonVisibleChanges);
|
||||
}
|
||||
|
||||
private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
|
||||
Iterable<ChangeData> changes) throws OrmException {
|
||||
ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
|
||||
ImmutableListMultimap.builder();
|
||||
for (ChangeData cd : changes) {
|
||||
builder.put(cd.change().getDest(), cd);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private OpenRepo getRepo(MergeOpRepoManager orm, Project.NameKey project) throws IOException {
|
||||
try {
|
||||
OpenRepo or = orm.getRepo(project);
|
||||
checkState(or.rw.hasRevSort(RevSort.TOPO));
|
||||
return or;
|
||||
} catch (NoSuchProjectException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isVisible(ReviewDb db, ChangeSet changeSet, ChangeData cd, CurrentUser user)
|
||||
throws PermissionBackendException {
|
||||
boolean visible = changeSet.ids().contains(cd.getId());
|
||||
if (visible
|
||||
&& !permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ)) {
|
||||
// We thought the change was visible, but it isn't.
|
||||
// This can happen if the ACL changes during the
|
||||
// completeChangeSet computation, for example.
|
||||
visible = false;
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
private SubmitType submitType(ChangeData cd) throws OrmException {
|
||||
SubmitTypeRecord str = cd.submitTypeRecord();
|
||||
if (!str.isOk()) {
|
||||
logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
|
||||
}
|
||||
return str.type;
|
||||
}
|
||||
|
||||
private List<ChangeData> byCommitsOnBranchNotMerged(
|
||||
OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
|
||||
throws OrmException, IOException {
|
||||
if (hashes.isEmpty()) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
QueryKey k = QueryKey.create(branch, hashes);
|
||||
List<ChangeData> cached = queryCache.get(k);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
List<ChangeData> result = new ArrayList<>();
|
||||
Iterable<ChangeData> destChanges =
|
||||
MergeSuperSet.query(queryProvider.get())
|
||||
.byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
|
||||
for (ChangeData chd : destChanges) {
|
||||
result.add(chd);
|
||||
}
|
||||
queryCache.put(k, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<String> walkChangesByHashes(
|
||||
Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
|
||||
throws IOException {
|
||||
Set<String> destHashes = new HashSet<>();
|
||||
or.rw.reset();
|
||||
markHeadUninteresting(or, b);
|
||||
for (RevCommit c : sourceCommits) {
|
||||
String name = c.name();
|
||||
if (ignoreHashes.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
destHashes.add(name);
|
||||
or.rw.markStart(c);
|
||||
}
|
||||
for (RevCommit c : or.rw) {
|
||||
String name = c.name();
|
||||
if (ignoreHashes.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
destHashes.add(name);
|
||||
}
|
||||
|
||||
return destHashes;
|
||||
}
|
||||
|
||||
private void markHeadUninteresting(OpenRepo or, Branch.NameKey b) throws IOException {
|
||||
Optional<RevCommit> head = heads.get(b);
|
||||
if (head == null) {
|
||||
Ref ref = or.repo.getRefDatabase().exactRef(b.get());
|
||||
head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
|
||||
heads.put(b, head);
|
||||
}
|
||||
if (head.isPresent()) {
|
||||
or.rw.markUninteresting(head.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void logErrorAndThrow(String msg) throws OrmException {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(msg);
|
||||
}
|
||||
throw new OrmException(msg);
|
||||
}
|
||||
}
|
@@ -17,27 +17,18 @@ package com.google.gerrit.server.git;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gerrit.common.data.SubmitTypeRecord;
|
||||
import com.google.gerrit.extensions.client.SubmitType;
|
||||
import com.google.gerrit.reviewdb.client.Branch;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.change.Submit;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
|
||||
import com.google.gerrit.server.index.change.ChangeField;
|
||||
import com.google.gerrit.server.permissions.ChangePermission;
|
||||
import com.google.gerrit.server.permissions.PermissionBackend;
|
||||
import com.google.gerrit.server.permissions.PermissionBackendException;
|
||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||
import com.google.gerrit.server.query.change.ChangeData;
|
||||
import com.google.gerrit.server.query.change.InternalChangeQuery;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
@@ -45,21 +36,10 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevSort;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Calculates the minimal superset of changes required to be merged.
|
||||
@@ -72,10 +52,9 @@ import org.slf4j.LoggerFactory;
|
||||
* included.
|
||||
*/
|
||||
public class MergeSuperSet {
|
||||
private static final Logger log = LoggerFactory.getLogger(MergeSuperSet.class);
|
||||
|
||||
public static void reloadChanges(ChangeSet changeSet) throws OrmException {
|
||||
// Clear exactly the fields requested by query() below.
|
||||
// Clear exactly the fields requested by query(InternalChangeQuery) below.
|
||||
for (ChangeData cd : changeSet.changes()) {
|
||||
cd.reloadChange();
|
||||
cd.setPatchSets(null);
|
||||
@@ -83,24 +62,26 @@ public class MergeSuperSet {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class QueryKey {
|
||||
private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
|
||||
return new AutoValue_MergeSuperSet_QueryKey(branch, ImmutableSet.copyOf(hashes));
|
||||
}
|
||||
|
||||
abstract Branch.NameKey branch();
|
||||
|
||||
abstract ImmutableSet<String> hashes();
|
||||
public static InternalChangeQuery query(InternalChangeQuery q) {
|
||||
// Request fields required for completing the ChangeSet and converting to
|
||||
// ChangeInfo without having to touch the database or opening the repository
|
||||
// more than necessary. This provides reasonable performance when loading
|
||||
// the change screen; callers that care about reading the latest value of
|
||||
// these fields should clear them explicitly using reloadChanges().
|
||||
Set<String> fields =
|
||||
ImmutableSet.of(
|
||||
ChangeField.CHANGE.getName(),
|
||||
ChangeField.PATCH_SET.getName(),
|
||||
ChangeField.MERGEABLE.getName());
|
||||
return q.setRequestedFields(fields);
|
||||
}
|
||||
|
||||
private final ChangeData.Factory changeDataFactory;
|
||||
private final Provider<InternalChangeQuery> queryProvider;
|
||||
private final Provider<MergeOpRepoManager> repoManagerProvider;
|
||||
private final DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation;
|
||||
private final PermissionBackend permissionBackend;
|
||||
private final Config cfg;
|
||||
private final Map<QueryKey, List<ChangeData>> queryCache;
|
||||
private final Map<Branch.NameKey, Optional<RevCommit>> heads;
|
||||
|
||||
private MergeOpRepoManager orm;
|
||||
private boolean closeOrm;
|
||||
@@ -111,14 +92,14 @@ public class MergeSuperSet {
|
||||
ChangeData.Factory changeDataFactory,
|
||||
Provider<InternalChangeQuery> queryProvider,
|
||||
Provider<MergeOpRepoManager> repoManagerProvider,
|
||||
DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
|
||||
PermissionBackend permissionBackend) {
|
||||
this.cfg = cfg;
|
||||
this.changeDataFactory = changeDataFactory;
|
||||
this.queryProvider = queryProvider;
|
||||
this.repoManagerProvider = repoManagerProvider;
|
||||
this.mergeSuperSetComputation = mergeSuperSetComputation;
|
||||
this.permissionBackend = permissionBackend;
|
||||
queryCache = new HashMap<>();
|
||||
heads = new HashMap<>();
|
||||
}
|
||||
|
||||
public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
|
||||
@@ -131,6 +112,11 @@ public class MergeSuperSet {
|
||||
public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
|
||||
throws IOException, OrmException, PermissionBackendException {
|
||||
try {
|
||||
if (orm == null) {
|
||||
orm = repoManagerProvider.get();
|
||||
closeOrm = true;
|
||||
}
|
||||
|
||||
ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
|
||||
ChangeSet changeSet =
|
||||
new ChangeSet(
|
||||
@@ -138,7 +124,7 @@ public class MergeSuperSet {
|
||||
if (Submit.wholeTopicEnabled(cfg)) {
|
||||
return completeChangeSetIncludingTopics(db, changeSet, user);
|
||||
}
|
||||
return completeChangeSetWithoutTopic(db, changeSet, user);
|
||||
return mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
|
||||
} finally {
|
||||
if (closeOrm && orm != null) {
|
||||
orm.close();
|
||||
@@ -147,161 +133,13 @@ public class MergeSuperSet {
|
||||
}
|
||||
}
|
||||
|
||||
private SubmitType submitType(ChangeData cd) throws OrmException {
|
||||
SubmitTypeRecord str = cd.submitTypeRecord();
|
||||
if (!str.isOk()) {
|
||||
logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage);
|
||||
}
|
||||
return str.type;
|
||||
}
|
||||
|
||||
private static ImmutableListMultimap<Branch.NameKey, ChangeData> byBranch(
|
||||
Iterable<ChangeData> changes) throws OrmException {
|
||||
ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
|
||||
ImmutableListMultimap.builder();
|
||||
for (ChangeData cd : changes) {
|
||||
builder.put(cd.change().getDest(), cd);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Set<String> walkChangesByHashes(
|
||||
Collection<RevCommit> sourceCommits, Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
|
||||
throws IOException {
|
||||
Set<String> destHashes = new HashSet<>();
|
||||
or.rw.reset();
|
||||
markHeadUninteresting(or, b);
|
||||
for (RevCommit c : sourceCommits) {
|
||||
String name = c.name();
|
||||
if (ignoreHashes.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
destHashes.add(name);
|
||||
or.rw.markStart(c);
|
||||
}
|
||||
for (RevCommit c : or.rw) {
|
||||
String name = c.name();
|
||||
if (ignoreHashes.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
destHashes.add(name);
|
||||
}
|
||||
|
||||
return destHashes;
|
||||
}
|
||||
|
||||
private ChangeSet completeChangeSetWithoutTopic(
|
||||
ReviewDb db, ChangeSet changeSet, CurrentUser user)
|
||||
throws IOException, OrmException, PermissionBackendException {
|
||||
Collection<ChangeData> visibleChanges = new ArrayList<>();
|
||||
Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
|
||||
|
||||
// For each target branch we run a separate rev walk to find open changes
|
||||
// reachable from changes already in the merge super set.
|
||||
ImmutableListMultimap<Branch.NameKey, ChangeData> bc =
|
||||
byBranch(Iterables.concat(changeSet.changes(), changeSet.nonVisibleChanges()));
|
||||
for (Branch.NameKey b : bc.keySet()) {
|
||||
OpenRepo or = getRepo(b.getParentKey());
|
||||
List<RevCommit> visibleCommits = new ArrayList<>();
|
||||
List<RevCommit> nonVisibleCommits = new ArrayList<>();
|
||||
for (ChangeData cd : bc.get(b)) {
|
||||
boolean visible = changeSet.ids().contains(cd.getId());
|
||||
if (visible && !canRead(db, user, cd)) {
|
||||
// We thought the change was visible, but it isn't.
|
||||
// This can happen if the ACL changes during the
|
||||
// completeChangeSet computation, for example.
|
||||
visible = false;
|
||||
}
|
||||
|
||||
if (submitType(cd) == SubmitType.CHERRY_PICK) {
|
||||
if (visible) {
|
||||
visibleChanges.add(cd);
|
||||
} else {
|
||||
nonVisibleChanges.add(cd);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the underlying git commit object
|
||||
String objIdStr = cd.currentPatchSet().getRevision().get();
|
||||
RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
|
||||
|
||||
// Always include the input, even if merged. This allows
|
||||
// SubmitStrategyOp to correct the situation later, assuming it gets
|
||||
// returned by byCommitsOnBranchNotMerged below.
|
||||
if (visible) {
|
||||
visibleCommits.add(commit);
|
||||
} else {
|
||||
nonVisibleCommits.add(commit);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visibleHashes =
|
||||
walkChangesByHashes(visibleCommits, Collections.emptySet(), or, b);
|
||||
Iterables.addAll(visibleChanges, byCommitsOnBranchNotMerged(or, db, b, visibleHashes));
|
||||
|
||||
Set<String> nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
|
||||
Iterables.addAll(nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, b, nonVisibleHashes));
|
||||
}
|
||||
|
||||
return new ChangeSet(visibleChanges, nonVisibleChanges);
|
||||
}
|
||||
|
||||
private OpenRepo getRepo(Project.NameKey project) throws IOException {
|
||||
if (orm == null) {
|
||||
orm = repoManagerProvider.get();
|
||||
closeOrm = true;
|
||||
}
|
||||
try {
|
||||
OpenRepo or = orm.getRepo(project);
|
||||
checkState(or.rw.hasRevSort(RevSort.TOPO));
|
||||
return or;
|
||||
} catch (NoSuchProjectException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void markHeadUninteresting(OpenRepo or, Branch.NameKey b) throws IOException {
|
||||
Optional<RevCommit> head = heads.get(b);
|
||||
if (head == null) {
|
||||
Ref ref = or.repo.getRefDatabase().exactRef(b.get());
|
||||
head = ref != null ? Optional.of(or.rw.parseCommit(ref.getObjectId())) : Optional.empty();
|
||||
heads.put(b, head);
|
||||
}
|
||||
if (head.isPresent()) {
|
||||
or.rw.markUninteresting(head.get());
|
||||
}
|
||||
}
|
||||
|
||||
private List<ChangeData> byCommitsOnBranchNotMerged(
|
||||
OpenRepo or, ReviewDb db, Branch.NameKey branch, Set<String> hashes)
|
||||
throws OrmException, IOException {
|
||||
if (hashes.isEmpty()) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
QueryKey k = QueryKey.create(branch, hashes);
|
||||
List<ChangeData> cached = queryCache.get(k);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
List<ChangeData> result = new ArrayList<>();
|
||||
Iterable<ChangeData> destChanges =
|
||||
query().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
|
||||
for (ChangeData chd : destChanges) {
|
||||
result.add(chd);
|
||||
}
|
||||
queryCache.put(k, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes {@code changeSet} with any additional changes from its topics
|
||||
*
|
||||
* <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
|
||||
* #completeChangeSetWithoutTopic}, to discover what additional changes should be submitted with a
|
||||
* change until the set stops growing.
|
||||
* MergeSuperSetComputation#completeWithoutTopic(ReviewDb, MergeOpRepoManager, ChangeSet,
|
||||
* CurrentUser)}, to discover what additional changes should be submitted with a change until the
|
||||
* set stops growing.
|
||||
*
|
||||
* <p>{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to
|
||||
* avoid wasted work.
|
||||
@@ -324,7 +162,7 @@ public class MergeSuperSet {
|
||||
if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
|
||||
continue;
|
||||
}
|
||||
for (ChangeData topicCd : query().byTopicOpen(topic)) {
|
||||
for (ChangeData topicCd : byTopicOpen(topic)) {
|
||||
if (canRead(db, user, topicCd)) {
|
||||
visibleChanges.add(topicCd);
|
||||
} else {
|
||||
@@ -340,7 +178,7 @@ public class MergeSuperSet {
|
||||
if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
|
||||
continue;
|
||||
}
|
||||
for (ChangeData topicCd : query().byTopicOpen(topic)) {
|
||||
for (ChangeData topicCd : byTopicOpen(topic)) {
|
||||
nonVisibleChanges.add(topicCd);
|
||||
}
|
||||
topicsSeen.add(topic);
|
||||
@@ -360,36 +198,15 @@ public class MergeSuperSet {
|
||||
oldSeen = seen;
|
||||
|
||||
changeSet = topicClosure(db, changeSet, user, topicsSeen, visibleTopicsSeen);
|
||||
changeSet = completeChangeSetWithoutTopic(db, changeSet, user);
|
||||
changeSet = mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
|
||||
|
||||
seen = topicsSeen.size() + visibleTopicsSeen.size();
|
||||
} while (seen != oldSeen);
|
||||
return changeSet;
|
||||
}
|
||||
|
||||
private InternalChangeQuery query() {
|
||||
// Request fields required for completing the ChangeSet and converting to
|
||||
// ChangeInfo without having to touch the database or opening the repository
|
||||
// more than necessary. This provides reasonable performance when loading
|
||||
// the change screen; callers that care about reading the latest value of
|
||||
// these fields should clear them explicitly using reloadChanges().
|
||||
Set<String> fields =
|
||||
ImmutableSet.of(
|
||||
ChangeField.CHANGE.getName(),
|
||||
ChangeField.PATCH_SET.getName(),
|
||||
ChangeField.MERGEABLE.getName());
|
||||
return queryProvider.get().setRequestedFields(fields);
|
||||
}
|
||||
|
||||
private void logError(String msg) {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void logErrorAndThrow(String msg) throws OrmException {
|
||||
logError(msg);
|
||||
throw new OrmException(msg);
|
||||
private List<ChangeData> byTopicOpen(String topic) throws OrmException {
|
||||
return query(queryProvider.get()).byTopicOpen(topic);
|
||||
}
|
||||
|
||||
private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
|
||||
|
@@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2017 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 com.google.gerrit.extensions.annotations.ExtensionPoint;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.permissions.PermissionBackendException;
|
||||
import com.google.gwtorm.server.OrmException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Interface to compute the merge super set to detect changes that should be submitted together.
|
||||
*
|
||||
* <p>E.g. to speed up performance implementations could decide to do the computation in batches in
|
||||
* parallel on different server nodes.
|
||||
*/
|
||||
@ExtensionPoint
|
||||
public interface MergeSuperSetComputation {
|
||||
|
||||
/**
|
||||
* Compute the set of changes that should be submitted together. As input a set of changes is
|
||||
* provided for which it is known that they should be submitted together. This method should
|
||||
* complete the set by including open predecessor changes that need to be submitted as well. To
|
||||
* decide whether open predecessor changes should be included the method must take the submit type
|
||||
* into account (e.g. for changes with submit type "Cherry-Pick" open predecessor changes must not
|
||||
* be included).
|
||||
*
|
||||
* <p>This method is invoked iteratively while new changes to be submitted together are discovered
|
||||
* by expanding the topics of the changes. This method must not do any topic expansion on its own.
|
||||
*
|
||||
* @param db {@link ReviewDb} instance
|
||||
* @param orm {@link MergeOpRepoManager} that should be used to access repositories
|
||||
* @param changeSet A set of changes for which it is known that they should be submitted together
|
||||
* @param user The user for which the visibility checks should be performed
|
||||
* @return the completed set of changes that should be submitted together
|
||||
*/
|
||||
ChangeSet completeWithoutTopic(
|
||||
ReviewDb db, MergeOpRepoManager orm, ChangeSet changeSet, CurrentUser user)
|
||||
throws OrmException, IOException, PermissionBackendException;
|
||||
}
|
@@ -47,6 +47,7 @@ import com.google.gerrit.server.config.TrackingFooters;
|
||||
import com.google.gerrit.server.config.TrackingFootersProvider;
|
||||
import com.google.gerrit.server.git.GarbageCollection;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
|
||||
import com.google.gerrit.server.git.PerThreadRequestScope;
|
||||
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
||||
import com.google.gerrit.server.git.SendEmailExecutor;
|
||||
@@ -202,7 +203,7 @@ public class InMemoryModule extends FactoryModule {
|
||||
return CanonicalWebUrlProvider.class;
|
||||
}
|
||||
});
|
||||
//Replacement of DiffExecutorModule to not use thread pool in the tests
|
||||
// Replacement of DiffExecutorModule to not use thread pool in the tests
|
||||
install(
|
||||
new AbstractModule() {
|
||||
@Override
|
||||
@@ -220,6 +221,7 @@ public class InMemoryModule extends FactoryModule {
|
||||
install(new SignedTokenEmailTokenVerifier.Module());
|
||||
install(new GpgModule(cfg));
|
||||
install(new InMemoryAccountPatchReviewStore.Module());
|
||||
install(new LocalMergeSuperSetComputation.Module());
|
||||
|
||||
bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
|
||||
bind(AllChangesIndexer.class).toProvider(Providers.of(null));
|
||||
|
@@ -50,6 +50,7 @@ import com.google.gerrit.server.config.SitePath;
|
||||
import com.google.gerrit.server.events.StreamEventsApiListener;
|
||||
import com.google.gerrit.server.git.GarbageCollectionModule;
|
||||
import com.google.gerrit.server.git.GitRepositoryManagerModule;
|
||||
import com.google.gerrit.server.git.LocalMergeSuperSetComputation;
|
||||
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
||||
import com.google.gerrit.server.git.WorkQueue;
|
||||
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
|
||||
@@ -325,6 +326,7 @@ public class WebAppInitializer extends GuiceServletContextListener implements Fi
|
||||
modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
|
||||
modules.add(new SmtpEmailSender.Module());
|
||||
modules.add(new SignedTokenEmailTokenVerifier.Module());
|
||||
modules.add(new LocalMergeSuperSetComputation.Module());
|
||||
|
||||
// Plugin module needs to be inserted *before* the index module.
|
||||
// There is the concept of LifecycleModule, in Gerrit's own extension
|
||||
|
Reference in New Issue
Block a user