| // 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 static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID; |
| import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN; |
| import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals; |
| import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters; |
| |
| import static org.eclipse.jgit.lib.Constants.R_HEADS; |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT; |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Splitter; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.HashMultimap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.LinkedListMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.collect.Sets; |
| import com.google.common.util.concurrent.CheckedFuture; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.ListeningExecutorService; |
| import com.google.gerrit.common.ChangeHookRunner.HookResult; |
| import com.google.gerrit.common.ChangeHooks; |
| import com.google.gerrit.common.data.Capable; |
| import com.google.gerrit.common.data.LabelTypes; |
| import com.google.gerrit.common.data.PermissionRule; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Branch; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.ChangeMessage; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.PatchSetApproval; |
| import com.google.gerrit.reviewdb.client.PatchSetInfo; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.ApprovalsUtil; |
| 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.change.ChangeInserter; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.CanonicalWebUrl; |
| import com.google.gerrit.server.config.TrackingFooters; |
| import com.google.gerrit.server.events.CommitReceivedEvent; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.MultiProgressMonitor.Task; |
| import com.google.gerrit.server.git.validators.CommitValidationException; |
| import com.google.gerrit.server.git.validators.CommitValidationMessage; |
| import com.google.gerrit.server.git.validators.CommitValidators; |
| import com.google.gerrit.server.mail.CreateChangeSender; |
| import com.google.gerrit.server.mail.MailUtil.MailRecipients; |
| 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.ProjectCache; |
| import com.google.gerrit.server.project.ProjectControl; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.project.RefControl; |
| import com.google.gerrit.server.ssh.SshInfo; |
| import com.google.gerrit.server.util.MagicBranch; |
| import com.google.gerrit.server.util.RequestScopePropagator; |
| import com.google.gerrit.util.cli.CmdLineParser; |
| import com.google.gwtorm.server.AtomicUpdate; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.gwtorm.server.ResultSet; |
| import com.google.gwtorm.server.SchemaFactory; |
| 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.BatchRefUpdate; |
| 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.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.RevObject; |
| import org.eclipse.jgit.revwalk.RevSort; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.revwalk.filter.RevFilter; |
| import org.eclipse.jgit.transport.AdvertiseRefsHook; |
| import org.eclipse.jgit.transport.AdvertiseRefsHookChain; |
| import org.eclipse.jgit.transport.BaseReceivePack; |
| import org.eclipse.jgit.transport.ReceiveCommand; |
| import org.eclipse.jgit.transport.ReceiveCommand.Result; |
| import org.eclipse.jgit.transport.ReceivePack; |
| import org.eclipse.jgit.transport.UploadPack; |
| import org.kohsuke.args4j.CmdLineException; |
| import org.kohsuke.args4j.Option; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.io.StringWriter; |
| 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.concurrent.Callable; |
| 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 { |
| private static final Logger log = |
| LoggerFactory.getLogger(ReceiveCommits.class); |
| |
| public static final Pattern NEW_PATCHSET = |
| Pattern.compile("^refs/changes/(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$"); |
| |
| private static final FooterKey CHANGE_ID = new FooterKey("Change-Id"); |
| |
| private static final String COMMAND_REJECTION_MESSAGE_FOOTER = |
| "Please read the documentation and contact an administrator\n" |
| + "if you feel the configuration is incorrect"; |
| |
| private enum Error { |
| CONFIG_UPDATE("You are not allowed to perform this operation.\n" |
| + "Configuration changes can only be pushed by project owners\n" |
| + "who also have 'Push' rights on " + GitRepositoryManager.REF_CONFIG), |
| UPDATE("You are not allowed to perform this operation.\n" |
| + "To push into this reference you need 'Push' rights."), |
| DELETE("You need 'Push' rights with the 'Force Push'\n" |
| + "flag set to delete references."), |
| DELETE_CHANGES("Cannot delete from 'refs/changes'"), |
| CODE_REVIEW("You need 'Push' rights to upload code review requests.\n" |
| + "Verify that you are pushing to the right branch."); |
| |
| private final String value; |
| |
| Error(String value) { |
| this.value = value; |
| } |
| |
| public String get() { |
| return value; |
| } |
| } |
| |
| interface Factory { |
| ReceiveCommits create(ProjectControl projectControl, Repository repository); |
| } |
| |
| public interface MessageSender { |
| void sendMessage(String what); |
| void sendError(String what); |
| void sendBytes(byte[] what); |
| void sendBytes(byte[] what, int off, int len); |
| void flush(); |
| } |
| |
| private class ReceivePackMessageSender implements MessageSender { |
| @Override |
| public void sendMessage(String what) { |
| rp.sendMessage(what); |
| } |
| |
| @Override |
| public void sendError(String what) { |
| rp.sendError(what); |
| } |
| |
| @Override |
| public void sendBytes(byte[] what) { |
| sendBytes(what, 0, what.length); |
| } |
| |
| @Override |
| public void sendBytes(byte[] what, int off, int len) { |
| try { |
| rp.getMessageOutputStream().write(what, off, len); |
| } catch (IOException e) { |
| // Ignore write failures (matching JGit behavior). |
| } |
| } |
| |
| @Override |
| public void flush() { |
| try { |
| rp.getMessageOutputStream().flush(); |
| } catch (IOException e) { |
| // Ignore write failures (matching JGit behavior). |
| } |
| } |
| } |
| |
| private static final Function<Exception, OrmException> ORM_EXCEPTION = |
| new Function<Exception, OrmException>() { |
| @Override |
| public OrmException apply(Exception input) { |
| if (input instanceof OrmException) { |
| return (OrmException) input; |
| } |
| return new OrmException("Error updating database", input); |
| } |
| }; |
| |
| private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet(); |
| private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet(); |
| |
| private final IdentifiedUser currentUser; |
| private final ReviewDb db; |
| private final SchemaFactory<ReviewDb> schemaFactory; |
| private final AccountResolver accountResolver; |
| private final CmdLineParser.Factory optionParserFactory; |
| private final CreateChangeSender.Factory createChangeSenderFactory; |
| private final MergedSender.Factory mergedSenderFactory; |
| private final ReplacePatchSetSender.Factory replacePatchSetFactory; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final PatchSetInfoFactory patchSetInfoFactory; |
| private final ChangeHooks hooks; |
| private final ApprovalsUtil approvalsUtil; |
| private final GitRepositoryManager repoManager; |
| private final ProjectCache projectCache; |
| private final String canonicalWebUrl; |
| private final CommitValidators.Factory commitValidatorsFactory; |
| private final TrackingFooters trackingFooters; |
| private final TagCache tagCache; |
| private final ChangeInserter changeInserter; |
| private final WorkQueue workQueue; |
| private final ListeningExecutorService changeUpdateExector; |
| private final RequestScopePropagator requestScopePropagator; |
| private final SshInfo sshInfo; |
| private final AllProjectsName allProjectsName; |
| private final ReceiveConfig receiveConfig; |
| |
| private final ProjectControl projectControl; |
| private final Project project; |
| private final LabelTypes labelTypes; |
| private final Repository repo; |
| private final ReceivePack rp; |
| private final NoteMap rejectCommits; |
| private MagicBranchInput magicBranch; |
| |
| private List<CreateRequest> newChanges = Collections.emptyList(); |
| private final Map<Change.Id, ReplaceRequest> replaceByChange = |
| new HashMap<Change.Id, ReplaceRequest>(); |
| private final Map<RevCommit, ReplaceRequest> replaceByCommit = |
| new HashMap<RevCommit, ReplaceRequest>(); |
| private final Set<RevCommit> validCommits = new HashSet<RevCommit>(); |
| |
| private SetMultimap<ObjectId, Ref> refsById; |
| private Map<String, Ref> allRefs; |
| |
| private final SubmoduleOp.Factory subOpFactory; |
| |
| private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>(); |
| private ListMultimap<Error, String> errors = LinkedListMultimap.create(); |
| private Task newProgress; |
| private Task replaceProgress; |
| private Task closeProgress; |
| private Task commandProgress; |
| private MessageSender messageSender; |
| private BatchRefUpdate batch; |
| |
| @Inject |
| ReceiveCommits(final ReviewDb db, |
| final SchemaFactory<ReviewDb> schemaFactory, |
| final AccountResolver accountResolver, |
| final CmdLineParser.Factory optionParserFactory, |
| final CreateChangeSender.Factory createChangeSenderFactory, |
| final MergedSender.Factory mergedSenderFactory, |
| final ReplacePatchSetSender.Factory replacePatchSetFactory, |
| final GitReferenceUpdated gitRefUpdated, |
| final PatchSetInfoFactory patchSetInfoFactory, |
| final ChangeHooks hooks, |
| final ApprovalsUtil approvalsUtil, |
| final ProjectCache projectCache, |
| final GitRepositoryManager repoManager, |
| final TagCache tagCache, |
| final ChangeCache changeCache, |
| final ChangeInserter changeInserter, |
| final CommitValidators.Factory commitValidatorsFactory, |
| @CanonicalWebUrl @Nullable final String canonicalWebUrl, |
| @GerritPersonIdent final PersonIdent gerritIdent, |
| final TrackingFooters trackingFooters, |
| final WorkQueue workQueue, |
| @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector, |
| final RequestScopePropagator requestScopePropagator, |
| final SshInfo sshInfo, |
| final AllProjectsName allProjectsName, |
| ReceiveConfig config, |
| @Assisted final ProjectControl projectControl, |
| @Assisted final Repository repo, |
| final SubmoduleOp.Factory subOpFactory) throws IOException { |
| this.currentUser = (IdentifiedUser) projectControl.getCurrentUser(); |
| this.db = db; |
| this.schemaFactory = schemaFactory; |
| this.accountResolver = accountResolver; |
| this.optionParserFactory = optionParserFactory; |
| this.createChangeSenderFactory = createChangeSenderFactory; |
| this.mergedSenderFactory = mergedSenderFactory; |
| this.replacePatchSetFactory = replacePatchSetFactory; |
| this.gitRefUpdated = gitRefUpdated; |
| this.patchSetInfoFactory = patchSetInfoFactory; |
| this.hooks = hooks; |
| this.approvalsUtil = approvalsUtil; |
| this.projectCache = projectCache; |
| this.repoManager = repoManager; |
| this.canonicalWebUrl = canonicalWebUrl; |
| this.trackingFooters = trackingFooters; |
| this.tagCache = tagCache; |
| this.changeInserter = changeInserter; |
| this.commitValidatorsFactory = commitValidatorsFactory; |
| this.workQueue = workQueue; |
| this.changeUpdateExector = changeUpdateExector; |
| this.requestScopePropagator = requestScopePropagator; |
| this.sshInfo = sshInfo; |
| this.allProjectsName = allProjectsName; |
| this.receiveConfig = config; |
| |
| this.projectControl = projectControl; |
| this.labelTypes = projectControl.getLabelTypes(); |
| this.project = projectControl.getProject(); |
| this.repo = repo; |
| this.rp = new ReceivePack(repo); |
| this.rejectCommits = loadRejectCommitsMap(); |
| |
| this.subOpFactory = subOpFactory; |
| |
| this.messageSender = new ReceivePackMessageSender(); |
| |
| rp.setAllowCreates(true); |
| rp.setAllowDeletes(true); |
| rp.setAllowNonFastForwards(true); |
| rp.setCheckReceivedObjects(true); |
| |
| if (!projectControl.allRefsAreVisible()) { |
| rp.setCheckReferencedObjectsAreReachable(config.checkReferencedObjectsAreReachable); |
| rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false)); |
| } |
| List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(3); |
| advHooks.add(new AdvertiseRefsHook() { |
| @Override |
| public void advertiseRefs(BaseReceivePack rp) { |
| allRefs = rp.getAdvertisedRefs(); |
| if (allRefs == null) { |
| allRefs = rp.getRepository().getAllRefs(); |
| } |
| rp.setAdvertisedRefs(allRefs, rp.getAdvertisedObjects()); |
| } |
| |
| @Override |
| public void advertiseRefs(UploadPack uploadPack) { |
| } |
| }); |
| advHooks.add(rp.getAdvertiseRefsHook()); |
| advHooks.add(new ReceiveCommitsAdvertiseRefsHook()); |
| rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); |
| } |
| |
| /** Add reviewers for new (or updated) changes. */ |
| public void addReviewers(Collection<Account.Id> who) { |
| reviewersFromCommandLine.addAll(who); |
| } |
| |
| /** Add reviewers for new (or updated) changes. */ |
| public void addExtraCC(Collection<Account.Id> who) { |
| ccFromCommandLine.addAll(who); |
| } |
| |
| /** Set a message sender for this operation. */ |
| public void setMessageSender(final MessageSender ms) { |
| messageSender = ms != null ? ms : new ReceivePackMessageSender(); |
| } |
| |
| MessageSender getMessageSender() { |
| if (messageSender == null) { |
| setMessageSender(null); |
| } |
| return messageSender; |
| } |
| |
| Project getProject() { |
| return project; |
| } |
| |
| /** @return the ReceivePack instance to speak the native Git protocol. */ |
| public ReceivePack getReceivePack() { |
| return rp; |
| } |
| |
| /** Scan part of history and include it in the advertisement. */ |
| public void advertiseHistory() { |
| Set<ObjectId> toInclude = new HashSet<ObjectId>(); |
| |
| // Advertise some recent open changes, in case a commit is based one. |
| try { |
| Set<PatchSet.Id> toGet = new HashSet<PatchSet.Id>(); |
| for (Change change : db.changes() |
| .byProjectOpenNext(project.getNameKey(), "z", 32)) { |
| PatchSet.Id id = change.currentPatchSetId(); |
| if (id != null) { |
| toGet.add(id); |
| } |
| } |
| for (PatchSet ps : db.patchSets().get(toGet)) { |
| if (ps.getRevision() != null && ps.getRevision().get() != null) { |
| toInclude.add(ObjectId.fromString(ps.getRevision().get())); |
| } |
| } |
| } catch (OrmException err) { |
| log.error("Cannot list open changes of " + project.getNameKey(), err); |
| } |
| |
| // Size of an additional ".have" line. |
| final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1; |
| |
| // Maximum number of bytes to "waste" in the advertisement with |
| // a peek at this repository's current reachable history. |
| final int maxExtraSize = 8192; |
| |
| // Number of recent commits to advertise immediately, hoping to |
| // show a client a nearby merge base. |
| final int base = 64; |
| |
| // Number of commits to skip once base has already been shown. |
| final int step = 16; |
| |
| // Total number of commits to extract from the history. |
| final int max = maxExtraSize / haveLineLen; |
| |
| // Scan history until the advertisement is full. |
| Set<ObjectId> alreadySending = rp.getAdvertisedObjects(); |
| RevWalk rw = rp.getRevWalk(); |
| for (ObjectId haveId : alreadySending) { |
| try { |
| rw.markStart(rw.parseCommit(haveId)); |
| } catch (IOException badCommit) { |
| continue; |
| } |
| } |
| |
| int stepCnt = 0; |
| RevCommit c; |
| try { |
| while ((c = rw.next()) != null && toInclude.size() < max) { |
| if (alreadySending.contains(c)) { |
| } else if (toInclude.contains(c)) { |
| } else if (c.getParentCount() > 1) { |
| } else if (toInclude.size() < base) { |
| toInclude.add(c); |
| } else { |
| stepCnt = ++stepCnt % step; |
| if (stepCnt == 0) { |
| toInclude.add(c); |
| } |
| } |
| } |
| } catch (IOException err) { |
| log.error("Error trying to advertise history on " + project.getNameKey(), err); |
| } |
| rw.reset(); |
| rp.getAdvertisedObjects().addAll(toInclude); |
| } |
| |
| /** Determine if the user can upload commits. */ |
| public Capable canUpload() { |
| Capable result = projectControl.canPushToAtLeastOneRef(); |
| if (result != Capable.OK) { |
| return result; |
| } |
| if (receiveConfig.checkMagicRefs) { |
| result = MagicBranch.checkMagicBranchRefs(repo, project); |
| } |
| return result; |
| } |
| |
| private void addMessage(String message) { |
| messages.add(new CommitValidationMessage(message, false)); |
| } |
| |
| void addError(String error) { |
| messages.add(new CommitValidationMessage(error, true)); |
| } |
| |
| void sendMessages() { |
| for (CommitValidationMessage m : messages) { |
| if (m.isError()) { |
| messageSender.sendError(m.getMessage()); |
| } else { |
| messageSender.sendMessage(m.getMessage()); |
| } |
| } |
| } |
| |
| void processCommands(final Collection<ReceiveCommand> commands, |
| final MultiProgressMonitor progress) { |
| newProgress = progress.beginSubTask("new", UNKNOWN); |
| replaceProgress = progress.beginSubTask("updated", UNKNOWN); |
| closeProgress = progress.beginSubTask("closed", UNKNOWN); |
| commandProgress = progress.beginSubTask("refs", UNKNOWN); |
| |
| batch = repo.getRefDatabase().newBatchUpdate(); |
| batch.setRefLogIdent(rp.getRefLogIdent()); |
| batch.setRefLogMessage("push", true); |
| |
| parseCommands(commands); |
| if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) { |
| newChanges = selectNewChanges(); |
| } |
| preparePatchSetsForReplace(); |
| |
| if (!batch.getCommands().isEmpty()) { |
| try { |
| batch.execute(rp.getRevWalk(), commandProgress); |
| } catch (IOException err) { |
| int cnt = 0; |
| for (ReceiveCommand cmd : batch.getCommands()) { |
| if (cmd.getResult() == NOT_ATTEMPTED) { |
| cmd.setResult(REJECTED_OTHER_REASON, "internal server error"); |
| cnt++; |
| } |
| } |
| log.error(String.format( |
| "Failed to store %d refs in %s", cnt, project.getName()), err); |
| } |
| } |
| |
| insertChangesAndPatchSets(); |
| newProgress.end(); |
| replaceProgress.end(); |
| |
| if (!errors.isEmpty()) { |
| for (Error error : errors.keySet()) { |
| rp.sendMessage(buildError(error, errors.get(error))); |
| } |
| rp.sendMessage(String.format("User: %s", displayName(currentUser))); |
| rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER); |
| } |
| |
| for (final ReceiveCommand c : commands) { |
| if (c.getResult() == OK) { |
| switch (c.getType()) { |
| case CREATE: |
| if (isHead(c)) { |
| autoCloseChanges(c); |
| } |
| break; |
| |
| case UPDATE: // otherwise known as a fast-forward |
| tagCache.updateFastForward(project.getNameKey(), |
| c.getRefName(), |
| c.getOldId(), |
| c.getNewId()); |
| if (isHead(c)) { |
| autoCloseChanges(c); |
| } |
| break; |
| |
| case UPDATE_NONFASTFORWARD: |
| if (isHead(c)) { |
| autoCloseChanges(c); |
| } |
| break; |
| |
| case DELETE: |
| break; |
| } |
| |
| if (isConfig(c)) { |
| projectCache.evict(project); |
| ProjectState ps = projectCache.get(project.getNameKey()); |
| repoManager.setProjectDescription(project.getNameKey(), // |
| ps.getProject().getDescription()); |
| } |
| |
| if (!MagicBranch.isMagicBranch(c.getRefName())) { |
| // We only fire gitRefUpdated for direct refs updates. |
| // Events for change refs are fired when they are created. |
| // |
| gitRefUpdated.fire(project.getNameKey(), c.getRefName(), |
| c.getOldId(), c.getNewId()); |
| hooks.doRefUpdatedHook( |
| new Branch.NameKey(project.getNameKey(), c.getRefName()), |
| c.getOldId(), |
| c.getNewId(), |
| currentUser.getAccount()); |
| } |
| } |
| } |
| closeProgress.end(); |
| commandProgress.end(); |
| progress.end(); |
| |
| Iterable<CreateRequest> created = |
| Iterables.filter(newChanges, new Predicate<CreateRequest>() { |
| @Override |
| public boolean apply(CreateRequest input) { |
| return input.created; |
| } |
| }); |
| if (!Iterables.isEmpty(created) && canonicalWebUrl != null) { |
| final String url = canonicalWebUrl; |
| addMessage(""); |
| addMessage("New Changes:"); |
| for (CreateRequest c : created) { |
| StringBuilder m = new StringBuilder() |
| .append(" ") |
| .append(url) |
| .append(c.change.getChangeId()); |
| if (c.change.getStatus() == Change.Status.DRAFT) { |
| m.append(" [DRAFT]"); |
| } |
| addMessage(m.toString()); |
| } |
| addMessage(""); |
| } |
| } |
| |
| private void insertChangesAndPatchSets() { |
| int replaceCount = 0; |
| int okToInsert = 0; |
| |
| for (Map.Entry<Change.Id, ReplaceRequest> e : replaceByChange.entrySet()) { |
| ReplaceRequest replace = e.getValue(); |
| if (magicBranch != null && replace.inputCommand == magicBranch.cmd) { |
| replaceCount++; |
| |
| if (replace.cmd != null && replace.cmd.getResult() == OK) { |
| okToInsert++; |
| } |
| } else if (replace.cmd != null && replace.cmd.getResult() == OK) { |
| try { |
| if (replace.insertPatchSet().checkedGet() != null) { |
| replace.inputCommand.setResult(OK); |
| } |
| } catch (IOException err) { |
| reject(replace.inputCommand, "internal server error"); |
| log.error(String.format( |
| "Cannot add patch set to %d of %s", |
| e.getKey().get(), project.getName()), err); |
| } catch (OrmException err) { |
| reject(replace.inputCommand, "internal server error"); |
| log.error(String.format( |
| "Cannot add patch set to %d of %s", |
| e.getKey().get(), project.getName()), err); |
| } |
| } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) { |
| reject(replace.inputCommand, "internal server error"); |
| } |
| } |
| |
| if (magicBranch == null || magicBranch.cmd.getResult() != NOT_ATTEMPTED) { |
| // refs/for/ or refs/drafts/ not used, or it already failed earlier. |
| // No need to continue. |
| return; |
| } |
| |
| for (CreateRequest create : newChanges) { |
| if (create.cmd.getResult() == OK) { |
| okToInsert++; |
| } |
| } |
| |
| if (okToInsert != replaceCount + newChanges.size()) { |
| // One or more new references failed to create. Assume the |
| // system isn't working correctly anymore and abort. |
| reject(magicBranch.cmd, "internal server error"); |
| log.error(String.format( |
| "Only %d of %d new change refs created in %s; aborting", |
| okToInsert, replaceCount + newChanges.size(), project.getName())); |
| return; |
| } |
| |
| try { |
| List<CheckedFuture<?, OrmException>> futures = Lists.newArrayList(); |
| for (ReplaceRequest replace : replaceByChange.values()) { |
| if (magicBranch != null && replace.inputCommand == magicBranch.cmd) { |
| futures.add(replace.insertPatchSet()); |
| } |
| } |
| |
| for (CreateRequest create : newChanges) { |
| futures.add(create.insertChange()); |
| } |
| |
| for (CheckedFuture<?, OrmException> f : futures) { |
| f.checkedGet(); |
| } |
| magicBranch.cmd.setResult(OK); |
| } catch (OrmException err) { |
| log.error("Can't insert changes for " + project.getName(), err); |
| reject(magicBranch.cmd, "internal server error"); |
| } catch (IOException err) { |
| log.error("Can't read commits for " + project.getName(), err); |
| reject(magicBranch.cmd, "internal server error"); |
| } |
| } |
| |
| private String buildError(Error error, List<String> branches) { |
| StringBuilder sb = new StringBuilder(); |
| if (branches.size() == 1) { |
| sb.append("Branch ").append(branches.get(0)).append(":\n"); |
| sb.append(error.get()); |
| return sb.toString(); |
| } |
| sb.append("Branches"); |
| String delim = " "; |
| for (String branch : branches) { |
| sb.append(delim).append(branch); |
| delim = ", "; |
| } |
| return sb.append(":\n").append(error.get()).toString(); |
| } |
| |
| private static String displayName(IdentifiedUser user) { |
| String displayName = user.getUserName(); |
| if (displayName == null) { |
| displayName = user.getAccount().getPreferredEmail(); |
| } |
| return displayName; |
| } |
| |
| private void parseCommands(final Collection<ReceiveCommand> commands) { |
| for (final ReceiveCommand cmd : commands) { |
| if (cmd.getResult() != NOT_ATTEMPTED) { |
| // Already rejected by the core receive process. |
| // |
| continue; |
| } |
| |
| if (!Repository.isValidRefName(cmd.getRefName()) |
| || cmd.getRefName().contains("//")) { |
| reject(cmd, "not valid ref"); |
| continue; |
| } |
| |
| HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(), |
| currentUser.getAccount(), cmd.getOldId(), |
| cmd.getNewId()); |
| |
| if (result != null) { |
| final String message = result.toString().trim(); |
| if (result.getExitValue() != 0) { |
| reject(cmd, message); |
| continue; |
| } |
| rp.sendMessage(message); |
| } |
| |
| if (MagicBranch.isMagicBranch(cmd.getRefName())) { |
| parseMagicBranch(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); |
| break; |
| |
| case UPDATE: |
| parseUpdate(cmd); |
| break; |
| |
| case DELETE: |
| parseDelete(cmd); |
| break; |
| |
| case UPDATE_NONFASTFORWARD: |
| parseRewind(cmd); |
| break; |
| |
| default: |
| reject(cmd); |
| continue; |
| } |
| |
| if (cmd.getResult() != NOT_ATTEMPTED) { |
| continue; |
| } |
| |
| if (isConfig(cmd)) { |
| if (!projectControl.isOwner()) { |
| reject(cmd, "not project owner"); |
| continue; |
| } |
| |
| switch (cmd.getType()) { |
| case CREATE: |
| case UPDATE: |
| case UPDATE_NONFASTFORWARD: |
| try { |
| ProjectConfig cfg = new ProjectConfig(project.getNameKey()); |
| cfg.load(repo, cmd.getNewId()); |
| if (!cfg.getValidationErrors().isEmpty()) { |
| addError("Invalid project configuration:"); |
| for (ValidationError err : cfg.getValidationErrors()) { |
| addError(" " + err.getMessage()); |
| } |
| reject(cmd, "invalid project configuration"); |
| log.error("User " + currentUser.getUserName() |
| + " tried to push invalid project configuration " |
| + cmd.getNewId().name() + " for " + project.getName()); |
| continue; |
| } |
| Project.NameKey newParent = cfg.getProject().getParent(allProjectsName); |
| Project.NameKey oldParent = project.getParent(allProjectsName); |
| if (oldParent == null) { |
| // update of the 'All-Projects' project |
| if (newParent != null) { |
| reject(cmd, "invalid project configuration: root project cannot have parent"); |
| continue; |
| } |
| } else { |
| if (!oldParent.equals(newParent) |
| && !currentUser.getCapabilities().canAdministrateServer()) { |
| reject(cmd, "invalid project configuration: only Gerrit admin can set parent"); |
| continue; |
| } |
| |
| if (projectCache.get(newParent) == null) { |
| reject(cmd, "invalid project configuration: parent does not exist"); |
| continue; |
| } |
| } |
| } catch (Exception e) { |
| reject(cmd, "invalid project configuration"); |
| log.error("User " + currentUser.getUserName() |
| + " tried to push invalid project configuration " |
| + cmd.getNewId().name() + " for " + project.getName(), e); |
| continue; |
| } |
| break; |
| |
| case DELETE: |
| break; |
| |
| default: |
| reject(cmd); |
| continue; |
| } |
| } |
| } |
| } |
| |
| 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); |
| batch.addCommand(cmd); |
| } 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); |
| batch.addCommand(cmd); |
| } else { |
| if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) { |
| errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG); |
| } else { |
| errors.put(Error.UPDATE, ctl.getRefName()); |
| } |
| 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.getRefName().startsWith("refs/changes/")) { |
| errors.put(Error.DELETE_CHANGES, ctl.getRefName()); |
| reject(cmd, "cannot delete changes"); |
| } else if (ctl.canDelete()) { |
| batch.addCommand(cmd); |
| } else { |
| if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) { |
| reject(cmd, "cannot delete project configuration"); |
| } else { |
| errors.put(Error.DELETE, ctl.getRefName()); |
| reject(cmd, "cannot delete references"); |
| } |
| } |
| } |
| |
| 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() != NOT_ATTEMPTED) { |
| return; |
| } |
| } |
| |
| if (ctl.canForceUpdate()) { |
| batch.setAllowNonFastForwards(true).addCommand(cmd); |
| } else { |
| cmd.setResult(REJECTED_NONFASTFORWARD, " need '" |
| + PermissionRule.FORCE_PUSH + "' privilege."); |
| } |
| } |
| |
| private static class MagicBranchInput { |
| private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings(); |
| |
| final ReceiveCommand cmd; |
| Branch.NameKey dest; |
| RefControl ctl; |
| Set<Account.Id> reviewer = Sets.newLinkedHashSet(); |
| Set<Account.Id> cc = Sets.newLinkedHashSet(); |
| |
| @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes") |
| String topic; |
| |
| @Option(name = "--draft", usage = "mark new/updated changes as draft") |
| boolean draft; |
| |
| @Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes") |
| void reviewer(Account.Id id) { |
| reviewer.add(id); |
| } |
| |
| @Option(name = "--cc", metaVar = "EMAIL", usage = "notify user by CC") |
| void cc(Account.Id id) { |
| cc.add(id); |
| } |
| |
| @Option(name = "--publish", usage = "publish new/updated changes") |
| void publish(boolean publish) { |
| draft = !publish; |
| } |
| |
| MagicBranchInput(ReceiveCommand cmd) { |
| this.cmd = cmd; |
| this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE); |
| } |
| |
| boolean isDraft() { |
| return draft; |
| } |
| |
| MailRecipients getMailRecipients() { |
| return new MailRecipients(reviewer, cc); |
| } |
| |
| String parse(CmdLineParser clp, Repository repo, Set<String> refs) |
| throws CmdLineException { |
| String ref = MagicBranch.getDestBranchName(cmd.getRefName()); |
| if (!ref.startsWith(Constants.R_REFS)) { |
| ref = Constants.R_HEADS + ref; |
| } |
| |
| int optionStart = ref.indexOf('%'); |
| if (0 < optionStart) { |
| ListMultimap<String, String> options = LinkedListMultimap.create(); |
| for (String s : COMMAS.split(ref.substring(optionStart + 1))) { |
| int e = s.indexOf('='); |
| if (0 < e) { |
| options.put(s.substring(0, e), s.substring(e + 1)); |
| } else { |
| options.put(s, ""); |
| } |
| } |
| clp.parseOptionMap(options); |
| ref = ref.substring(0, optionStart); |
| } |
| |
| // Split the destination branch by branch and topic. The topic |
| // suffix is entirely optional, so it might not even exist. |
| String head = readHEAD(repo); |
| int split = ref.length(); |
| for (;;) { |
| String name = ref.substring(0, split); |
| if (refs.contains(name) || name.equals(head)) { |
| break; |
| } |
| |
| split = name.lastIndexOf('/', split - 1); |
| if (split <= Constants.R_REFS.length()) { |
| return ref; |
| } |
| } |
| if (split < ref.length()) { |
| topic = Strings.emptyToNull(ref.substring(split + 1)); |
| } |
| return ref.substring(0, split); |
| } |
| } |
| |
| private void parseMagicBranch(final ReceiveCommand cmd) { |
| // Permit exactly one new change request per push. |
| if (magicBranch != null) { |
| reject(cmd, "duplicate request"); |
| return; |
| } |
| |
| magicBranch = new MagicBranchInput(cmd); |
| magicBranch.reviewer.addAll(reviewersFromCommandLine); |
| magicBranch.cc.addAll(ccFromCommandLine); |
| |
| String ref; |
| CmdLineParser clp = optionParserFactory.create(magicBranch); |
| try { |
| ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet()); |
| } catch (CmdLineException e) { |
| if (!clp.wasHelpRequestedByOption()) { |
| reject(cmd, e.getMessage()); |
| return; |
| } |
| ref = null; // never happen |
| } |
| if (clp.wasHelpRequestedByOption()) { |
| StringWriter w = new StringWriter(); |
| w.write("\nHelp for refs/for/branch:\n\n"); |
| clp.printUsage(w, null); |
| addMessage(w.toString()); |
| reject(cmd, "see help"); |
| return; |
| } |
| if (!rp.getAdvertisedRefs().containsKey(ref) && !ref.equals(readHEAD(repo))) { |
| if (ref.startsWith(Constants.R_HEADS)) { |
| String n = ref.substring(Constants.R_HEADS.length()); |
| reject(cmd, "branch " + n + " not found"); |
| } else { |
| reject(cmd, ref + " not found"); |
| } |
| return; |
| } |
| |
| magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref); |
| magicBranch.ctl = projectControl.controlForRef(ref); |
| if (!magicBranch.ctl.canUpload()) { |
| errors.put(Error.CODE_REVIEW, ref); |
| reject(cmd, "cannot upload review"); |
| return; |
| } |
| |
| // Validate that the new commits are connected with the target |
| // branch. If they aren't, we want to abort. We do this check by |
| // looking to see if we can compute a merge base between the new |
| // commits and the target branch head. |
| // |
| try { |
| final RevWalk walk = rp.getRevWalk(); |
| final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId()); |
| Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName()); |
| if (targetRef == null || targetRef.getObjectId() == null) { |
| // The destination branch does not yet exist. Assume the |
| // history being sent for review will start it and thus |
| // is "connected" to the branch. |
| return; |
| } |
| final RevCommit h = walk.parseCommit(targetRef.getObjectId()); |
| final RevFilter oldRevFilter = walk.getRevFilter(); |
| try { |
| walk.reset(); |
| walk.setRevFilter(RevFilter.MERGE_BASE); |
| walk.markStart(tip); |
| walk.markStart(h); |
| if (walk.next() == null) { |
| reject(magicBranch.cmd, "no common ancestry"); |
| return; |
| } |
| } finally { |
| walk.reset(); |
| walk.setRevFilter(oldRevFilter); |
| } |
| } catch (IOException e) { |
| magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT); |
| log.error("Invalid pack upload; one or more objects weren't sent", e); |
| return; |
| } |
| } |
| |
| private static String readHEAD(Repository repo) { |
| try { |
| return repo.getFullBranch(); |
| } catch (IOException e) { |
| log.error("Cannot read HEAD symref", e); |
| return null; |
| } |
| } |
| |
| /** |
| * 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 + " does not belong to project " + project.getName()); |
| return; |
| } |
| |
| requestReplace(cmd, true, changeEnt, newCommit); |
| } |
| |
| private boolean requestReplace(final ReceiveCommand cmd, |
| final boolean checkMergedInto, 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, checkMergedInto); |
| 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 List<CreateRequest> selectNewChanges() { |
| final List<CreateRequest> newChanges = Lists.newArrayList(); |
| final RevWalk walk = rp.getRevWalk(); |
| walk.reset(); |
| walk.sort(RevSort.TOPO); |
| walk.sort(RevSort.REVERSE, true); |
| try { |
| Set<ObjectId> existing = Sets.newHashSet(); |
| walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId())); |
| markHeadsAsUninteresting( |
| walk, |
| existing, |
| magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null); |
| |
| List<ChangeLookup> pending = Lists.newArrayList(); |
| final Set<Change.Key> newChangeIds = new HashSet<Change.Key>(); |
| for (;;) { |
| final RevCommit c = walk.next(); |
| if (c == null) { |
| break; |
| } |
| if (existing.contains(c) || replaceByCommit.containsKey(c)) { |
| // This commit was already scheduled to replace an existing PatchSet. |
| // |
| continue; |
| } |
| |
| if (!validCommit(magicBranch.ctl, magicBranch.cmd, c)) { |
| // Not a change the user can propose? Abort as early as possible. |
| // |
| return Collections.emptyList(); |
| } |
| |
| Change.Key changeKey = new Change.Key("I" + c.name()); |
| final List<String> idList = c.getFooterLines(CHANGE_ID); |
| if (idList.isEmpty()) { |
| newChanges.add(new CreateRequest(c, changeKey)); |
| continue; |
| } |
| |
| final String idStr = idList.get(idList.size() - 1).trim(); |
| if (idStr.matches("^I00*$")) { |
| // Reject this invalid line from EGit. |
| reject(magicBranch.cmd, "invalid Change-Id"); |
| return Collections.emptyList(); |
| } |
| |
| changeKey = new Change.Key(idStr); |
| pending.add(new ChangeLookup(c, changeKey)); |
| } |
| |
| for (ChangeLookup p : pending) { |
| if (newChangeIds.contains(p.changeKey)) { |
| reject(magicBranch.cmd, "squash commits first"); |
| return Collections.emptyList(); |
| } |
| |
| List<Change> changes = p.changes.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(magicBranch.cmd, p.changeKey.get() + " has duplicates"); |
| return Collections.emptyList(); |
| } |
| |
| if (changes.size() == 1) { |
| // Schedule as a replacement to this one matching change. |
| // |
| if (requestReplace(magicBranch.cmd, false, changes.get(0), p.commit)) { |
| continue; |
| } else { |
| return Collections.emptyList(); |
| } |
| } |
| |
| if (changes.size() == 0) { |
| if (!isValidChangeId(p.changeKey.get())) { |
| reject(magicBranch.cmd, "invalid Change-Id"); |
| return Collections.emptyList(); |
| } |
| |
| newChangeIds.add(p.changeKey); |
| } |
| newChanges.add(new CreateRequest(p.commit, p.changeKey)); |
| } |
| } catch (IOException e) { |
| // Should never happen, the core receive process would have |
| // identified the missing object earlier before we got control. |
| // |
| magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT); |
| log.error("Invalid pack upload; one or more objects weren't sent", e); |
| return Collections.emptyList(); |
| } catch (OrmException e) { |
| log.error("Cannot query database to locate prior changes", e); |
| reject(magicBranch.cmd, "database error"); |
| return Collections.emptyList(); |
| } |
| |
| if (newChanges.isEmpty() && replaceByChange.isEmpty()) { |
| reject(magicBranch.cmd, "no new changes"); |
| return Collections.emptyList(); |
| } |
| for (CreateRequest create : newChanges) { |
| batch.addCommand(create.cmd); |
| } |
| return newChanges; |
| } |
| |
| private void markHeadsAsUninteresting( |
| final RevWalk walk, |
| Set<ObjectId> existing, |
| @Nullable String forRef) { |
| for (Ref ref : allRefs.values()) { |
| if (ref.getObjectId() == null) { |
| continue; |
| } else if (ref.getName().startsWith("refs/changes/")) { |
| existing.add(ref.getObjectId()); |
| } else if (ref.getName().startsWith(R_HEADS) |
| || (forRef != null && forRef.equals(ref.getName()))) { |
| try { |
| walk.markUninteresting(walk.parseCommit(ref.getObjectId())); |
| } catch (IOException e) { |
| log.warn(String.format("Invalid ref %s in %s", |
| ref.getName(), project.getName()), e); |
| continue; |
| } |
| } |
| } |
| } |
| |
| private static boolean isValidChangeId(String idStr) { |
| return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$"); |
| } |
| |
| private class ChangeLookup { |
| final RevCommit commit; |
| final Change.Key changeKey; |
| final ResultSet<Change> changes; |
| |
| ChangeLookup(RevCommit c, Change.Key key) throws OrmException { |
| commit = c; |
| changeKey = key; |
| changes = db.changes().byBranchKey(magicBranch.dest, key); |
| } |
| } |
| |
| private class CreateRequest { |
| final RevCommit commit; |
| final Change change; |
| final PatchSet ps; |
| final ReceiveCommand cmd; |
| private final PatchSetInfo info; |
| boolean created; |
| |
| CreateRequest(RevCommit c, Change.Key changeKey) throws OrmException { |
| commit = c; |
| |
| change = new Change(changeKey, |
| new Change.Id(db.nextChangeId()), |
| currentUser.getAccountId(), |
| magicBranch.dest); |
| change.setTopic(magicBranch.topic); |
| |
| ps = new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID)); |
| ps.setCreatedOn(change.getCreatedOn()); |
| ps.setUploader(change.getOwner()); |
| ps.setRevision(toRevId(c)); |
| |
| if (magicBranch.isDraft()) { |
| change.setStatus(Change.Status.DRAFT); |
| ps.setDraft(true); |
| } |
| |
| info = patchSetInfoFactory.get(c, ps.getId()); |
| change.setCurrentPatchSet(info); |
| ChangeUtil.updated(change); |
| cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName()); |
| } |
| |
| CheckedFuture<Void, OrmException> insertChange() throws IOException { |
| rp.getRevWalk().parseBody(commit); |
| |
| final Thread caller = Thread.currentThread(); |
| ListenableFuture<Void> future = changeUpdateExector.submit( |
| requestScopePropagator.wrap(new Callable<Void>() { |
| @Override |
| public Void call() throws OrmException { |
| if (caller == Thread.currentThread()) { |
| insertChange(db); |
| } else { |
| ReviewDb db = schemaFactory.open(); |
| try { |
| insertChange(db); |
| } finally { |
| db.close(); |
| } |
| } |
| synchronized (newProgress) { |
| newProgress.update(1); |
| } |
| return null; |
| } |
| })); |
| return Futures.makeChecked(future, ORM_EXCEPTION); |
| } |
| |
| private void insertChange(ReviewDb db) throws OrmException { |
| final Account.Id me = currentUser.getAccountId(); |
| final List<FooterLine> footerLines = commit.getFooterLines(); |
| final MailRecipients recipients = new MailRecipients(); |
| if (magicBranch != null) { |
| recipients.add(magicBranch.getMailRecipients()); |
| } |
| recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines)); |
| recipients.remove(me); |
| |
| changeInserter.insertChange(db, change, ps, commit, labelTypes, |
| footerLines, info, recipients.getReviewers()); |
| |
| created = true; |
| |
| workQueue.getDefaultQueue() |
| .submit(requestScopePropagator.wrap(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| CreateChangeSender cm = |
| createChangeSenderFactory.create(change); |
| cm.setFrom(me); |
| cm.setPatchSet(ps, info); |
| cm.addReviewers(recipients.getReviewers()); |
| cm.addExtraCC(recipients.getCcOnly()); |
| cm.send(); |
| } catch (Exception e) { |
| log.error("Cannot send email for new change " + change.getId(), e); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "send-email newchange"; |
| } |
| })); |
| } |
| } |
| |
| private void preparePatchSetsForReplace() { |
| try { |
| readChangesForReplace(); |
| readPatchSetsForReplace(); |
| for (ReplaceRequest req : replaceByChange.values()) { |
| if (req.inputCommand.getResult() == NOT_ATTEMPTED) { |
| req.validate(false); |
| } |
| } |
| } catch (OrmException err) { |
| log.error("Cannot read database before replacement", err); |
| for (ReplaceRequest req : replaceByChange.values()) { |
| if (req.inputCommand.getResult() == NOT_ATTEMPTED) { |
| req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error"); |
| } |
| } |
| } catch (IOException err) { |
| log.error("Cannot read repository before replacement", err); |
| for (ReplaceRequest req : replaceByChange.values()) { |
| if (req.inputCommand.getResult() == NOT_ATTEMPTED) { |
| req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error"); |
| } |
| } |
| } |
| |
| for (ReplaceRequest req : replaceByChange.values()) { |
| if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) { |
| batch.addCommand(req.cmd); |
| } |
| } |
| |
| if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) { |
| // Cancel creations tied to refs/for/ or refs/drafts/ command. |
| for (ReplaceRequest req : replaceByChange.values()) { |
| if (req.inputCommand == magicBranch.cmd && req.cmd != null) { |
| req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted"); |
| } |
| } |
| for (CreateRequest req : newChanges) { |
| req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted"); |
| } |
| } |
| } |
| |
| private void readChangesForReplace() throws OrmException { |
| List<CheckedFuture<Change, OrmException>> futures = |
| Lists.newArrayListWithCapacity(replaceByChange.size()); |
| for (ReplaceRequest request : replaceByChange.values()) { |
| futures.add(db.changes().getAsync(request.ontoChange)); |
| } |
| for (CheckedFuture<Change, OrmException> f : futures) { |
| Change c = f.checkedGet(); |
| if (c != null) { |
| replaceByChange.get(c.getId()).change = c; |
| } |
| } |
| } |
| |
| private void readPatchSetsForReplace() throws OrmException { |
| Map<Change.Id, ResultSet<PatchSet>> results = Maps.newHashMap(); |
| for (ReplaceRequest request : replaceByChange.values()) { |
| Change.Id id = request.ontoChange; |
| results.put(id, db.patchSets().byChange(id)); |
| } |
| for (ReplaceRequest req : replaceByChange.values()) { |
| req.patchSets = results.get(req.ontoChange).toList(); |
| } |
| } |
| |
| private class ReplaceRequest { |
| final Change.Id ontoChange; |
| final RevCommit newCommit; |
| final ReceiveCommand inputCommand; |
| final boolean checkMergedInto; |
| Change change; |
| ChangeControl changeCtl; |
| List<PatchSet> patchSets; |
| PatchSet newPatchSet; |
| ReceiveCommand cmd; |
| PatchSetInfo info; |
| ChangeMessage msg; |
| String mergedIntoRef; |
| private PatchSet.Id priorPatchSet; |
| |
| ReplaceRequest(final Change.Id toChange, final RevCommit newCommit, |
| final ReceiveCommand cmd, final boolean checkMergedInto) { |
| this.ontoChange = toChange; |
| this.newCommit = newCommit; |
| this.inputCommand = cmd; |
| this.checkMergedInto = checkMergedInto; |
| } |
| |
| boolean validate(boolean autoClose) throws IOException { |
| if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) { |
| return false; |
| } |
| |
| if (change == null || patchSets == null) { |
| reject(inputCommand, "change " + ontoChange + " not found"); |
| return false; |
| } |
| |
| if (change.getStatus().isClosed()) { |
| reject(inputCommand, "change " + ontoChange + " closed"); |
| return false; |
| } |
| |
| changeCtl = projectControl.controlFor(change); |
| if (!changeCtl.canAddPatchSet()) { |
| reject(inputCommand, "cannot replace " + ontoChange); |
| return false; |
| } |
| |
| rp.getRevWalk().parseBody(newCommit); |
| |
| if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) { |
| return false; |
| } |
| |
| priorPatchSet = change.currentPatchSetId(); |
| for (final PatchSet ps : patchSets) { |
| if (ps.getRevision() == null) { |
| log.warn("Patch set " + ps.getId() + " has no revision"); |
| reject(inputCommand, "change state corrupt"); |
| return false; |
| } |
| |
| 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(inputCommand, "change state corrupt"); |
| return false; |
| } |
| |
| try { |
| final RevCommit prior = rp.getRevWalk().parseCommit(commitId); |
| |
| // Don't allow the same commit to appear twice on the same change |
| // |
| if (newCommit == prior) { |
| reject(inputCommand, "commit already exists"); |
| return false; |
| } |
| |
| // 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, newCommit)) { |
| reject(inputCommand, "squash commits first"); |
| return false; |
| } |
| |
| // 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()) && newCommit.getTree() == prior.getTree()) { |
| rp.getRevWalk().parseBody(prior); |
| final boolean messageEq = |
| eq(newCommit.getFullMessage(), prior.getFullMessage()); |
| final boolean parentsEq = parentsEqual(newCommit, prior); |
| final boolean authorEq = authorEqual(newCommit, prior); |
| |
| if (messageEq && parentsEq && authorEq && !autoClose) { |
| reject(inputCommand, "no changes made"); |
| return false; |
| } else { |
| ObjectReader reader = rp.getRevWalk().getObjectReader(); |
| StringBuilder msg = new StringBuilder(); |
| msg.append("(W) "); |
| msg.append(reader.abbreviate(newCommit).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"); |
| } |
| addMessage(msg.toString()); |
| } |
| } |
| } catch (IOException e) { |
| log.error("Change " + change.getId() + " missing " + revIdStr, e); |
| reject(inputCommand, "change state corrupt"); |
| return false; |
| } |
| } |
| |
| PatchSet.Id id = |
| ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId()); |
| newPatchSet = new PatchSet(id); |
| newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis())); |
| newPatchSet.setUploader(currentUser.getAccountId()); |
| newPatchSet.setRevision(toRevId(newCommit)); |
| if (magicBranch != null && magicBranch.isDraft()) { |
| newPatchSet.setDraft(true); |
| } |
| info = patchSetInfoFactory.get(newCommit, newPatchSet.getId()); |
| cmd = new ReceiveCommand( |
| ObjectId.zeroId(), |
| newCommit, |
| newPatchSet.getRefName()); |
| return true; |
| } |
| |
| CheckedFuture<PatchSet.Id, OrmException> insertPatchSet() |
| throws IOException { |
| rp.getRevWalk().parseBody(newCommit); |
| |
| final Thread caller = Thread.currentThread(); |
| ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit( |
| requestScopePropagator.wrap(new Callable<PatchSet.Id>() { |
| @Override |
| public PatchSet.Id call() throws OrmException { |
| try { |
| if (caller == Thread.currentThread()) { |
| return insertPatchSet(db); |
| } else { |
| ReviewDb db = schemaFactory.open(); |
| try { |
| return insertPatchSet(db); |
| } finally { |
| db.close(); |
| } |
| } |
| } finally { |
| synchronized (replaceProgress) { |
| replaceProgress.update(1); |
| } |
| } |
| } |
| })); |
| return Futures.makeChecked(future, ORM_EXCEPTION); |
| } |
| |
| PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException { |
| final Account.Id me = currentUser.getAccountId(); |
| final List<FooterLine> footerLines = newCommit.getFooterLines(); |
| final MailRecipients recipients = new MailRecipients(); |
| if (magicBranch != null) { |
| recipients.add(magicBranch.getMailRecipients()); |
| } |
| recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines)); |
| recipients.remove(me); |
| |
| db.changes().beginTransaction(change.getId()); |
| try { |
| change = db.changes().get(change.getId()); |
| if (change == null || change.getStatus().isClosed()) { |
| reject(inputCommand, "change is closed"); |
| return null; |
| } |
| |
| ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit); |
| db.patchSets().insert(Collections.singleton(newPatchSet)); |
| |
| if (checkMergedInto) { |
| final Ref mergedInto = findMergedInto(change.getDest().get(), newCommit); |
| mergedIntoRef = mergedInto != null ? mergedInto.getName() : null; |
| } |
| |
| List<PatchSetApproval> patchSetApprovals = |
| approvalsUtil.copyVetosToPatchSet(db, labelTypes, newPatchSet.getId()); |
| final MailRecipients oldRecipients = |
| getRecipientsFromApprovals(patchSetApprovals); |
| approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info, |
| recipients.getReviewers(), oldRecipients.getAll()); |
| recipients.add(oldRecipients); |
| |
| msg = |
| new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil |
| .messageUUID(db)), me, newPatchSet.getCreatedOn(), newPatchSet.getId()); |
| msg.setMessage("Uploaded patch set " + newPatchSet.getPatchSetId() + "."); |
| db.changeMessages().insert(Collections.singleton(msg)); |
| if (change.currentPatchSetId().equals(priorPatchSet)) { |
| ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines); |
| } |
| |
| if (mergedIntoRef == null) { |
| // 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().isClosed()) { |
| return null; |
| } |
| |
| if (!change.currentPatchSetId().equals(priorPatchSet)) { |
| return change; |
| } |
| |
| if (magicBranch != null && magicBranch.topic != null) { |
| change.setTopic(magicBranch.topic); |
| } |
| if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) { |
| // Leave in draft status. |
| } else { |
| change.setStatus(Change.Status.NEW); |
| } |
| change.setLastSha1MergeTested(null); |
| change.setCurrentPatchSet(info); |
| |
| final List<String> idList = newCommit.getFooterLines(CHANGE_ID); |
| if (idList.isEmpty()) { |
| change.setKey(new Change.Key("I" + newCommit.name())); |
| } else { |
| change.setKey(new Change.Key(idList.get(idList.size() - 1).trim())); |
| } |
| |
| ChangeUtil.updated(change); |
| return change; |
| } |
| }); |
| if (change == null) { |
| db.patchSets().delete(Collections.singleton(newPatchSet)); |
| db.changeMessages().delete(Collections.singleton(msg)); |
| reject(inputCommand, "change is closed"); |
| return null; |
| } |
| } |
| |
| db.commit(); |
| } finally { |
| db.rollback(); |
| } |
| |
| if (mergedIntoRef != null) { |
| // Change was already submitted to a branch, close it. |
| // |
| markChangeMergedByPush(db, this); |
| } |
| |
| if (cmd.getResult() == NOT_ATTEMPTED) { |
| cmd.execute(rp); |
| } |
| gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(), |
| ObjectId.zeroId(), newCommit); |
| hooks.doPatchsetCreatedHook(change, newPatchSet, db); |
| if (mergedIntoRef != null) { |
| hooks.doChangeMergedHook( |
| change, currentUser.getAccount(), newPatchSet, db); |
| } |
| workQueue.getDefaultQueue() |
| .submit(requestScopePropagator.wrap(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| ReplacePatchSetSender cm = |
| replacePatchSetFactory.create(change); |
| cm.setFrom(me); |
| cm.setPatchSet(newPatchSet, info); |
| cm.setChangeMessage(msg); |
| cm.addReviewers(recipients.getReviewers()); |
| cm.addExtraCC(recipients.getCcOnly()); |
| cm.send(); |
| } catch (Exception e) { |
| log.error("Cannot send email for new patch set " + newPatchSet.getId(), e); |
| } |
| if (mergedIntoRef != null) { |
| sendMergedEmail(ReplaceRequest.this); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "send-email newpatchset"; |
| } |
| })); |
| return newPatchSet.getId(); |
| } |
| } |
| |
| 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 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 void validateNewCommits(RefControl ctl, ReceiveCommand cmd) { |
| if (ctl.canForgeAuthor() |
| && ctl.canForgeCommitter() |
| && ctl.canForgeGerritServerIdentity() |
| && ctl.canUploadMerges() |
| && !projectControl.getProjectState().isUseSignedOffBy() |
| && Iterables.isEmpty(rejectCommits) |
| && !GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName()) |
| && !(MagicBranch.isMagicBranch(cmd.getRefName()) |
| || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) { |
| return; |
| } |
| |
| final RevWalk walk = rp.getRevWalk(); |
| walk.reset(); |
| walk.sort(RevSort.NONE); |
| try { |
| Set<ObjectId> existing = Sets.newHashSet(); |
| walk.markStart(walk.parseCommit(cmd.getNewId())); |
| markHeadsAsUninteresting(walk, existing, cmd.getRefName()); |
| |
| RevCommit c; |
| while ((c = walk.next()) != null) { |
| if (existing.contains(c)) { |
| continue; |
| } else if (!validCommit(ctl, cmd, c)) { |
| break; |
| } |
| } |
| } catch (IOException err) { |
| cmd.setResult(REJECTED_MISSING_OBJECT); |
| log.error("Invalid pack upload; one or more objects weren't sent", err); |
| } |
| } |
| |
| private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd, |
| final RevCommit c) throws MissingObjectException, IOException { |
| |
| if (validCommits.contains(c)) { |
| return true; |
| } |
| |
| CommitReceivedEvent receiveEvent = |
| new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser); |
| CommitValidators commitValidators = |
| commitValidatorsFactory.create(ctl, sshInfo, repo); |
| |
| try { |
| messages.addAll(commitValidators.validateForReceiveCommits(receiveEvent)); |
| } catch (CommitValidationException e) { |
| messages.addAll(e.getMessages()); |
| reject(cmd, e.getMessage()); |
| return false; |
| } |
| validCommits.add(c); |
| return true; |
| } |
| |
| 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 SetMultimap<ObjectId, Ref> byCommit = changeRefsById(); |
| final Map<Change.Key, Change.Id> byKey = openChangesByKey( |
| new Branch.NameKey(project.getNameKey(), cmd.getRefName())); |
| final List<ReplaceRequest> toClose = new ArrayList<ReplaceRequest>(); |
| RevCommit c; |
| while ((c = rw.next()) != null) { |
| final Set<Ref> refs = byCommit.get(c.copy()); |
| for (Ref ref : refs) { |
| if (ref != null) { |
| rw.parseBody(c); |
| Change.Key closedChange = |
| closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c); |
| closeProgress.update(1); |
| if (closedChange != null) { |
| byKey.remove(closedChange); |
| } |
| } |
| } |
| |
| 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) { |
| final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false); |
| req.change = db.changes().get(onto); |
| req.patchSets = db.patchSets().byChange(onto).toList(); |
| toClose.add(req); |
| break; |
| } |
| } |
| } |
| |
| for (final ReplaceRequest req : toClose) { |
| final PatchSet.Id psi = req.validate(true) |
| ? req.insertPatchSet().checkedGet() |
| : null; |
| if (psi != null) { |
| closeChange(req.inputCommand, psi, req.newCommit); |
| closeProgress.update(1); |
| } |
| } |
| |
| // It handles gitlinks if required. |
| |
| rw.reset(); |
| final RevCommit codeReviewCommit = rw.parseCommit(cmd.getNewId()); |
| |
| final SubmoduleOp subOp = |
| subOpFactory.create( |
| new Branch.NameKey(project.getNameKey(), cmd.getRefName()), |
| codeReviewCommit, rw, repo, project, new ArrayList<Change>(), |
| new HashMap<Change.Id, CodeReviewCommit>()); |
| subOp.update(); |
| } 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); |
| } catch (SubmoduleException e) { |
| log.error("Can't complete git links check", e); |
| } |
| } |
| |
| private Change.Key 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 null; |
| } |
| |
| if (change.getStatus() == Change.Status.MERGED || |
| change.getStatus() == Change.Status.ABANDONED || |
| !change.getDest().get().equals(refName)) { |
| // If it's already merged or the commit is not aimed for |
| // this change's destination, don't make further updates. |
| // |
| return null; |
| } |
| |
| ReplaceRequest result = new ReplaceRequest(cid, commit, cmd, false); |
| result.change = change; |
| result.newPatchSet = ps; |
| result.info = patchSetInfoFactory.get(commit, psi); |
| result.mergedIntoRef = refName; |
| markChangeMergedByPush(db, result); |
| hooks.doChangeMergedHook( |
| change, currentUser.getAccount(), result.newPatchSet, db); |
| sendMergedEmail(result); |
| return change.getKey(); |
| } |
| |
| private SetMultimap<ObjectId, Ref> changeRefsById() throws IOException { |
| if (refsById == null) { |
| refsById = HashMultimap.create(); |
| 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(Branch.NameKey branch) |
| throws OrmException { |
| final Map<Change.Key, Change.Id> r = new HashMap<Change.Key, Change.Id>(); |
| for (Change c : db.changes().byBranchOpenAll(branch)) { |
| r.put(c.getKey(), c.getId()); |
| } |
| return r; |
| } |
| |
| private void markChangeMergedByPush(final ReviewDb db, |
| final ReplaceRequest 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); |
| |
| approvalsUtil.syncChangeStatus(change); |
| |
| 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(Repository.shortenRefName(mergedIntoRef)); |
| } else { |
| msgBuf.append(mergedIntoRef); |
| } |
| } |
| msgBuf.append("."); |
| final ChangeMessage msg = |
| new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil |
| .messageUUID(db)), currentUser.getAccountId(), result.info.getKey()); |
| 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 ReplaceRequest result) { |
| workQueue.getDefaultQueue() |
| .submit(requestScopePropagator.wrap(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| final MergedSender cm = mergedSenderFactory.create(result.changeCtl); |
| cm.setFrom(currentUser.getAccountId()); |
| cm.setPatchSet(result.newPatchSet, result.info); |
| cm.send(); |
| } catch (Exception e) { |
| final PatchSet.Id psi = result.newPatchSet.getId(); |
| log.error("Cannot send email for submitted patch set " + psi, e); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "send-email merged"; |
| } |
| })); |
| } |
| |
| private static RevId toRevId(final RevCommit src) { |
| return new RevId(src.getId().name()); |
| } |
| |
| private void reject(final ReceiveCommand cmd) { |
| reject(cmd, "prohibited by Gerrit"); |
| } |
| |
| private void reject(final ReceiveCommand cmd, final String why) { |
| cmd.setResult(REJECTED_OTHER_REASON, why); |
| commandProgress.update(1); |
| } |
| |
| 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); |
| } |
| |
| private static boolean isConfig(final ReceiveCommand cmd) { |
| return cmd.getRefName().equals(GitRepositoryManager.REF_CONFIG); |
| } |
| } |