blob: edaed13239e242c03e8a2b2b6d54f047be78b3e0 [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.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubscribeSection;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
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.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
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.transport.RefSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class SubmoduleOp {
public interface Factory {
SubmoduleOp create(MergeOpRepoManager orm);
}
private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
private final GitModules.Factory gitmodulesFactory;
private final PersonIdent myIdent;
private final GitReferenceUpdated gitRefUpdated;
private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
private final Account account;
private final boolean verboseSuperProject;
private final boolean enableSuperProjectSubscriptions;
private final MergeOpRepoManager orm;
@AssistedInject
public SubmoduleOp(
GitModules.Factory gitmodulesFactory,
@GerritPersonIdent PersonIdent myIdent,
@GerritServerConfig Config cfg,
GitReferenceUpdated gitRefUpdated,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
@Nullable Account account,
@Assisted MergeOpRepoManager orm) {
this.gitmodulesFactory = gitmodulesFactory;
this.myIdent = myIdent;
this.gitRefUpdated = gitRefUpdated;
this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
this.account = account;
this.verboseSuperProject = cfg.getBoolean("submodule",
"verboseSuperprojectUpdate", true);
this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
"enableSuperProjectSubscriptions", true);
this.orm = orm;
}
public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
SubscribeSection s) throws IOException {
Collection<Branch.NameKey> ret = new ArrayList<>();
logDebug("Inspecting SubscribeSection " + s);
for (RefSpec r : s.getRefSpecs()) {
logDebug("Inspecting ref " + r);
if (r.matchSource(src.get())) {
if (r.getDestination() == null) {
// no need to care for wildcard, as we matched already
try {
orm.openRepo(s.getProject(), false);
} catch (NoSuchProjectException e) {
// A project listed a non existent project to be allowed
// to subscribe to it. Allow this for now.
continue;
}
OpenRepo or = orm.getRepo(s.getProject());
for (Ref ref : or.repo.getRefDatabase().getRefs(
RefNames.REFS_HEADS).values()) {
ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
}
} else if (r.isWildcard()) {
// refs/heads/*:refs/heads/*
ret.add(new Branch.NameKey(s.getProject(),
r.expandFromSource(src.get()).getDestination()));
} else {
// e.g. refs/heads/master:refs/heads/stable
ret.add(new Branch.NameKey(s.getProject(), r.getDestination()));
}
}
}
logDebug("Returning possible branches: " + ret +
"for project " + s.getProject());
return ret;
}
public Collection<SubmoduleSubscription>
superProjectSubscriptionsForSubmoduleBranch(Branch.NameKey srcBranch)
throws IOException {
logDebug("Calculating possible superprojects for " + srcBranch);
Collection<SubmoduleSubscription> ret = new ArrayList<>();
Project.NameKey srcProject = srcBranch.getParentKey();
ProjectConfig cfg = projectCache.get(srcProject).getConfig();
for (SubscribeSection s : projectStateFactory.create(cfg)
.getSubscribeSections(srcBranch)) {
logDebug("Checking subscribe section " + s);
Collection<Branch.NameKey> branches =
getDestinationBranches(srcBranch, s);
for (Branch.NameKey targetBranch : branches) {
Project.NameKey targetProject = targetBranch.getParentKey();
try {
orm.openRepo(targetProject, false);
OpenRepo or = orm.getRepo(targetProject);
ObjectId id = or.repo.resolve(targetBranch.get());
if (id == null) {
logDebug("The branch " + targetBranch + " doesn't exist.");
continue;
}
} catch (NoSuchProjectException e) {
logDebug("The project " + targetProject + " doesn't exist");
continue;
}
GitModules m = gitmodulesFactory.create(targetBranch, orm);
for (SubmoduleSubscription ss : m.subscribedTo(srcBranch)) {
logDebug("Checking SubmoduleSubscription " + ss);
if (projectCache.get(ss.getSubmodule().getParentKey()) != null) {
logDebug("Adding SubmoduleSubscription " + ss);
ret.add(ss);
}
}
}
}
logDebug("Calculated superprojects for " + srcBranch + " are " + ret);
return ret;
}
protected void updateSuperProjects(Collection<Branch.NameKey> updatedBranches)
throws SubmoduleException {
if (!enableSuperProjectSubscriptions) {
logDebug("Updating superprojects disabled");
return;
}
logDebug("Updating superprojects");
Multimap<Branch.NameKey, SubmoduleSubscription> targets =
HashMultimap.create();
for (Branch.NameKey updatedBranch : updatedBranches) {
logDebug("Now processing " + updatedBranch);
Set<Branch.NameKey> checkedTargets = new HashSet<>();
Set<Branch.NameKey> targetsToProcess = new HashSet<>();
targetsToProcess.add(updatedBranch);
while (!targetsToProcess.isEmpty()) {
Set<Branch.NameKey> newTargets = new HashSet<>();
for (Branch.NameKey b : targetsToProcess) {
try {
Collection<SubmoduleSubscription> subs =
superProjectSubscriptionsForSubmoduleBranch(b);
for (SubmoduleSubscription sub : subs) {
Branch.NameKey dst = sub.getSuperProject();
targets.put(dst, sub);
newTargets.add(dst);
}
} catch (IOException e) {
throw new SubmoduleException("Cannot find superprojects for " + b, e);
}
}
logDebug("adding to done " + targetsToProcess);
checkedTargets.addAll(targetsToProcess);
logDebug("completely done with " + checkedTargets);
Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
intersection.retainAll(newTargets);
if (!intersection.isEmpty()) {
throw new SubmoduleException(
"Possible circular subscription involving " + updatedBranch);
}
targetsToProcess = newTargets;
}
}
for (Branch.NameKey dst : targets.keySet()) {
try {
updateGitlinks(dst, targets.get(dst));
} catch (SubmoduleException e) {
throw new SubmoduleException("Cannot update gitlinks for " + dst, 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(Branch.NameKey subscriber,
Collection<SubmoduleSubscription> updates)
throws SubmoduleException {
PersonIdent author = null;
StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
boolean sameAuthorForAll = true;
try {
orm.openRepo(subscriber.getParentKey(), false);
} catch (NoSuchProjectException | IOException e) {
throw new SubmoduleException("Cannot access superproject", e);
}
OpenRepo or = orm.getRepo(subscriber.getParentKey());
try {
Ref r = or.repo.exactRef(subscriber.get());
if (r == null) {
throw new SubmoduleException(
"The branch was probably deleted from the subscriber repository");
}
DirCache dc = readTree(r, or.rw);
DirCacheEditor ed = dc.editor();
for (SubmoduleSubscription s : updates) {
try {
orm.openRepo(s.getSubmodule().getParentKey(), false);
} catch (NoSuchProjectException | IOException e) {
throw new SubmoduleException("Cannot access submodule", e);
}
OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
Repository subrepo = subOr.repo;
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 = subOr.rw.parseCommit(updateTo);
subOr.rw.parseBody(newCommit);
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 {
subOr.rw.resetRetain(subOr.canMergeFlag);
subOr.rw.markStart(newCommit);
subOr.rw.markUninteresting(subOr.rw.parseCommit(oldId));
for (RevCommit c : subOr.rw) {
subOr.rw.parseBody(c);
msgbuf.append(c.getFullMessage() + "\n\n");
}
} catch (IOException e) {
throw new SubmoduleException("Could not perform a revwalk to "
+ "create superproject commit message", e);
}
}
}
ed.finish();
if (!sameAuthorForAll || author == null) {
author = myIdent;
}
ObjectInserter oi = or.repo.newObjectInserter();
ObjectId tree = dc.writeTree(oi);
ObjectId currentCommitId =
or.repo.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 = or.repo.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, account);
// TODO since this is performed "in the background" no mail will be
// sent to inform users about the updated branch
break;
case FORCED:
case IO_FAILURE:
case LOCK_FAILURE:
case NOT_ATTEMPTED:
case NO_CHANGE:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
default:
throw new IOException(rfu.getResult().name());
}
} catch (IOException e) {
throw new SubmoduleException("Cannot update gitlinks for "
+ subscriber.get(), e);
}
}
private static DirCache readTree(final Ref branch, RevWalk rw)
throws MissingObjectException, IncorrectObjectTypeException,
IOException {
final DirCache dc = DirCache.newInCore();
final DirCacheBuilder b = dc.builder();
b.addTree(new byte[0], // no prefix path
DirCacheEntry.STAGE_0, // standard stage
rw.getObjectReader(), rw.parseTree(branch.getObjectId()));
b.finish();
return dc;
}
private void logDebug(String msg, Object... args) {
if (log.isDebugEnabled()) {
log.debug("[" + orm.getSubmissionId() + "]" + msg, args);
}
}
}