// Copyright (C) 2008 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.gerrit.common.ChangeHookRunner; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.reviewdb.AbstractAgreement; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.AccountAgreement; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.AccountGroupAgreement; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.ChangeMessage; import com.google.gerrit.reviewdb.ContributorAgreement; import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSetAncestor; import com.google.gerrit.reviewdb.PatchSetApproval; import com.google.gerrit.reviewdb.PatchSetInfo; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.RevId; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.mail.CreateChangeSender; import com.google.gerrit.server.mail.EmailException; import com.google.gerrit.server.mail.MergedSender; import com.google.gerrit.server.mail.ReplacePatchSetSender; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.RefControl; import com.google.gwtorm.client.AtomicUpdate; import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; 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.notes.NoteMap; import org.eclipse.jgit.revwalk.FooterKey; import org.eclipse.jgit.revwalk.FooterLine; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevFlagSet; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.PostReceiveHook; import org.eclipse.jgit.transport.PreReceiveHook; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; /** Receives change upload using the Git receive-pack protocol. */ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { private static final Logger log = LoggerFactory.getLogger(ReceiveCommits.class); public static final String NEW_CHANGE = "refs/for/"; private static final Pattern NEW_PATCHSET = Pattern.compile("^refs/changes/(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$"); private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by"); private static final FooterKey TESTED_BY = new FooterKey("Tested-by"); private static final FooterKey CHANGE_ID = new FooterKey("Change-Id"); public interface Factory { ReceiveCommits create(ProjectControl projectControl, Repository repository); } public static class Capable { public static final Capable OK = new Capable("OK"); private final String message; Capable(String msg) { message = msg; } public String getMessage() { return message; } } private final Set<Account.Id> reviewerId = new HashSet<Account.Id>(); private final Set<Account.Id> ccId = new HashSet<Account.Id>(); private final IdentifiedUser currentUser; private final ReviewDb db; private final ApprovalTypes approvalTypes; private final AccountResolver accountResolver; private final CreateChangeSender.Factory createChangeSenderFactory; private final MergedSender.Factory mergedSenderFactory; private final ReplacePatchSetSender.Factory replacePatchSetFactory; private final ReplicationQueue replication; private final PatchSetInfoFactory patchSetInfoFactory; private final ChangeHookRunner hooks; private final String canonicalWebUrl; private final PersonIdent gerritIdent; private final TrackingFooters trackingFooters; private final ProjectControl projectControl; private final Project project; private final Repository repo; private final ReceivePack rp; private final NoteMap rejectCommits; private ReceiveCommand newChange; private Branch.NameKey destBranch; private RefControl destBranchCtl; private final List<Change.Id> allNewChanges = new ArrayList<Change.Id>(); private final Map<Change.Id, ReplaceRequest> replaceByChange = new HashMap<Change.Id, ReplaceRequest>(); private final Map<RevCommit, ReplaceRequest> replaceByCommit = new HashMap<RevCommit, ReplaceRequest>(); private Collection<ObjectId> existingObjects; private Map<ObjectId, Ref> refsById; private String destTopicName; @Inject ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes, final AccountResolver accountResolver, final CreateChangeSender.Factory createChangeSenderFactory, final MergedSender.Factory mergedSenderFactory, final ReplacePatchSetSender.Factory replacePatchSetFactory, final ReplicationQueue replication, final PatchSetInfoFactory patchSetInfoFactory, final ChangeHookRunner hooks, @CanonicalWebUrl @Nullable final String canonicalWebUrl, @GerritPersonIdent final PersonIdent gerritIdent, final TrackingFooters trackingFooters, @Assisted final ProjectControl projectControl, @Assisted final Repository repo) throws IOException { this.currentUser = (IdentifiedUser) projectControl.getCurrentUser(); this.db = db; this.approvalTypes = approvalTypes; this.accountResolver = accountResolver; this.createChangeSenderFactory = createChangeSenderFactory; this.mergedSenderFactory = mergedSenderFactory; this.replacePatchSetFactory = replacePatchSetFactory; this.replication = replication; this.patchSetInfoFactory = patchSetInfoFactory; this.hooks = hooks; this.canonicalWebUrl = canonicalWebUrl; this.gerritIdent = gerritIdent; this.trackingFooters = trackingFooters; this.projectControl = projectControl; this.project = projectControl.getProject(); this.repo = repo; this.rp = new ReceivePack(repo); this.rejectCommits = loadRejectCommitsMap(); rp.setAllowCreates(true); rp.setAllowDeletes(true); rp.setAllowNonFastForwards(true); rp.setCheckReceivedObjects(true); if (!projectControl.allRefsAreVisible()) { rp.setCheckReferencedObjectsAreReachable(true); rp.setRefFilter(new VisibleRefFilter(repo, projectControl, db, false)); } rp.setRefFilter(new ReceiveCommitsRefFilter(rp.getRefFilter())); rp.setPreReceiveHook(this); rp.setPostReceiveHook(this); } /** Add reviewers for new (or updated) changes. */ public void addReviewers(Collection<Account.Id> who) { reviewerId.addAll(who); } /** Add reviewers for new (or updated) changes. */ public void addExtraCC(Collection<Account.Id> who) { ccId.addAll(who); } /** @return the ReceivePack instance to speak the native Git protocol. */ public ReceivePack getReceivePack() { return rp; } /** Determine if the user can upload commits. */ public Capable canUpload() { if (!projectControl.canPushToAtLeastOneRef()) { String reqName = project.getName(); return new Capable("Upload denied for project '" + reqName + "'"); } // Don't permit receive-pack to be executed if a refs/for/branch_name // reference exists in the destination repository. These block the // client from being able to even send us a pack file, as it is very // unlikely the user passed the --force flag and the new commit is // probably not going to fast-forward the branch. // Map<String, Ref> blockingFors; try { blockingFors = repo.getRefDatabase().getRefs("refs/for/"); } catch (IOException err) { String projName = project.getName(); log.warn("Cannot scan refs in '" + projName + "'", err); return new Capable("Server process cannot read '" + projName + "'"); } if (!blockingFors.isEmpty()) { String projName = project.getName(); log.error("Repository '" + projName + "' needs the following refs removed to receive changes: " + blockingFors.keySet()); return new Capable("One or more refs/for/ names blocks change upload"); } if (project.isUseContributorAgreements()) { try { return verifyActiveContributorAgreement(); } catch (OrmException e) { log.error("Cannot query database for agreements", e); return new Capable("Cannot verify contribution agreement"); } } else { return Capable.OK; } } @Override public void onPreReceive(final ReceivePack arg0, final Collection<ReceiveCommand> commands) { parseCommands(commands); if (newChange != null && newChange.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { createNewChanges(); } doReplaces(); } @Override public void onPostReceive(final ReceivePack arg0, final Collection<ReceiveCommand> commands) { for (final ReceiveCommand c : commands) { if (c.getResult() == Result.OK) { if (isHead(c)) { switch (c.getType()) { case CREATE: autoCloseChanges(c); break; case DELETE: break; case UPDATE: case UPDATE_NONFASTFORWARD: autoCloseChanges(c); break; } } if (!c.getRefName().startsWith(NEW_CHANGE)) { // We only schedule direct refs updates for replication. // Change refs are scheduled when they are created. // replication.scheduleUpdate(project.getNameKey(), c.getRefName()); Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName()); hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount()); } } } if (!allNewChanges.isEmpty() && canonicalWebUrl != null) { final String url = canonicalWebUrl; rp.sendMessage(""); rp.sendMessage("New Changes:"); for (final Change.Id c : allNewChanges) { rp.sendMessage(" " + url + c.get()); } rp.sendMessage(""); } } private Capable verifyActiveContributorAgreement() throws OrmException { AbstractAgreement bestAgreement = null; ContributorAgreement bestCla = null; OUTER: for (AccountGroup.Id groupId : currentUser.getEffectiveGroups()) { final List<AccountGroupAgreement> temp = db.accountGroupAgreements().byGroup(groupId).toList(); Collections.reverse(temp); for (final AccountGroupAgreement a : temp) { final ContributorAgreement cla = db.contributorAgreements().get(a.getAgreementId()); if (cla == null) { continue; } bestAgreement = a; bestCla = cla; break OUTER; } } if (bestAgreement == null) { final List<AccountAgreement> temp = db.accountAgreements().byAccount(currentUser.getAccountId()).toList(); Collections.reverse(temp); for (final AccountAgreement a : temp) { final ContributorAgreement cla = db.contributorAgreements().get(a.getAgreementId()); if (cla == null) { continue; } bestAgreement = a; bestCla = cla; break; } } if (bestCla != null && !bestCla.isActive()) { final StringBuilder msg = new StringBuilder(); msg.append(bestCla.getShortName()); msg.append(" contributor agreement is expired.\n"); if (canonicalWebUrl != null) { msg.append("\nPlease complete a new agreement"); msg.append(":\n\n "); msg.append(canonicalWebUrl); msg.append("#"); msg.append(PageLinks.SETTINGS_AGREEMENTS); msg.append("\n"); } msg.append("\n"); return new Capable(msg.toString()); } if (bestCla != null && bestCla.isRequireContactInformation()) { boolean fail = false; fail |= missing(currentUser.getAccount().getFullName()); fail |= missing(currentUser.getAccount().getPreferredEmail()); fail |= !currentUser.getAccount().isContactFiled(); if (fail) { final StringBuilder msg = new StringBuilder(); msg.append(bestCla.getShortName()); msg.append(" contributor agreement requires"); msg.append(" current contact information.\n"); if (canonicalWebUrl != null) { msg.append("\nPlease review your contact information"); msg.append(":\n\n "); msg.append(canonicalWebUrl); msg.append("#"); msg.append(PageLinks.SETTINGS_CONTACT); msg.append("\n"); } msg.append("\n"); return new Capable(msg.toString()); } } if (bestAgreement != null) { switch (bestAgreement.getStatus()) { case VERIFIED: return Capable.OK; case REJECTED: return new Capable(bestCla.getShortName() + " contributor agreement was rejected." + "\n (rejected on " + bestAgreement.getReviewedOn() + ")\n"); case NEW: return new Capable(bestCla.getShortName() + " contributor agreement is still pending review.\n"); } } final StringBuilder msg = new StringBuilder(); msg.append(" A Contributor Agreement must be completed before uploading"); if (canonicalWebUrl != null) { msg.append(":\n\n "); msg.append(canonicalWebUrl); msg.append("#"); msg.append(PageLinks.SETTINGS_AGREEMENTS); msg.append("\n"); } else { msg.append("."); } msg.append("\n"); return new Capable(msg.toString()); } private static boolean missing(final String value) { return value == null || value.trim().equals(""); } private Account.Id toAccountId(final String nameOrEmail) throws OrmException, NoSuchAccountException { final Account a = accountResolver.findByNameOrEmail(nameOrEmail); if (a == null) { throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered"); } return a.getId(); } private void parseCommands(final Collection<ReceiveCommand> commands) { for (final ReceiveCommand cmd : commands) { if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { // Already rejected by the core receive process. // continue; } if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) { reject(cmd, "not valid ref"); continue; } if (cmd.getRefName().startsWith(NEW_CHANGE)) { parseNewChangeCommand(cmd); continue; } final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName()); if (m.matches()) { // The referenced change must exist and must still be open. // final Change.Id changeId = Change.Id.parse(m.group(1)); parseReplaceCommand(cmd, changeId); continue; } switch (cmd.getType()) { case CREATE: parseCreate(cmd); continue; case UPDATE: parseUpdate(cmd); continue; case DELETE: parseDelete(cmd); continue; case UPDATE_NONFASTFORWARD: parseRewind(cmd); continue; } // Everything else is bogus as far as we are concerned. // reject(cmd); } } private void parseCreate(final ReceiveCommand cmd) { RevObject obj; try { obj = rp.getRevWalk().parseAny(cmd.getNewId()); } catch (IOException err) { log.error("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " creation", err); reject(cmd, "invalid object"); return; } if (isHead(cmd) && !isCommit(cmd)) { return; } RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (ctl.canCreate(rp.getRevWalk(), obj)) { validateNewCommits(ctl, cmd); // Let the core receive process handle it } else { reject(cmd); } } private void parseUpdate(final ReceiveCommand cmd) { RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (ctl.canUpdate()) { if (isHead(cmd) && !isCommit(cmd)) { return; } validateNewCommits(ctl, cmd); // Let the core receive process handle it } else { reject(cmd); } } private boolean isCommit(final ReceiveCommand cmd) { RevObject obj; try { obj = rp.getRevWalk().parseAny(cmd.getNewId()); } catch (IOException err) { log.error("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName(), err); reject(cmd, "invalid object"); return false; } if (obj instanceof RevCommit) { return true; } else { reject(cmd, "not a commit"); return false; } } private void parseDelete(final ReceiveCommand cmd) { RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (ctl.canDelete()) { // Let the core receive process handle it } else { reject(cmd); } } private void parseRewind(final ReceiveCommand cmd) { RevCommit newObject; try { newObject = rp.getRevWalk().parseCommit(cmd.getNewId()); } catch (IncorrectObjectTypeException notCommit) { newObject = null; } catch (IOException err) { log.error("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " forced update", err); reject(cmd, "invalid object"); return; } RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (newObject != null) { validateNewCommits(ctl, cmd); if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { return; } } if (ctl.canForceUpdate()) { // Let the core receive process handle it } else { cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD); } } private void parseNewChangeCommand(final ReceiveCommand cmd) { // Permit exactly one new change request per push. // if (newChange != null) { reject(cmd, "duplicate request"); return; } newChange = cmd; String destBranchName = cmd.getRefName().substring(NEW_CHANGE.length()); if (!destBranchName.startsWith(Constants.R_REFS)) { destBranchName = Constants.R_HEADS + destBranchName; } final String head; try { head = repo.getFullBranch(); } catch (IOException e) { log.error("Cannot read HEAD symref", e); reject(cmd, "internal error"); return; } // Split the destination branch by branch and topic. The topic // suffix is entirely optional, so it might not even exist. // int split = destBranchName.length(); for (;;) { String name = destBranchName.substring(0, split); if (rp.getAdvertisedRefs().containsKey(name)) { // We advertised the branch to the client so we know // the branch exists. Target this branch for the upload. // break; } else if (head.equals(name)) { // We didn't advertise the branch, because it doesn't exist yet. // Allow it anyway as HEAD is a symbolic reference to the name. // break; } split = name.lastIndexOf('/', split - 1); if (split <= Constants.R_REFS.length()) { String n = destBranchName; if (n.startsWith(Constants.R_HEADS)) n = n.substring(Constants.R_HEADS.length()); reject(cmd, "branch " + n + " not found"); return; } } if (split < destBranchName.length()) { destTopicName = destBranchName.substring(split + 1); if (destTopicName.isEmpty()) { destTopicName = null; } } else { destTopicName = null; } destBranch = new Branch.NameKey(project.getNameKey(), // destBranchName.substring(0, split)); destBranchCtl = projectControl.controlForRef(destBranch); if (!destBranchCtl.canUpload()) { reject(cmd); } // Validate that the new commits are connected with the existing heads // or tags of this repository. If they aren't, we want to abort. We do // this check by coloring the tip CONNECTED and letting a RevWalk push // that color through the graph until it reaches at least one of our // already existing heads or tags. We then test to see if that color // made it back onto that set. // try { final RevWalk walk = rp.getRevWalk(); final RevFlag SIDE_NEW = walk.newFlag("NEW"); final RevFlag SIDE_HAVE = walk.newFlag("HAVE"); final RevFlagSet COMMON = new RevFlagSet(); COMMON.add(SIDE_NEW); COMMON.add(SIDE_HAVE); walk.carry(COMMON); walk.reset(); walk.sort(RevSort.TOPO); walk.sort(RevSort.REVERSE, true); final RevCommit tip = walk.parseCommit(newChange.getNewId()); tip.add(SIDE_NEW); walk.markStart(tip); boolean haveHeads = false; for (final Ref r : rp.getAdvertisedRefs().values()) { if (isHead(r) || isTag(r)) { try { final RevCommit h = walk.parseCommit(r.getObjectId()); h.add(SIDE_HAVE); walk.markStart(h); haveHeads = true; } catch (IOException e) { continue; } } } if (haveHeads) { boolean isConnected = false; RevCommit c; while ((c = walk.next()) != null) { if (c.hasAll(COMMON)) { isConnected = true; break; } } if (!isConnected) { reject(newChange, "no common ancestry"); return; } } } catch (IOException e) { newChange.setResult(Result.REJECTED_MISSING_OBJECT); log.error("Invalid pack upload; one or more objects weren't sent", e); return; } } /** * Loads a list of commits to reject from {@code refs/meta/reject-commits}. * * @return NoteMap of commits to be rejected, null if there are none. * @throws IOException the map cannot be loaded. */ private NoteMap loadRejectCommitsMap() throws IOException { try { Ref ref = repo.getRef(GitRepositoryManager.REF_REJECT_COMMITS); if (ref == null) { return NoteMap.newEmptyMap(); } RevWalk rw = rp.getRevWalk(); RevCommit map = rw.parseCommit(ref.getObjectId()); return NoteMap.read(rw.getObjectReader(), map); } catch (IOException badMap) { throw new IOException("Cannot load " + GitRepositoryManager.REF_REJECT_COMMITS, badMap); } } private void parseReplaceCommand(final ReceiveCommand cmd, final Change.Id changeId) { if (cmd.getType() != ReceiveCommand.Type.CREATE) { reject(cmd, "invalid usage"); return; } final RevCommit newCommit; try { newCommit = rp.getRevWalk().parseCommit(cmd.getNewId()); } catch (IOException e) { log.error("Cannot parse " + cmd.getNewId().name() + " as commit", e); reject(cmd, "invalid commit"); return; } final Change changeEnt; try { changeEnt = db.changes().get(changeId); } catch (OrmException e) { log.error("Cannot lookup existing change " + changeId, e); reject(cmd, "database error"); return; } if (changeEnt == null) { reject(cmd, "change " + changeId + " not found"); return; } if (!project.getNameKey().equals(changeEnt.getProject())) { reject(cmd, "change " + changeId + " not found"); return; } requestReplace(cmd, changeEnt, newCommit); } private boolean requestReplace(final ReceiveCommand cmd, final Change change, final RevCommit newCommit) { if (change.getStatus().isClosed()) { reject(cmd, "change " + change.getId() + " closed"); return false; } final ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd); if (replaceByChange.containsKey(req.ontoChange)) { reject(cmd, "duplicate request"); return false; } if (replaceByCommit.containsKey(req.newCommit)) { reject(cmd, "duplicate request"); return false; } replaceByChange.put(req.ontoChange, req); replaceByCommit.put(req.newCommit, req); return true; } private void createNewChanges() { final List<RevCommit> toCreate = new ArrayList<RevCommit>(); final RevWalk walk = rp.getRevWalk(); walk.reset(); walk.sort(RevSort.TOPO); walk.sort(RevSort.REVERSE, true); try { walk.markStart(walk.parseCommit(newChange.getNewId())); for (ObjectId id : existingObjects()) { try { walk.markUninteresting(walk.parseCommit(id)); } catch (IOException e) { continue; } } for (;;) { final RevCommit c = walk.next(); if (c == null) { break; } if (replaceByCommit.containsKey(c)) { // This commit was already scheduled to replace an existing PatchSet. // continue; } if (!validCommit(destBranchCtl, newChange, c)) { // Not a change the user can propose? Abort as early as possible. // return; } final List<String> idList = c.getFooterLines(CHANGE_ID); if (!idList.isEmpty()) { final String idStr = idList.get(idList.size() - 1).trim(); final Change.Key key = new Change.Key(idStr); final List<Change> changes = db.changes().byProjectKey(project.getNameKey(), key).toList(); if (changes.size() > 1) { // WTF, multiple changes in this project have the same key? // Since the commit is new, the user should recreate it with // a different Change-Id. In practice, we should never see // this error message as Change-Id should be unique. // reject(newChange, key.get() + " has duplicates"); return; } if (changes.size() == 1) { // Schedule as a replacement to this one matching change. // if (requestReplace(newChange, changes.get(0), c)) { continue; } else { return; } } } toCreate.add(c); } } catch (IOException e) { // Should never happen, the core receive process would have // identified the missing object earlier before we got control. // newChange.setResult(Result.REJECTED_MISSING_OBJECT); log.error("Invalid pack upload; one or more objects weren't sent", e); return; } catch (OrmException e) { log.error("Cannot query database to locate prior changes", e); reject(newChange, "database error"); return; } if (toCreate.isEmpty() && replaceByChange.isEmpty()) { reject(newChange, "no new changes"); return; } for (final RevCommit c : toCreate) { try { createChange(walk, c); } catch (IOException e) { log.error("Error computing patch of commit " + c.name(), e); reject(newChange, "diff error"); return; } catch (OrmException e) { log.error("Error creating change for commit " + c.name(), e); reject(newChange, "database error"); return; } } newChange.setResult(ReceiveCommand.Result.OK); } private void createChange(final RevWalk walk, final RevCommit c) throws OrmException, IOException { walk.parseBody(c); warnMalformedMessage(c); final Account.Id me = currentUser.getAccountId(); Change.Key changeKey = new Change.Key("I" + c.name()); final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId); final Set<Account.Id> cc = new HashSet<Account.Id>(ccId); final List<FooterLine> footerLines = c.getFooterLines(); for (final FooterLine footerLine : footerLines) { try { if (footerLine.matches(CHANGE_ID)) { final String v = footerLine.getValue().trim(); if (v.matches("^I[0-9a-f]{8,}.*$")) { changeKey = new Change.Key(v); } } else if (isReviewer(footerLine)) { reviewers.add(toAccountId(footerLine.getValue().trim())); } else if (footerLine.matches(FooterKey.CC)) { cc.add(toAccountId(footerLine.getValue().trim())); } } catch (NoSuchAccountException e) { continue; } } reviewers.remove(me); cc.remove(me); cc.removeAll(reviewers); final Change change = new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch); change.setTopic(destTopicName); change.nextPatchSetId(); final PatchSet ps = new PatchSet(change.currPatchSetId()); ps.setCreatedOn(change.getCreatedOn()); ps.setUploader(me); ps.setRevision(toRevId(c)); insertAncestors(ps.getId(), c); db.patchSets().insert(Collections.singleton(ps)); final PatchSetInfo info = patchSetInfoFactory.get(c, ps.getId()); change.setCurrentPatchSet(info); ChangeUtil.updated(change); db.changes().insert(Collections.singleton(change)); final Set<Account.Id> haveApprovals = new HashSet<Account.Id>(); final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes(); haveApprovals.add(me); if (allTypes.size() > 0) { final Account.Id authorId = info.getAuthor() != null ? info.getAuthor().getAccount() : null; final Account.Id committerId = info.getCommitter() != null ? info.getCommitter().getAccount() : null; final ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId(); if (authorId != null && haveApprovals.add(authorId)) { insertDummyApproval(change, ps.getId(), authorId, catId, db); } if (committerId != null && haveApprovals.add(committerId)) { insertDummyApproval(change, ps.getId(), committerId, catId, db); } for (final Account.Id reviewer : reviewers) { if (haveApprovals.add(reviewer)) { insertDummyApproval(change, ps.getId(), reviewer, catId, db); } } } final RefUpdate ru = repo.updateRef(ps.getRefName()); ru.setNewObjectId(c); ru.disableRefLog(); if (ru.update(walk) != RefUpdate.Result.NEW) { throw new IOException("Failed to create ref " + ps.getRefName() + " in " + repo.getDirectory() + ": " + ru.getResult()); } replication.scheduleUpdate(project.getNameKey(), ru.getName()); allNewChanges.add(change.getId()); try { final CreateChangeSender cm; cm = createChangeSenderFactory.create(change); cm.setFrom(me); cm.setPatchSet(ps, info); cm.addReviewers(reviewers); cm.addExtraCC(cc); cm.send(); } catch (EmailException e) { log.error("Cannot send email for new change " + change.getId(), e); } ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines); hooks.doPatchsetCreatedHook(change, ps); } private static boolean isReviewer(final FooterLine candidateFooterLine) { return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY) || candidateFooterLine.matches(FooterKey.ACKED_BY) || candidateFooterLine.matches(REVIEWED_BY) || candidateFooterLine.matches(TESTED_BY); } private void doReplaces() { for (final ReplaceRequest request : replaceByChange.values()) { try { doReplace(request); } catch (IOException err) { log.error("Error computing replacement patch for change " + request.ontoChange + ", commit " + request.newCommit.name(), err); reject(request.cmd, "diff error"); } catch (OrmException err) { log.error("Error storing replacement patch for change " + request.ontoChange + ", commit " + request.newCommit.name(), err); reject(request.cmd, "database error"); } if (request.cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { log.error("Replacement patch for change " + request.ontoChange + ", commit " + request.newCommit.name() + " wasn't attempted." + " This is a bug in the receive process implementation."); reject(request.cmd, "internal error"); } } } private PatchSet.Id doReplace(final ReplaceRequest request) throws IOException, OrmException { final RevCommit c = request.newCommit; rp.getRevWalk().parseBody(c); warnMalformedMessage(c); final Account.Id me = currentUser.getAccountId(); final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId); final Set<Account.Id> cc = new HashSet<Account.Id>(ccId); final List<FooterLine> footerLines = c.getFooterLines(); for (final FooterLine footerLine : footerLines) { try { if (isReviewer(footerLine)) { reviewers.add(toAccountId(footerLine.getValue().trim())); } else if (footerLine.matches(FooterKey.CC)) { cc.add(toAccountId(footerLine.getValue().trim())); } } catch (NoSuchAccountException e) { continue; } } reviewers.remove(me); cc.remove(me); cc.removeAll(reviewers); final ReplaceResult result = new ReplaceResult(); final Set<Account.Id> oldReviewers = new HashSet<Account.Id>(); final Set<Account.Id> oldCC = new HashSet<Account.Id>(); Change change = db.changes().get(request.ontoChange); if (change == null) { reject(request.cmd, "change " + request.ontoChange + " not found"); return null; } if (change.getStatus().isClosed()) { reject(request.cmd, "change " + request.ontoChange + " closed"); return null; } final ChangeControl changeCtl = projectControl.controlFor(change); if (!changeCtl.canAddPatchSet()) { reject(request.cmd, "cannot replace " + request.ontoChange); return null; } if (!validCommit(changeCtl.getRefControl(), request.cmd, c)) { return null; } final PatchSet.Id priorPatchSet = change.currentPatchSetId(); for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) { if (ps.getRevision() == null) { log.warn("Patch set " + ps.getId() + " has no revision"); reject(request.cmd, "change state corrupt"); return null; } final String revIdStr = ps.getRevision().get(); final ObjectId commitId; try { commitId = ObjectId.fromString(revIdStr); } catch (IllegalArgumentException e) { log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr); reject(request.cmd, "change state corrupt"); return null; } try { final RevCommit prior = rp.getRevWalk().parseCommit(commitId); // Don't allow a change to directly depend upon itself. This is a // very common error due to users making a new commit rather than // amending when trying to address review comments. // if (rp.getRevWalk().isMergedInto(prior, c)) { reject(request.cmd, "squash commits first"); return null; } // Don't allow the same commit to appear twice on the same change // if (c == prior) { reject(request.cmd, "commit already exists"); return null; } // Don't allow the same tree if the commit message is unmodified // or no parents were updated (rebase), else warn that only part // of the commit was modified. // if (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) { rp.getRevWalk().parseBody(prior); final boolean messageEq = eq(c.getFullMessage(), prior.getFullMessage()); final boolean parentsEq = parentsEqual(c, prior); final boolean authorEq = authorEqual(c, prior); if (messageEq && parentsEq && authorEq) { reject(request.cmd, "no changes made"); return null; } else { ObjectReader reader = rp.getRevWalk().getObjectReader(); StringBuilder msg = new StringBuilder(); msg.append("(W) "); msg.append(reader.abbreviate(c).name()); msg.append(":"); msg.append(" no files changed"); if (!authorEq) { msg.append(", author changed"); } if (!messageEq) { msg.append(", message updated"); } if (!parentsEq) { msg.append(", was rebased"); } rp.sendMessage(msg.toString()); } } } catch (IOException e) { log.error("Change " + change.getId() + " missing " + revIdStr, e); reject(request.cmd, "change state corrupt"); return null; } } change = db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change change) { if (change.getStatus().isOpen()) { change.nextPatchSetId(); return change; } else { return null; } } }); if (change == null) { reject(request.cmd, "change is closed"); return null; } final PatchSet ps = new PatchSet(change.currPatchSetId()); ps.setCreatedOn(new Timestamp(System.currentTimeMillis())); ps.setUploader(currentUser.getAccountId()); ps.setRevision(toRevId(c)); insertAncestors(ps.getId(), c); db.patchSets().insert(Collections.singleton(ps)); final Ref mergedInto = findMergedInto(change.getDest().get(), c); result.mergedIntoRef = mergedInto != null ? mergedInto.getName() : null; result.change = change; result.patchSet = ps; result.info = patchSetInfoFactory.get(c, ps.getId()); final Account.Id authorId = result.info.getAuthor() != null ? result.info.getAuthor().getAccount() : null; final Account.Id committerId = result.info.getCommitter() != null ? result.info.getCommitter() .getAccount() : null; boolean haveAuthor = false; boolean haveCommitter = false; final Set<Account.Id> haveApprovals = new HashSet<Account.Id>(); oldReviewers.clear(); oldCC.clear(); for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) { haveApprovals.add(a.getAccountId()); if (a.getValue() != 0) { oldReviewers.add(a.getAccountId()); } else { oldCC.add(a.getAccountId()); } final ApprovalType type = approvalTypes.getApprovalType(a.getCategoryId()); if (a.getPatchSetId().equals(priorPatchSet) && type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) { // If there was a negative vote on the prior patch set, carry it // into this patch set. // db.patchSetApprovals().insert( Collections.singleton(new PatchSetApproval(ps.getId(), a))); } if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) { haveAuthor = true; } if (!haveCommitter && committerId != null && a.getAccountId().equals(committerId)) { haveCommitter = true; } } final ChangeMessage msg = new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil .messageUUID(db)), me, ps.getCreatedOn()); msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + "."); db.changeMessages().insert(Collections.singleton(msg)); result.msg = msg; if (result.mergedIntoRef != null) { // Change was already submitted to a branch, close it. // markChangeMergedByPush(db, result); } else { // Change should be new, so it can go through review again. // change = db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change change) { if (change.getStatus().isOpen()) { if (destTopicName != null) { change.setTopic(destTopicName); } change.setStatus(Change.Status.NEW); change.setCurrentPatchSet(result.info); ChangeUtil.updated(change); return change; } else { return null; } } }); if (change == null) { db.patchSets().delete(Collections.singleton(ps)); db.changeMessages().delete(Collections.singleton(msg)); reject(request.cmd, "change is closed"); return null; } } final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes(); if (allTypes.size() > 0) { final ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId(); if (authorId != null && haveApprovals.add(authorId)) { insertDummyApproval(result, authorId, catId, db); } if (committerId != null && haveApprovals.add(committerId)) { insertDummyApproval(result, committerId, catId, db); } for (final Account.Id reviewer : reviewers) { if (haveApprovals.add(reviewer)) { insertDummyApproval(result, reviewer, catId, db); } } hooks.doPatchsetCreatedHook(result.change, ps); } final RefUpdate ru = repo.updateRef(ps.getRefName()); ru.setNewObjectId(c); ru.disableRefLog(); if (ru.update(rp.getRevWalk()) != RefUpdate.Result.NEW) { throw new IOException("Failed to create ref " + ps.getRefName() + " in " + repo.getDirectory() + ": " + ru.getResult()); } replication.scheduleUpdate(project.getNameKey(), ru.getName()); request.cmd.setResult(ReceiveCommand.Result.OK); try { final ReplacePatchSetSender cm; cm = replacePatchSetFactory.create(result.change); cm.setFrom(me); cm.setPatchSet(ps, result.info); cm.setChangeMessage(result.msg); cm.addReviewers(reviewers); cm.addExtraCC(cc); cm.addReviewers(oldReviewers); cm.addExtraCC(oldCC); cm.send(); } catch (EmailException e) { log.error("Cannot send email for new patch set " + ps.getId(), e); } ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines); sendMergedEmail(result); return result != null ? result.info.getKey() : null; } static boolean parentsEqual(RevCommit a, RevCommit b) { if (a.getParentCount() != b.getParentCount()) { return false; } for (int i = 0; i < a.getParentCount(); i++) { if (a.getParent(i) != b.getParent(i)) { return false; } } return true; } static boolean authorEqual(RevCommit a, RevCommit b) { PersonIdent aAuthor = a.getAuthorIdent(); PersonIdent bAuthor = b.getAuthorIdent(); if (aAuthor == null && bAuthor == null) { return true; } else if (aAuthor == null || bAuthor == null) { return false; } return eq(aAuthor.getName(), bAuthor.getName()) && eq(aAuthor.getEmailAddress(), bAuthor.getEmailAddress()); } static boolean eq(String a, String b) { if (a == null && b == null) { return true; } else if (a == null || b == null) { return false; } else { return a.equals(b); } } private void insertDummyApproval(final ReplaceResult result, final Account.Id forAccount, final ApprovalCategory.Id catId, final ReviewDb db) throws OrmException { insertDummyApproval(result.change, result.patchSet.getId(), forAccount, catId, db); } private void insertDummyApproval(final Change change, final PatchSet.Id psId, final Account.Id forAccount, final ApprovalCategory.Id catId, final ReviewDb db) throws OrmException { final PatchSetApproval ca = new PatchSetApproval(new PatchSetApproval.Key(psId, forAccount, catId), (short) 0); ca.cache(change); db.patchSetApprovals().insert(Collections.singleton(ca)); } private Ref findMergedInto(final String first, final RevCommit commit) { try { final Map<String, Ref> all = repo.getAllRefs(); Ref firstRef = all.get(first); if (firstRef != null && isMergedInto(commit, firstRef)) { return firstRef; } for (Ref ref : all.values()) { if (isHead(ref)) { if (isMergedInto(commit, ref)) { return ref; } } } return null; } catch (IOException e) { log.warn("Can't check for already submitted change", e); return null; } } private boolean isMergedInto(final RevCommit commit, final Ref ref) throws IOException { final RevWalk rw = rp.getRevWalk(); return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId())); } private static class ReplaceRequest { final Change.Id ontoChange; final RevCommit newCommit; final ReceiveCommand cmd; ReplaceRequest(final Change.Id toChange, final RevCommit newCommit, final ReceiveCommand cmd) { this.ontoChange = toChange; this.newCommit = newCommit; this.cmd = cmd; } } private static class ReplaceResult { Change change; PatchSet patchSet; PatchSetInfo info; ChangeMessage msg; String mergedIntoRef; } private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) { final RevWalk walk = rp.getRevWalk(); walk.reset(); walk.sort(RevSort.NONE); try { walk.markStart(walk.parseCommit(cmd.getNewId())); for (ObjectId id : existingObjects()) { try { walk.markUninteresting(walk.parseCommit(id)); } catch (IOException e) { continue; } } RevCommit c; while ((c = walk.next()) != null) { if (!validCommit(ctl, cmd, c)) { break; } } } catch (IOException err) { cmd.setResult(Result.REJECTED_MISSING_OBJECT); log.error("Invalid pack upload; one or more objects weren't sent", err); } } private Collection<ObjectId> existingObjects() { if (existingObjects == null) { Map<String, Ref> refs = repo.getAllRefs(); existingObjects = new ArrayList<ObjectId>(refs.size()); for (Ref r : refs.values()) { existingObjects.add(r.getObjectId()); } } return existingObjects; } private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd, final RevCommit c) throws MissingObjectException, IOException { rp.getRevWalk().parseBody(c); final PersonIdent committer = c.getCommitterIdent(); final PersonIdent author = c.getAuthorIdent(); // Don't allow the user to amend a merge created by Gerrit Code Review. // This seems to happen all too often, due to users not paying any // attention to what they are doing. // if (c.getParentCount() > 1 && author.getName().equals(gerritIdent.getName()) && author.getEmailAddress().equals(gerritIdent.getEmailAddress()) && !ctl.canForgeGerritServerIdentity()) { reject(cmd, "do not amend merges not made by you"); return false; } // Require that author matches the uploader. // if (!currentUser.getEmailAddresses().contains(author.getEmailAddress()) && !ctl.canForgeAuthor()) { reject(cmd, "you are not author " + author.getEmailAddress()); return false; } // Require that committer matches the uploader. // if (!currentUser.getEmailAddresses().contains(committer.getEmailAddress()) && !ctl.canForgeCommitter()) { reject(cmd, "you are not committer " + committer.getEmailAddress()); return false; } if (project.isUseSignedOffBy()) { // If the project wants Signed-off-by / Acked-by lines, verify we // have them for the blamable parties involved on this change. // boolean sboAuthor = false, sboCommitter = false, sboMe = false; for (final FooterLine footer : c.getFooterLines()) { if (footer.matches(FooterKey.SIGNED_OFF_BY)) { final String e = footer.getEmailAddress(); if (e != null) { sboAuthor |= author.getEmailAddress().equals(e); sboCommitter |= committer.getEmailAddress().equals(e); sboMe |= currentUser.getEmailAddresses().contains(e); } } } if (!sboAuthor && !sboCommitter && !sboMe && !ctl.canForgeCommitter()) { reject(cmd, "not Signed-off-by author/committer/uploader"); return false; } } final List<String> idList = c.getFooterLines(CHANGE_ID); if (idList.isEmpty()) { if (project.isRequireChangeID()) { reject(cmd, "missing Change-Id in commit message"); return false; } } else if (idList.size() > 1) { reject(cmd, "multiple Change-Id lines in commit message"); return false; } else { final String v = idList.get(idList.size() - 1).trim(); if (!v.matches("^I[0-9a-f]{8,}.*$")) { reject(cmd, "invalid Change-Id line format in commit message "); return false; } } // Check for banned commits to prevent them from entering the tree again. if (rejectCommits.contains(c)) { reject(newChange, "contains banned commit " + c.getName()); return false; } return true; } private void warnMalformedMessage(RevCommit c) { ObjectReader reader = rp.getRevWalk().getObjectReader(); if (65 < c.getShortMessage().length()) { AbbreviatedObjectId id; try { id = reader.abbreviate(c); } catch (IOException err) { id = c.abbreviate(6); } rp.sendMessage("(W) " + id.name() // + ": commit subject >65 characters; use shorter first paragraph"); } int longLineCnt = 0, nonEmptyCnt = 0; for (String line : c.getFullMessage().split("\n")) { if (!line.trim().isEmpty()) { nonEmptyCnt++; } if (70 < line.length()) { longLineCnt++; } } if (0 < longLineCnt && 33 < longLineCnt * 100 / nonEmptyCnt) { AbbreviatedObjectId id; try { id = reader.abbreviate(c); } catch (IOException err) { id = c.abbreviate(6); } rp.sendMessage("(W) " + id.name() // + ": commit message lines >70 characters; manually wrap lines"); } } private void autoCloseChanges(final ReceiveCommand cmd) { final RevWalk rw = rp.getRevWalk(); try { rw.reset(); rw.markStart(rw.parseCommit(cmd.getNewId())); if (!ObjectId.zeroId().equals(cmd.getOldId())) { rw.markUninteresting(rw.parseCommit(cmd.getOldId())); } final Map<ObjectId, Ref> byCommit = changeRefsById(); final Map<Change.Key, Change.Id> byKey = openChangesByKey(); final List<ReplaceRequest> toClose = new ArrayList<ReplaceRequest>(); RevCommit c; while ((c = rw.next()) != null) { final Ref ref = byCommit.get(c.copy()); if (ref != null) { rw.parseBody(c); closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c); continue; } rw.parseBody(c); for (final String changeId : c.getFooterLines(CHANGE_ID)) { final Change.Id onto = byKey.get(new Change.Key(changeId.trim())); if (onto != null) { toClose.add(new ReplaceRequest(onto, c, cmd)); break; } } } for (final ReplaceRequest req : toClose) { final PatchSet.Id psi = doReplace(req); if (psi != null) { closeChange(req.cmd, psi, req.newCommit); } } } catch (IOException e) { log.error("Can't scan for changes to close", e); } catch (OrmException e) { log.error("Can't scan for changes to close", e); } } private void closeChange(final ReceiveCommand cmd, final PatchSet.Id psi, final RevCommit commit) throws OrmException { final String refName = cmd.getRefName(); final Change.Id cid = psi.getParentKey(); final Change change = db.changes().get(cid); final PatchSet ps = db.patchSets().get(psi); if (change == null || ps == null) { log.warn(project.getName() + " " + psi + " is missing"); return; } if (change.getStatus() == Change.Status.MERGED) { // If its already merged, don't make further updates, it // might just be moving from an experimental branch into // a more stable branch. // return; } final ReplaceResult result = new ReplaceResult(); result.change = change; result.patchSet = ps; result.info = patchSetInfoFactory.get(commit, psi); result.mergedIntoRef = refName; markChangeMergedByPush(db, result); sendMergedEmail(result); } private Map<ObjectId, Ref> changeRefsById() throws IOException { if (refsById == null) { refsById = new HashMap<ObjectId, Ref>(); for (Ref r : repo.getRefDatabase().getRefs("refs/changes/").values()) { if (PatchSet.isRef(r.getName())) { refsById.put(r.getObjectId(), r); } } } return refsById; } private Map<Change.Key, Change.Id> openChangesByKey() throws OrmException { final Map<Change.Key, Change.Id> r = new HashMap<Change.Key, Change.Id>(); for (Change c : db.changes().byProjectOpenAll(project.getNameKey())) { r.put(c.getKey(), c.getId()); } return r; } private void markChangeMergedByPush(final ReviewDb db, final ReplaceResult result) throws OrmException { final Change change = result.change; final String mergedIntoRef = result.mergedIntoRef; change.setCurrentPatchSet(result.info); change.setStatus(Change.Status.MERGED); ChangeUtil.updated(change); final List<PatchSetApproval> approvals = db.patchSetApprovals().byChange(change.getId()).toList(); for (PatchSetApproval a : approvals) { a.cache(change); } db.patchSetApprovals().update(approvals); final StringBuilder msgBuf = new StringBuilder(); msgBuf.append("Change has been successfully pushed"); if (!mergedIntoRef.equals(change.getDest().get())) { msgBuf.append(" into "); if (mergedIntoRef.startsWith(Constants.R_HEADS)) { msgBuf.append("branch "); msgBuf.append(repo.shortenRefName(mergedIntoRef)); } else { msgBuf.append(mergedIntoRef); } } msgBuf.append("."); final ChangeMessage msg = new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil .messageUUID(db)), currentUser.getAccountId()); msg.setMessage(msgBuf.toString()); db.changeMessages().insert(Collections.singleton(msg)); db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() { @Override public Change update(Change change) { if (change.getStatus().isOpen()) { change.setCurrentPatchSet(result.info); change.setStatus(Change.Status.MERGED); ChangeUtil.updated(change); } return change; } }); } private void sendMergedEmail(final ReplaceResult result) { if (result != null && result.mergedIntoRef != null) { try { final MergedSender cm = mergedSenderFactory.create(result.change); cm.setFrom(currentUser.getAccountId()); cm.setPatchSet(result.patchSet, result.info); cm.setDest(new Branch.NameKey(project.getNameKey(), result.mergedIntoRef)); cm.send(); } catch (EmailException e) { final PatchSet.Id psi = result.patchSet.getId(); log.error("Cannot send email for submitted patch set " + psi, e); } hooks.doChangeMergedHook(result.change, currentUser.getAccount(), result.patchSet); } } private void insertAncestors(PatchSet.Id id, RevCommit src) throws OrmException { final int cnt = src.getParentCount(); List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt); for (int p = 0; p < cnt; p++) { PatchSetAncestor a; a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1)); a.setAncestorRevision(toRevId(src.getParent(p))); toInsert.add(a); } db.patchSetAncestors().insert(toInsert); } private static RevId toRevId(final RevCommit src) { return new RevId(src.getId().name()); } private static void reject(final ReceiveCommand cmd) { reject(cmd, "prohibited by Gerrit"); } private static void reject(final ReceiveCommand cmd, final String why) { cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, why); } private static boolean isTag(final Ref ref) { return ref.getName().startsWith(Constants.R_TAGS); } private static boolean isTag(final ReceiveCommand cmd) { return cmd.getRefName().startsWith(Constants.R_TAGS); } private static boolean isHead(final Ref ref) { return ref.getName().startsWith(Constants.R_HEADS); } private static boolean isHead(final ReceiveCommand cmd) { return cmd.getRefName().startsWith(Constants.R_HEADS); } }