| // Copyright (C) 2013 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.base.Function; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.util.concurrent.AsyncFunction; |
| import com.google.common.util.concurrent.CheckedFuture; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.ListeningExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| 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.client.RefNames; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority; |
| import com.google.gerrit.server.change.Mergeable.MergeableInfo; |
| import com.google.gerrit.server.git.MetaDataUpdate; |
| import com.google.gerrit.server.git.ProjectConfig; |
| import com.google.gerrit.server.git.WorkQueue.Executor; |
| import com.google.gerrit.server.index.ChangeIndexer; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.util.RequestContext; |
| import com.google.gerrit.server.util.ThreadLocalRequestContext; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.gwtorm.server.SchemaFactory; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.ProvisionException; |
| |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| |
| public class MergeabilityChecker implements GitReferenceUpdatedListener { |
| private static final Logger log = LoggerFactory |
| .getLogger(MergeabilityChecker.class); |
| |
| private static final Function<Exception, IOException> MAPPER = |
| new Function<Exception, IOException>() { |
| @Override |
| public IOException apply(Exception in) { |
| if (in instanceof IOException) { |
| return (IOException) in; |
| } else if (in instanceof ExecutionException |
| && in.getCause() instanceof IOException) { |
| return (IOException) in.getCause(); |
| } else { |
| return new IOException(in); |
| } |
| } |
| }; |
| |
| public class Check { |
| private List<Change> changes; |
| private List<Branch.NameKey> branches; |
| private List<Project.NameKey> projects; |
| private boolean force; |
| private boolean reindex; |
| private boolean interactive; |
| |
| private Check() { |
| changes = Lists.newArrayListWithExpectedSize(1); |
| branches = Lists.newArrayListWithExpectedSize(1); |
| projects = Lists.newArrayListWithExpectedSize(1); |
| interactive = true; |
| } |
| |
| public Check addChange(Change change) { |
| changes.add(change); |
| return this; |
| } |
| |
| public Check addBranch(Branch.NameKey branch) { |
| branches.add(branch); |
| interactive = false; |
| return this; |
| } |
| |
| public Check addProject(Project.NameKey project) { |
| projects.add(project); |
| interactive = false; |
| return this; |
| } |
| |
| /** Force reindexing regardless of whether mergeable flag was modified. */ |
| public Check reindex() { |
| reindex = true; |
| return this; |
| } |
| |
| /** Force mergeability check even if change is not stale. */ |
| private Check force() { |
| force = true; |
| return this; |
| } |
| |
| private ListeningExecutorService getExecutor() { |
| return interactive ? interactiveExecutor : backgroundExecutor; |
| } |
| |
| public CheckedFuture<?, IOException> runAsync() { |
| final ListeningExecutorService executor = getExecutor(); |
| ListenableFuture<List<Change>> getChanges; |
| if (branches.isEmpty() && projects.isEmpty()) { |
| getChanges = Futures.immediateFuture(changes); |
| } else { |
| getChanges = executor.submit( |
| new Callable<List<Change>>() { |
| @Override |
| public List<Change> call() throws OrmException { |
| return getChanges(); |
| } |
| }); |
| } |
| |
| return Futures.makeChecked(Futures.transform(getChanges, |
| new AsyncFunction<List<Change>, List<Object>>() { |
| @Override |
| public ListenableFuture<List<Object>> apply(List<Change> changes) { |
| List<ListenableFuture<?>> result = |
| Lists.newArrayListWithCapacity(changes.size()); |
| for (final Change c : changes) { |
| ListenableFuture<Boolean> b = |
| executor.submit(new Task(c, force)); |
| if (reindex) { |
| result.add(Futures.transform( |
| b, new AsyncFunction<Boolean, Object>() { |
| @SuppressWarnings("unchecked") |
| @Override |
| public ListenableFuture<Object> apply( |
| Boolean indexUpdated) throws Exception { |
| if (!indexUpdated) { |
| return (ListenableFuture<Object>) |
| indexer.indexAsync(c.getId()); |
| } |
| return Futures.immediateFuture(null); |
| } |
| })); |
| } else { |
| result.add(b); |
| } |
| } |
| return Futures.allAsList(result); |
| } |
| }), MAPPER); |
| } |
| |
| public void run() throws IOException { |
| try { |
| runAsync().checkedGet(); |
| } catch (Exception e) { |
| Throwables.propagateIfPossible(e, IOException.class); |
| throw MAPPER.apply(e); |
| } |
| } |
| |
| private List<Change> getChanges() throws OrmException { |
| ReviewDb db = schemaFactory.open(); |
| try { |
| List<Change> results = Lists.newArrayList(); |
| results.addAll(changes); |
| for (Project.NameKey p : projects) { |
| Iterables.addAll(results, db.changes().byProjectOpenAll(p)); |
| } |
| for (Branch.NameKey b : branches) { |
| Iterables.addAll(results, db.changes().byBranchOpenAll(b)); |
| } |
| return results; |
| } catch (OrmException e) { |
| log.error("Failed to fetch changes for mergeability check", e); |
| throw e; |
| } finally { |
| db.close(); |
| } |
| } |
| } |
| |
| private final ThreadLocalRequestContext tl; |
| private final SchemaFactory<ReviewDb> schemaFactory; |
| private final IdentifiedUser.GenericFactory identifiedUserFactory; |
| private final ChangeControl.GenericFactory changeControlFactory; |
| private final Provider<Mergeable> mergeable; |
| private final ChangeIndexer indexer; |
| private final ListeningExecutorService backgroundExecutor; |
| private final ListeningExecutorService interactiveExecutor; |
| private final MergeabilityCheckQueue mergeabilityCheckQueue; |
| private final MetaDataUpdate.Server metaDataUpdateFactory; |
| |
| @Inject |
| public MergeabilityChecker(ThreadLocalRequestContext tl, |
| SchemaFactory<ReviewDb> schemaFactory, |
| IdentifiedUser.GenericFactory identifiedUserFactory, |
| ChangeControl.GenericFactory changeControlFactory, |
| Provider<Mergeable> mergeable, ChangeIndexer indexer, |
| @MergeabilityChecksExecutor(Priority.BACKGROUND) |
| Executor backgroundExecutor, |
| @MergeabilityChecksExecutor(Priority.INTERACTIVE) |
| Executor interactiveExecutor, |
| MergeabilityCheckQueue mergeabilityCheckQueue, |
| MetaDataUpdate.Server metaDataUpdateFactory) { |
| this.tl = tl; |
| this.schemaFactory = schemaFactory; |
| this.identifiedUserFactory = identifiedUserFactory; |
| this.changeControlFactory = changeControlFactory; |
| this.mergeable = mergeable; |
| this.indexer = indexer; |
| this.backgroundExecutor = |
| MoreExecutors.listeningDecorator(backgroundExecutor); |
| this.interactiveExecutor = |
| MoreExecutors.listeningDecorator(interactiveExecutor); |
| this.mergeabilityCheckQueue = mergeabilityCheckQueue; |
| this.metaDataUpdateFactory = metaDataUpdateFactory; |
| } |
| |
| public Check newCheck() { |
| return new Check(); |
| } |
| |
| @Override |
| public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) { |
| String ref = event.getRefName(); |
| if (ref.startsWith(Constants.R_HEADS) || ref.equals(RefNames.REFS_CONFIG)) { |
| Branch.NameKey branch = new Branch.NameKey( |
| new Project.NameKey(event.getProjectName()), ref); |
| newCheck().addBranch(branch).runAsync(); |
| } |
| if (ref.equals(RefNames.REFS_CONFIG)) { |
| Project.NameKey p = new Project.NameKey(event.getProjectName()); |
| try { |
| ProjectConfig oldCfg = parseConfig(p, event.getOldObjectId()); |
| ProjectConfig newCfg = parseConfig(p, event.getNewObjectId()); |
| if (recheckMerges(oldCfg, newCfg)) { |
| newCheck().addProject(p).force().runAsync(); |
| } |
| } catch (ConfigInvalidException | IOException e) { |
| String msg = "Failed to update mergeability flags for project " + p.get() |
| + " on update of " + RefNames.REFS_CONFIG; |
| log.error(msg, e); |
| throw new RuntimeException(msg, e); |
| } |
| } |
| } |
| |
| private boolean recheckMerges(ProjectConfig oldCfg, ProjectConfig newCfg) { |
| if (oldCfg == null || newCfg == null) { |
| return true; |
| } |
| return !oldCfg.getProject().getSubmitType().equals(newCfg.getProject().getSubmitType()) |
| || oldCfg.getProject().getUseContentMerge() != newCfg.getProject().getUseContentMerge() |
| || (oldCfg.getRulesId() == null |
| ? newCfg.getRulesId() != null |
| : !oldCfg.getRulesId().equals(newCfg.getRulesId())); |
| } |
| |
| private ProjectConfig parseConfig(Project.NameKey p, String idStr) |
| throws IOException, ConfigInvalidException, RepositoryNotFoundException { |
| ObjectId id = ObjectId.fromString(idStr); |
| if (ObjectId.zeroId().equals(id)) { |
| return null; |
| } |
| return ProjectConfig.read(metaDataUpdateFactory.create(p), id); |
| } |
| |
| private class Task implements Callable<Boolean> { |
| private final Change change; |
| private final boolean force; |
| |
| private ReviewDb reviewDb; |
| |
| Task(Change change, boolean force) { |
| this.change = change; |
| this.force = force; |
| } |
| |
| @Override |
| public Boolean call() throws Exception { |
| mergeabilityCheckQueue.updatingMergeabilityFlag(change, force); |
| |
| RequestContext context = new RequestContext() { |
| @Override |
| public CurrentUser getCurrentUser() { |
| return identifiedUserFactory.create(change.getOwner()); |
| } |
| |
| @Override |
| public Provider<ReviewDb> getReviewDbProvider() { |
| return new Provider<ReviewDb>() { |
| @Override |
| public ReviewDb get() { |
| if (reviewDb == null) { |
| try { |
| reviewDb = schemaFactory.open(); |
| } catch (OrmException e) { |
| throw new ProvisionException("Cannot open ReviewDb", e); |
| } |
| } |
| return reviewDb; |
| } |
| }; |
| } |
| }; |
| RequestContext old = tl.setContext(context); |
| ReviewDb db = context.getReviewDbProvider().get(); |
| try { |
| PatchSet ps = db.patchSets().get(change.currentPatchSetId()); |
| Mergeable m = mergeable.get(); |
| m.setForce(force); |
| |
| ChangeControl control = |
| changeControlFactory.controlFor(change.getId(), context.getCurrentUser()); |
| MergeableInfo info = m.apply( |
| new RevisionResource(new ChangeResource(control), ps)); |
| return change.isMergeable() != info.mergeable; |
| } catch (ResourceConflictException e) { |
| // change is closed |
| return false; |
| } catch (Exception e) { |
| String msg = "Failed to update mergeability flags for project " |
| + change.getDest().getParentKey() + " on update of " |
| + change.getDest().get(); |
| log.error(msg, e); |
| throw e; |
| } finally { |
| tl.setContext(old); |
| if (reviewDb != null) { |
| reviewDb.close(); |
| reviewDb = null; |
| } |
| } |
| } |
| } |
| } |