All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.git.MergeSuperSet Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.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.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
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.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
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.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.
 *
 * 

This includes all parents between a change and the tip of its target branch for the * merging/rebasing submit strategies. For the cherry-pick strategy no additional changes are * included. * *

If change.submitWholeTopic is enabled, also all changes of the topic and their parents are * included. */ public class MergeSuperSet { private static final Logger log = LoggerFactory.getLogger(MergeSuperSet.class); public static void reloadChanges(ChangeSet cs) throws OrmException { // Clear exactly the fields requested by query() below. for (ChangeData cd : cs.changes()) { cd.reloadChange(); cd.setPatchSets(null); cd.setMergeable(null); } } @AutoValue abstract static class QueryKey { private static QueryKey create(Branch.NameKey branch, Iterable hashes) { return new AutoValue_MergeSuperSet_QueryKey(branch, ImmutableSet.copyOf(hashes)); } abstract Branch.NameKey branch(); abstract ImmutableSet hashes(); } private final ChangeData.Factory changeDataFactory; private final Provider queryProvider; private final Provider repoManagerProvider; private final Config cfg; private final Map> queryCache; private final Map> heads; private MergeOpRepoManager orm; private boolean closeOrm; @Inject MergeSuperSet( @GerritServerConfig Config cfg, ChangeData.Factory changeDataFactory, Provider queryProvider, Provider repoManagerProvider) { this.cfg = cfg; this.changeDataFactory = changeDataFactory; this.queryProvider = queryProvider; this.repoManagerProvider = repoManagerProvider; queryCache = new HashMap<>(); heads = new HashMap<>(); } public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) { checkState(this.orm == null); this.orm = checkNotNull(orm); closeOrm = false; return this; } public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user) throws IOException, OrmException { try { ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId()); cd.changeControl(user); ChangeSet cs = new ChangeSet(cd, cd.changeControl().isVisible(db, cd)); if (Submit.wholeTopicEnabled(cfg)) { return completeChangeSetIncludingTopics(db, cs, user); } return completeChangeSetWithoutTopic(db, cs, user); } finally { if (closeOrm && orm != null) { orm.close(); orm = null; } } } private SubmitType submitType(ChangeData cd, PatchSet ps, boolean visible) throws OrmException { // Submit type prolog rules mean that the submit type can depend on the // submitting user and the content of the change. // // If the current user can see the change, run that evaluation to get a // preview of what would happen on submit. If the current user can't see // the change, instead of guessing who would do the submitting, rely on the // project configuration and ignore the prolog rule. If the prolog rule // doesn't match that, we may pick the wrong submit type and produce a // misleading (but still nonzero) count of the non visible changes that // would be submitted together with the visible ones. if (!visible) { return cd.changeControl().getProject().getSubmitType(); } SubmitTypeRecord str = ps == cd.currentPatchSet() ? cd.submitTypeRecord() : new SubmitRuleEvaluator(cd).setPatchSet(ps).getSubmitType(); if (!str.isOk()) { logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage); } return str.type; } private static ImmutableListMultimap byBranch( Iterable changes) throws OrmException { ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder(); for (ChangeData cd : changes) { builder.put(cd.change().getDest(), cd); } return builder.build(); } private Set walkChangesByHashes( Collection sourceCommits, Set ignoreHashes, OpenRepo or, Branch.NameKey b) throws IOException { Set 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 changes, CurrentUser user) throws IOException, OrmException { Collection visibleChanges = new ArrayList<>(); Collection 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 bc = byBranch(Iterables.concat(changes.changes(), changes.nonVisibleChanges())); for (Branch.NameKey b : bc.keySet()) { OpenRepo or = getRepo(b.getParentKey()); List visibleCommits = new ArrayList<>(); List nonVisibleCommits = new ArrayList<>(); for (ChangeData cd : bc.get(b)) { checkState( cd.hasChangeControl(), "completeChangeSet forgot to set changeControl for current user" + " at ChangeData creation time"); boolean visible = changes.ids().contains(cd.getId()); if (visible && !cd.changeControl().isVisible(db, 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; } Collection toWalk = visible ? visibleCommits : nonVisibleCommits; // Pick a revision to use for traversal. If any of the patch sets // is visible, we use the most recent one. Otherwise, use the current // patch set. PatchSet ps = cd.currentPatchSet(); boolean visiblePatchSet = visible; if (!cd.changeControl().isPatchVisible(ps, cd)) { Iterable visiblePatchSets = cd.visiblePatchSets(); if (Iterables.isEmpty(visiblePatchSets)) { visiblePatchSet = false; } else { ps = Iterables.getLast(visiblePatchSets); } } if (submitType(cd, ps, visiblePatchSet) == SubmitType.CHERRY_PICK) { if (visible) { visibleChanges.add(cd); } else { nonVisibleChanges.add(cd); } continue; } // Get the underlying git commit object String objIdStr = ps.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. toWalk.add(commit); } Set emptySet = Collections.emptySet(); Set visibleHashes = walkChangesByHashes(visibleCommits, emptySet, or, b); List cds = byCommitsOnBranchNotMerged(or, db, user, b, visibleHashes); for (ChangeData chd : cds) { chd.changeControl(user); visibleChanges.add(chd); } Set nonVisibleHashes = walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b); Iterables.addAll( nonVisibleChanges, byCommitsOnBranchNotMerged(or, db, user, 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 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 byCommitsOnBranchNotMerged( OpenRepo or, ReviewDb db, CurrentUser user, Branch.NameKey branch, Set hashes) throws OrmException, IOException { if (hashes.isEmpty()) { return ImmutableList.of(); } QueryKey k = QueryKey.create(branch, hashes); List cached = queryCache.get(k); if (cached != null) { return cached; } List result = new ArrayList<>(); Iterable destChanges = query().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes); for (ChangeData chd : destChanges) { chd.changeControl(user); result.add(chd); } queryCache.put(k, result); return result; } /** * Completes {@code cs} with any additional changes from its topics * *

{@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. * *

{@code topicsSeen} and {@code visibleTopicsSeen} keep track of topics already explored to * avoid wasted work. * * @return the resulting larger {@link ChangeSet} */ private ChangeSet topicClosure( ReviewDb db, ChangeSet cs, CurrentUser user, Set topicsSeen, Set visibleTopicsSeen) throws OrmException { List visibleChanges = new ArrayList<>(); List nonVisibleChanges = new ArrayList<>(); for (ChangeData cd : cs.changes()) { visibleChanges.add(cd); String topic = cd.change().getTopic(); if (Strings.isNullOrEmpty(topic) || visibleTopicsSeen.contains(topic)) { continue; } for (ChangeData topicCd : query().byTopicOpen(topic)) { try { topicCd.changeControl(user); if (topicCd.changeControl().isVisible(db, topicCd)) { visibleChanges.add(topicCd); } else { nonVisibleChanges.add(topicCd); } } catch (OrmException e) { if (e.getCause() instanceof NoSuchChangeException) { // Ignore and skip this change } else { throw e; } } } topicsSeen.add(topic); visibleTopicsSeen.add(topic); } for (ChangeData cd : cs.nonVisibleChanges()) { nonVisibleChanges.add(cd); String topic = cd.change().getTopic(); if (Strings.isNullOrEmpty(topic) || topicsSeen.contains(topic)) { continue; } for (ChangeData topicCd : query().byTopicOpen(topic)) { topicCd.changeControl(user); nonVisibleChanges.add(topicCd); } topicsSeen.add(topic); } return new ChangeSet(visibleChanges, nonVisibleChanges); } private ChangeSet completeChangeSetIncludingTopics( ReviewDb db, ChangeSet changes, CurrentUser user) throws IOException, OrmException { Set topicsSeen = new HashSet<>(); Set visibleTopicsSeen = new HashSet<>(); int oldSeen; int seen = 0; do { oldSeen = seen; changes = completeChangeSetWithoutTopic(db, changes, user); changes = topicClosure(db, changes, user, topicsSeen, visibleTopicsSeen); seen = topicsSeen.size() + visibleTopicsSeen.size(); } while (seen != oldSeen); return changes; } 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 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy