com.google.gerrit.server.change.MergeabilityChecker.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.change.MergeabilityChecker.java

Source

// 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;
                }
            }
        }
    }
}