blob: 667045038e4a987a01f8c514af3acdee04d9e15b [file] [log] [blame]
// 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);
}
}