com.google.gerrit.server.git.SubmoduleOp.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.git.SubmoduleOp.java

Source

// Copyright (C) 2011 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.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.util.SubmoduleSectionParser;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SubmoduleOp {
    private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
    private static final String GIT_MODULES = ".gitmodules";

    private final Provider<String> urlProvider;
    private final PersonIdent myIdent;
    private final GitRepositoryManager repoManager;
    private final GitReferenceUpdated gitRefUpdated;
    private final Set<Branch.NameKey> updatedSubscribers;
    private final Account account;
    private final ChangeHooks changeHooks;
    private final SubmoduleSectionParser.Factory subSecParserFactory;
    private final boolean verboseSuperProject;

    @Inject
    public SubmoduleOp(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
            @GerritPersonIdent PersonIdent myIdent, @GerritServerConfig Config cfg,
            GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated, @Nullable Account account,
            ChangeHooks changeHooks, SubmoduleSectionParser.Factory subSecParserFactory) {
        this.urlProvider = urlProvider;
        this.myIdent = myIdent;
        this.repoManager = repoManager;
        this.gitRefUpdated = gitRefUpdated;
        this.account = account;
        this.changeHooks = changeHooks;
        this.subSecParserFactory = subSecParserFactory;
        this.verboseSuperProject = cfg.getBoolean("submodule", "verboseSuperprojectUpdate", true);

        updatedSubscribers = new HashSet<>();
    }

    void updateSubmoduleSubscriptions(ReviewDb db, Set<Branch.NameKey> branches) throws SubmoduleException {
        for (Branch.NameKey branch : branches) {
            updateSubmoduleSubscriptions(db, branch);
        }
    }

    void updateSubmoduleSubscriptions(ReviewDb db, Branch.NameKey destBranch) throws SubmoduleException {
        if (urlProvider.get() == null) {
            logAndThrowSubmoduleException("Cannot establish canonical web url used "
                    + "to access gerrit. It should be provided in gerrit.config file.");
        }
        try (Repository repo = repoManager.openRepository(destBranch.getParentKey());
                RevWalk rw = new RevWalk(repo)) {

            ObjectId id = repo.resolve(destBranch.get());
            if (id == null) {
                logAndThrowSubmoduleException("Cannot resolve submodule destination branch " + destBranch);
            }
            RevCommit commit = rw.parseCommit(id);

            Set<SubmoduleSubscription> oldSubscriptions = Sets
                    .newHashSet(db.submoduleSubscriptions().bySuperProject(destBranch));

            Set<SubmoduleSubscription> newSubscriptions;
            TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
            if (tw != null && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0))
                    || FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
                BlobBasedConfig bbc = new BlobBasedConfig(null, repo, commit, GIT_MODULES);

                String thisServer = new URI(urlProvider.get()).getHost();

                newSubscriptions = subSecParserFactory.create(bbc, thisServer, destBranch).parseAllSections();
            } else {
                newSubscriptions = Collections.emptySet();
            }

            Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
            for (SubmoduleSubscription s : newSubscriptions) {
                if (oldSubscriptions.contains(s)) {
                    alreadySubscribeds.add(s);
                }
            }

            oldSubscriptions.removeAll(newSubscriptions);
            newSubscriptions.removeAll(alreadySubscribeds);

            if (!oldSubscriptions.isEmpty()) {
                db.submoduleSubscriptions().delete(oldSubscriptions);
            }
            if (!newSubscriptions.isEmpty()) {
                db.submoduleSubscriptions().insert(newSubscriptions);
            }

        } catch (OrmException e) {
            logAndThrowSubmoduleException(
                    "Database problem at update of subscriptions table from " + GIT_MODULES + " file.", e);
        } catch (ConfigInvalidException e) {
            logAndThrowSubmoduleException(
                    "Problem at update of subscriptions table: " + GIT_MODULES + " config file is invalid.", e);
        } catch (IOException e) {
            logAndThrowSubmoduleException("Problem at update of subscriptions table from " + GIT_MODULES + ".", e);
        } catch (URISyntaxException e) {
            logAndThrowSubmoduleException("Incorrect gerrit canonical web url provided in gerrit.config file.", e);
        }
    }

    protected void updateSuperProjects(ReviewDb db, Collection<Branch.NameKey> updatedBranches)
            throws SubmoduleException {
        try {
            // These (repo/branch) will be updated later with all the given
            // individual submodule subscriptions
            Multimap<Branch.NameKey, SubmoduleSubscription> targets = HashMultimap.create();

            for (Branch.NameKey updatedBranch : updatedBranches) {
                for (SubmoduleSubscription sub : db.submoduleSubscriptions().bySubmodule(updatedBranch)) {
                    targets.put(sub.getSuperProject(), sub);
                }
            }
            updatedSubscribers.addAll(updatedBranches);
            // Update subscribers.
            for (Branch.NameKey dest : targets.keySet()) {
                try {
                    if (!updatedSubscribers.add(dest)) {
                        log.error("Possible circular subscription involving " + dest);
                    } else {
                        updateGitlinks(db, dest, targets.get(dest));
                    }
                } catch (SubmoduleException e) {
                    log.warn("Cannot update gitlinks for " + dest, e);
                }
            }
        } catch (OrmException e) {
            logAndThrowSubmoduleException("Cannot read subscription records", e);
        }
    }

    /**
     * Update the submodules in one branch of one repository.
     *
     * @param subscriber the branch of the repository which should be changed.
     * @param updates submodule updates which should be updated to.
     * @throws SubmoduleException
     */
    private void updateGitlinks(ReviewDb db, Branch.NameKey subscriber, Collection<SubmoduleSubscription> updates)
            throws SubmoduleException {
        PersonIdent author = null;
        StringBuilder msgbuf = new StringBuilder("Updated git submodules\n\n");
        boolean sameAuthorForAll = true;

        try (Repository pdb = repoManager.openRepository(subscriber.getParentKey())) {
            if (pdb.getRef(subscriber.get()) == null) {
                throw new SubmoduleException("The branch was probably deleted from the subscriber repository");
            }

            DirCache dc = readTree(pdb, pdb.getRef(subscriber.get()));
            DirCacheEditor ed = dc.editor();

            for (SubmoduleSubscription s : updates) {
                try (Repository subrepo = repoManager.openRepository(s.getSubmodule().getParentKey());
                        RevWalk rw = CodeReviewCommit.newRevWalk(subrepo)) {
                    Ref ref = subrepo.getRefDatabase().exactRef(s.getSubmodule().get());
                    if (ref == null) {
                        ed.add(new DeletePath(s.getPath()));
                        continue;
                    }

                    final ObjectId updateTo = ref.getObjectId();
                    RevCommit newCommit = rw.parseCommit(updateTo);

                    if (author == null) {
                        author = newCommit.getAuthorIdent();
                    } else if (!author.equals(newCommit.getAuthorIdent())) {
                        sameAuthorForAll = false;
                    }

                    DirCacheEntry dce = dc.getEntry(s.getPath());
                    ObjectId oldId;
                    if (dce != null) {
                        if (!dce.getFileMode().equals(FileMode.GITLINK)) {
                            log.error("Requested to update gitlink " + s.getPath() + " in "
                                    + s.getSubmodule().getParentKey().get() + " but entry "
                                    + "doesn't have gitlink file mode.");
                            continue;
                        }
                        oldId = dce.getObjectId();
                    } else {
                        // This submodule did not exist before. We do not want to add
                        // the full submodule history to the commit message, so omit it.
                        oldId = updateTo;
                    }

                    ed.add(new PathEdit(s.getPath()) {
                        @Override
                        public void apply(DirCacheEntry ent) {
                            ent.setFileMode(FileMode.GITLINK);
                            ent.setObjectId(updateTo);
                        }
                    });
                    if (verboseSuperProject) {
                        msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
                        msgbuf.append(" " + s.getSubmodule().getShortName());
                        msgbuf.append(" " + updateTo.getName());
                        msgbuf.append("\n\n");

                        try {
                            rw.markStart(newCommit);
                            rw.markUninteresting(rw.parseCommit(oldId));
                            for (RevCommit c : rw) {
                                msgbuf.append(c.getFullMessage() + "\n\n");
                            }
                        } catch (IOException e) {
                            logAndThrowSubmoduleException(
                                    "Could not perform a revwalk to " + "create superproject commit message", e);
                        }
                    }
                }
            }
            ed.finish();

            if (!sameAuthorForAll || author == null) {
                author = myIdent;
            }

            ObjectInserter oi = pdb.newObjectInserter();
            ObjectId tree = dc.writeTree(oi);

            ObjectId currentCommitId = pdb.getRef(subscriber.get()).getObjectId();

            CommitBuilder commit = new CommitBuilder();
            commit.setTreeId(tree);
            commit.setParentIds(new ObjectId[] { currentCommitId });
            commit.setAuthor(author);
            commit.setCommitter(myIdent);
            commit.setMessage(msgbuf.toString());
            oi.insert(commit);
            oi.flush();

            ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());

            final RefUpdate rfu = pdb.updateRef(subscriber.get());
            rfu.setForceUpdate(false);
            rfu.setNewObjectId(commitId);
            rfu.setExpectedOldObjectId(currentCommitId);
            rfu.setRefLogMessage("Submit to " + subscriber.getParentKey().get(), true);

            switch (rfu.update()) {
            case NEW:
            case FAST_FORWARD:
                gitRefUpdated.fire(subscriber.getParentKey(), rfu);
                changeHooks.doRefUpdatedHook(subscriber, rfu, account);
                // TODO since this is performed "in the background" no mail will be
                // sent to inform users about the updated branch
                break;

            default:
                throw new IOException(rfu.getResult().name());
            }
            // Recursive call: update subscribers of the subscriber
            updateSuperProjects(db, Sets.newHashSet(subscriber));
        } catch (IOException e) {
            throw new SubmoduleException("Cannot update gitlinks for " + subscriber.get(), e);
        }
    }

    private static DirCache readTree(final Repository pdb, final Ref branch)
            throws MissingObjectException, IncorrectObjectTypeException, IOException {
        try (RevWalk rw = new RevWalk(pdb)) {
            final DirCache dc = DirCache.newInCore();
            final DirCacheBuilder b = dc.builder();
            b.addTree(new byte[0], // no prefix path
                    DirCacheEntry.STAGE_0, // standard stage
                    pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
            b.finish();
            return dc;
        }
    }

    private static void logAndThrowSubmoduleException(final String errorMsg, final Exception e)
            throws SubmoduleException {
        log.error(errorMsg, e);
        throw new SubmoduleException(errorMsg, e);
    }

    private static void logAndThrowSubmoduleException(final String errorMsg) throws SubmoduleException {
        log.error(errorMsg);
        throw new SubmoduleException(errorMsg);
    }
}