| // 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.exactRef(subscriber.get()) == null) { |
| throw new SubmoduleException( |
| "The branch was probably deleted from the subscriber repository"); |
| } |
| |
| DirCache dc = readTree(pdb, pdb.exactRef(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.exactRef(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); |
| } |
| } |