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
|
submitted by Rebase Always and Cherry Pick submit strategies as well as
|
||||||
change being queried with COMMIT_FOOTERS option.
|
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]]
|
||||||
== Receive Pack Initializers
|
== 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.config.RestCacheAdminModule;
|
||||||
import com.google.gerrit.server.events.StreamEventsApiListener;
|
import com.google.gerrit.server.events.StreamEventsApiListener;
|
||||||
import com.google.gerrit.server.git.GarbageCollectionModule;
|
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.SearchingChangeCacheImpl;
|
||||||
import com.google.gerrit.server.git.WorkQueue;
|
import com.google.gerrit.server.git.WorkQueue;
|
||||||
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
|
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
|
||||||
@@ -472,6 +473,7 @@ public class Daemon extends SiteProgram {
|
|||||||
if (testSysModule != null) {
|
if (testSysModule != null) {
|
||||||
modules.add(testSysModule);
|
modules.add(testSysModule);
|
||||||
}
|
}
|
||||||
|
modules.add(new LocalMergeSuperSetComputation.Module());
|
||||||
return cfgInjector.createChildInjector(modules);
|
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.EmailMerge;
|
||||||
import com.google.gerrit.server.git.GitModule;
|
import com.google.gerrit.server.git.GitModule;
|
||||||
import com.google.gerrit.server.git.GitModules;
|
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.MergeUtil;
|
||||||
import com.google.gerrit.server.git.MergedByPushOp;
|
import com.google.gerrit.server.git.MergedByPushOp;
|
||||||
import com.google.gerrit.server.git.NotesBranchUtil;
|
import com.google.gerrit.server.git.NotesBranchUtil;
|
||||||
@@ -375,6 +376,7 @@ public class GerritGlobalModule extends FactoryModule {
|
|||||||
DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
|
DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
|
||||||
DynamicSet.setOf(binder(), AssigneeValidationListener.class);
|
DynamicSet.setOf(binder(), AssigneeValidationListener.class);
|
||||||
DynamicSet.setOf(binder(), ActionVisitor.class);
|
DynamicSet.setOf(binder(), ActionVisitor.class);
|
||||||
|
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
|
||||||
|
|
||||||
DynamicMap.mapOf(binder(), MailFilter.class);
|
DynamicMap.mapOf(binder(), MailFilter.class);
|
||||||
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.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.checkNotNull;
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
import com.google.auto.value.AutoValue;
|
|
||||||
import com.google.common.base.Strings;
|
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.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||||
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.reviewdb.client.Change;
|
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.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.change.Submit;
|
import com.google.gerrit.server.change.Submit;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
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.index.change.ChangeField;
|
||||||
import com.google.gerrit.server.permissions.ChangePermission;
|
import com.google.gerrit.server.permissions.ChangePermission;
|
||||||
import com.google.gerrit.server.permissions.PermissionBackend;
|
import com.google.gerrit.server.permissions.PermissionBackend;
|
||||||
import com.google.gerrit.server.permissions.PermissionBackendException;
|
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.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;
|
||||||
@@ -45,21 +36,10 @@ import com.google.inject.Inject;
|
|||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.eclipse.jgit.lib.Config;
|
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.
|
* Calculates the minimal superset of changes required to be merged.
|
||||||
@@ -72,10 +52,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
* included.
|
* included.
|
||||||
*/
|
*/
|
||||||
public class MergeSuperSet {
|
public class MergeSuperSet {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MergeSuperSet.class);
|
|
||||||
|
|
||||||
public static void reloadChanges(ChangeSet changeSet) throws OrmException {
|
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()) {
|
for (ChangeData cd : changeSet.changes()) {
|
||||||
cd.reloadChange();
|
cd.reloadChange();
|
||||||
cd.setPatchSets(null);
|
cd.setPatchSets(null);
|
||||||
@@ -83,24 +62,26 @@ public class MergeSuperSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoValue
|
public static InternalChangeQuery query(InternalChangeQuery q) {
|
||||||
abstract static class QueryKey {
|
// Request fields required for completing the ChangeSet and converting to
|
||||||
private static QueryKey create(Branch.NameKey branch, Iterable<String> hashes) {
|
// ChangeInfo without having to touch the database or opening the repository
|
||||||
return new AutoValue_MergeSuperSet_QueryKey(branch, ImmutableSet.copyOf(hashes));
|
// 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().
|
||||||
abstract Branch.NameKey branch();
|
Set<String> fields =
|
||||||
|
ImmutableSet.of(
|
||||||
abstract ImmutableSet<String> hashes();
|
ChangeField.CHANGE.getName(),
|
||||||
|
ChangeField.PATCH_SET.getName(),
|
||||||
|
ChangeField.MERGEABLE.getName());
|
||||||
|
return q.setRequestedFields(fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ChangeData.Factory changeDataFactory;
|
private final ChangeData.Factory changeDataFactory;
|
||||||
private final Provider<InternalChangeQuery> queryProvider;
|
private final Provider<InternalChangeQuery> queryProvider;
|
||||||
private final Provider<MergeOpRepoManager> repoManagerProvider;
|
private final Provider<MergeOpRepoManager> repoManagerProvider;
|
||||||
|
private final DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation;
|
||||||
private final PermissionBackend permissionBackend;
|
private final PermissionBackend permissionBackend;
|
||||||
private final Config cfg;
|
private final Config cfg;
|
||||||
private final Map<QueryKey, List<ChangeData>> queryCache;
|
|
||||||
private final Map<Branch.NameKey, Optional<RevCommit>> heads;
|
|
||||||
|
|
||||||
private MergeOpRepoManager orm;
|
private MergeOpRepoManager orm;
|
||||||
private boolean closeOrm;
|
private boolean closeOrm;
|
||||||
@@ -111,14 +92,14 @@ public class MergeSuperSet {
|
|||||||
ChangeData.Factory changeDataFactory,
|
ChangeData.Factory changeDataFactory,
|
||||||
Provider<InternalChangeQuery> queryProvider,
|
Provider<InternalChangeQuery> queryProvider,
|
||||||
Provider<MergeOpRepoManager> repoManagerProvider,
|
Provider<MergeOpRepoManager> repoManagerProvider,
|
||||||
|
DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
|
||||||
PermissionBackend permissionBackend) {
|
PermissionBackend permissionBackend) {
|
||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
this.changeDataFactory = changeDataFactory;
|
this.changeDataFactory = changeDataFactory;
|
||||||
this.queryProvider = queryProvider;
|
this.queryProvider = queryProvider;
|
||||||
this.repoManagerProvider = repoManagerProvider;
|
this.repoManagerProvider = repoManagerProvider;
|
||||||
|
this.mergeSuperSetComputation = mergeSuperSetComputation;
|
||||||
this.permissionBackend = permissionBackend;
|
this.permissionBackend = permissionBackend;
|
||||||
queryCache = new HashMap<>();
|
|
||||||
heads = new HashMap<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
|
public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
|
||||||
@@ -131,6 +112,11 @@ public class MergeSuperSet {
|
|||||||
public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
|
public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
|
||||||
throws IOException, OrmException, PermissionBackendException {
|
throws IOException, OrmException, PermissionBackendException {
|
||||||
try {
|
try {
|
||||||
|
if (orm == null) {
|
||||||
|
orm = repoManagerProvider.get();
|
||||||
|
closeOrm = true;
|
||||||
|
}
|
||||||
|
|
||||||
ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
|
ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
|
||||||
ChangeSet changeSet =
|
ChangeSet changeSet =
|
||||||
new ChangeSet(
|
new ChangeSet(
|
||||||
@@ -138,7 +124,7 @@ public class MergeSuperSet {
|
|||||||
if (Submit.wholeTopicEnabled(cfg)) {
|
if (Submit.wholeTopicEnabled(cfg)) {
|
||||||
return completeChangeSetIncludingTopics(db, changeSet, user);
|
return completeChangeSetIncludingTopics(db, changeSet, user);
|
||||||
}
|
}
|
||||||
return completeChangeSetWithoutTopic(db, changeSet, user);
|
return mergeSuperSetComputation.get().completeWithoutTopic(db, orm, changeSet, user);
|
||||||
} finally {
|
} finally {
|
||||||
if (closeOrm && orm != null) {
|
if (closeOrm && orm != null) {
|
||||||
orm.close();
|
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
|
* Completes {@code changeSet} with any additional changes from its topics
|
||||||
*
|
*
|
||||||
* <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
|
* <p>{@link #completeChangeSetIncludingTopics} calls this repeatedly, alternating with {@link
|
||||||
* #completeChangeSetWithoutTopic}, to discover what additional changes should be submitted with a
|
* MergeSuperSetComputation#completeWithoutTopic(ReviewDb, MergeOpRepoManager, ChangeSet,
|
||||||
* change until the set stops growing.
|
* 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
|
* <p>{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to
|
||||||
* avoid wasted work.
|
* avoid wasted work.
|
||||||
@@ -324,7 +162,7 @@ public class MergeSuperSet {
|
|||||||
if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
|
if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (ChangeData topicCd : query().byTopicOpen(topic)) {
|
for (ChangeData topicCd : byTopicOpen(topic)) {
|
||||||
if (canRead(db, user, topicCd)) {
|
if (canRead(db, user, topicCd)) {
|
||||||
visibleChanges.add(topicCd);
|
visibleChanges.add(topicCd);
|
||||||
} else {
|
} else {
|
||||||
@@ -340,7 +178,7 @@ public class MergeSuperSet {
|
|||||||
if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
|
if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (ChangeData topicCd : query().byTopicOpen(topic)) {
|
for (ChangeData topicCd : byTopicOpen(topic)) {
|
||||||
nonVisibleChanges.add(topicCd);
|
nonVisibleChanges.add(topicCd);
|
||||||
}
|
}
|
||||||
topicsSeen.add(topic);
|
topicsSeen.add(topic);
|
||||||
@@ -360,36 +198,15 @@ public class MergeSuperSet {
|
|||||||
oldSeen = seen;
|
oldSeen = seen;
|
||||||
|
|
||||||
changeSet = topicClosure(db, changeSet, user, topicsSeen, visibleTopicsSeen);
|
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();
|
seen = topicsSeen.size() + visibleTopicsSeen.size();
|
||||||
} while (seen != oldSeen);
|
} while (seen != oldSeen);
|
||||||
return changeSet;
|
return changeSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private InternalChangeQuery query() {
|
private List<ChangeData> byTopicOpen(String topic) throws OrmException {
|
||||||
// Request fields required for completing the ChangeSet and converting to
|
return query(queryProvider.get()).byTopicOpen(topic);
|
||||||
// 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 boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
|
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.config.TrackingFootersProvider;
|
||||||
import com.google.gerrit.server.git.GarbageCollection;
|
import com.google.gerrit.server.git.GarbageCollection;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
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.PerThreadRequestScope;
|
||||||
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
|
||||||
import com.google.gerrit.server.git.SendEmailExecutor;
|
import com.google.gerrit.server.git.SendEmailExecutor;
|
||||||
@@ -202,7 +203,7 @@ public class InMemoryModule extends FactoryModule {
|
|||||||
return CanonicalWebUrlProvider.class;
|
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(
|
install(
|
||||||
new AbstractModule() {
|
new AbstractModule() {
|
||||||
@Override
|
@Override
|
||||||
@@ -220,6 +221,7 @@ public class InMemoryModule extends FactoryModule {
|
|||||||
install(new SignedTokenEmailTokenVerifier.Module());
|
install(new SignedTokenEmailTokenVerifier.Module());
|
||||||
install(new GpgModule(cfg));
|
install(new GpgModule(cfg));
|
||||||
install(new InMemoryAccountPatchReviewStore.Module());
|
install(new InMemoryAccountPatchReviewStore.Module());
|
||||||
|
install(new LocalMergeSuperSetComputation.Module());
|
||||||
|
|
||||||
bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
|
bind(AllAccountsIndexer.class).toProvider(Providers.of(null));
|
||||||
bind(AllChangesIndexer.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.events.StreamEventsApiListener;
|
||||||
import com.google.gerrit.server.git.GarbageCollectionModule;
|
import com.google.gerrit.server.git.GarbageCollectionModule;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManagerModule;
|
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.SearchingChangeCacheImpl;
|
||||||
import com.google.gerrit.server.git.WorkQueue;
|
import com.google.gerrit.server.git.WorkQueue;
|
||||||
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
|
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(cfgInjector.getInstance(MailReceiver.Module.class));
|
||||||
modules.add(new SmtpEmailSender.Module());
|
modules.add(new SmtpEmailSender.Module());
|
||||||
modules.add(new SignedTokenEmailTokenVerifier.Module());
|
modules.add(new SignedTokenEmailTokenVerifier.Module());
|
||||||
|
modules.add(new LocalMergeSuperSetComputation.Module());
|
||||||
|
|
||||||
// Plugin module needs to be inserted *before* the index module.
|
// Plugin module needs to be inserted *before* the index module.
|
||||||
// There is the concept of LifecycleModule, in Gerrit's own extension
|
// There is the concept of LifecycleModule, in Gerrit's own extension
|
||||||
|
Reference in New Issue
Block a user