// 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.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.common.FooterConstants.CHANGE_ID; import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES; import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag; import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN; import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.eclipse.jgit.lib.Constants.R_HEADS; import static org.eclipse.jgit.lib.RefDatabase.ALL; 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.Splitter; import com.google.common.base.Strings; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; 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.MultimapBuilder; import com.google.common.collect.Sets; import com.google.common.collect.SortedSetMultimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.Capable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.extensions.api.changes.HashtagsInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicMap.Entry; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; 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.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.RefNames; 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.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.SetHashtagsOp; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.ProjectConfigEntry; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditUtil; import com.google.gerrit.server.events.CommitReceivedEvent; 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.git.validators.RefOperationValidationException; import com.google.gerrit.server.git.validators.RefOperationValidators; import com.google.gerrit.server.git.validators.ValidationMessage; import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.mail.MailUtil.MailRecipients; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.permissions.ChangePermission; import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; 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.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; import com.google.gerrit.server.update.RepoContext; import com.google.gerrit.server.update.RepoOnlyOp; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.server.util.RequestId; import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gerrit.util.cli.CmdLineParser; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; 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.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.RefFilter; import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.UploadPack; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** 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 String COMMAND_REJECTION_MESSAGE_FOOTER = "Please read the documentation and contact an administrator\n" + "if you feel the configuration is incorrect"; private static final String SAME_CHANGE_ID_IN_MULTIPLE_CHANGES = "same Change-Id in multiple changes.\n" + "Squash the commits with the same Change-Id or " + "ensure Change-Ids are unique for each commit"; 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 " + RefNames.REFS_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'\nflag 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, RestApiException> INSERT_EXCEPTION = new Function<Exception, RestApiException>() { @Override public RestApiException apply(Exception input) { if (input instanceof RestApiException) { return (RestApiException) input; } else if ((input instanceof ExecutionException) && (input.getCause() instanceof RestApiException)) { return (RestApiException) input.getCause(); } return new RestApiException("Error inserting change/patchset", input); } }; private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet(); private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet(); private final IdentifiedUser user; private final ReviewDb db; private final Sequences seq; private final Provider<InternalChangeQuery> queryProvider; private final ChangeNotes.Factory notesFactory; private final AccountResolver accountResolver; private final PermissionBackend permissionBackend; private final PermissionBackend.ForProject permissions; private final CmdLineParser.Factory optionParserFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; private final ProjectCache projectCache; private final String canonicalWebUrl; private final CommitValidators.Factory commitValidatorsFactory; private final RefOperationValidators.Factory refValidatorsFactory; private final TagCache tagCache; private final AccountCache accountCache; private final ChangeInserter.Factory changeInserterFactory; private final RequestScopePropagator requestScopePropagator; private final SshInfo sshInfo; private final AllProjectsName allProjectsName; private final ReceiveConfig receiveConfig; private final DynamicSet<ReceivePackInitializer> initializers; private final BatchUpdate.Factory batchUpdateFactory; private final SetHashtagsOp.Factory hashtagsFactory; private final ReplaceOp.Factory replaceOpFactory; private final MergedByPushOp.Factory mergedByPushOpFactory; 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 final RequestId receiveId; private MagicBranchInput magicBranch; private boolean newChangeForAllNotInTarget; private final ListMultimap<String, String> pushOptions = LinkedListMultimap.create(); private List<CreateRequest> newChanges = Collections.emptyList(); private final Map<Change.Id, ReplaceRequest> replaceByChange = new LinkedHashMap<>(); private final List<UpdateGroupsRequest> updateGroups = new ArrayList<>(); private final Set<ObjectId> validCommits = new HashSet<>(); private ListMultimap<Change.Id, Ref> refsByChange; private ListMultimap<ObjectId, Ref> refsById; private Map<String, Ref> allRefs; private final SubmoduleOp.Factory subOpFactory; private final Provider<MergeOp> mergeOpProvider; private final Provider<MergeOpRepoManager> ormProvider; private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final NotesMigration notesMigration; private final ChangeEditUtil editUtil; private final ChangeIndexer indexer; /** * Actual commands to be executed, as opposed to the mix of actual and magic commands that were * provided over the wire. * * <p>Excludes commands executed implicitly as part of other {@link BatchUpdateOp}s, such as * creating patch set refs. */ private final List<ReceiveCommand> actualCommands = new ArrayList<>(); private final List<ValidationMessage> messages = new ArrayList<>(); private ListMultimap<Error, String> errors = LinkedListMultimap.create(); private Task newProgress; private Task replaceProgress; private Task closeProgress; private Task commandProgress; private MessageSender messageSender; @Inject ReceiveCommits( ReviewDb db, Sequences seq, Provider<InternalChangeQuery> queryProvider, ChangeNotes.Factory notesFactory, AccountResolver accountResolver, PermissionBackend permissionBackend, CmdLineParser.Factory optionParserFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, ProjectCache projectCache, TagCache tagCache, AccountCache accountCache, @Nullable SearchingChangeCacheImpl changeCache, ChangeInserter.Factory changeInserterFactory, CommitValidators.Factory commitValidatorsFactory, RefOperationValidators.Factory refValidatorsFactory, @CanonicalWebUrl String canonicalWebUrl, RequestScopePropagator requestScopePropagator, SshInfo sshInfo, AllProjectsName allProjectsName, ReceiveConfig receiveConfig, TransferConfig transferConfig, DynamicSet<ReceivePackInitializer> initializers, Provider<LazyPostReceiveHookChain> lazyPostReceive, @Assisted ProjectControl projectControl, @Assisted Repository repo, SubmoduleOp.Factory subOpFactory, Provider<MergeOp> mergeOpProvider, Provider<MergeOpRepoManager> ormProvider, DynamicMap<ProjectConfigEntry> pluginConfigEntries, NotesMigration notesMigration, ChangeEditUtil editUtil, ChangeIndexer indexer, BatchUpdate.Factory batchUpdateFactory, SetHashtagsOp.Factory hashtagsFactory, ReplaceOp.Factory replaceOpFactory, MergedByPushOp.Factory mergedByPushOpFactory) throws IOException, PermissionBackendException { this.user = projectControl.getUser().asIdentifiedUser(); this.db = db; this.seq = seq; this.queryProvider = queryProvider; this.notesFactory = notesFactory; this.accountResolver = accountResolver; this.permissionBackend = permissionBackend; this.optionParserFactory = optionParserFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.projectCache = projectCache; this.canonicalWebUrl = canonicalWebUrl; this.tagCache = tagCache; this.accountCache = accountCache; this.changeInserterFactory = changeInserterFactory; this.commitValidatorsFactory = commitValidatorsFactory; this.refValidatorsFactory = refValidatorsFactory; this.requestScopePropagator = requestScopePropagator; this.sshInfo = sshInfo; this.allProjectsName = allProjectsName; this.receiveConfig = receiveConfig; this.initializers = initializers; this.batchUpdateFactory = batchUpdateFactory; this.hashtagsFactory = hashtagsFactory; this.replaceOpFactory = replaceOpFactory; this.mergedByPushOpFactory = mergedByPushOpFactory; this.projectControl = projectControl; this.labelTypes = projectControl.getLabelTypes(); this.project = projectControl.getProject(); this.repo = repo; this.rp = new ReceivePack(repo); this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk()); this.receiveId = RequestId.forProject(project.getNameKey()); this.subOpFactory = subOpFactory; this.mergeOpProvider = mergeOpProvider; this.ormProvider = ormProvider; this.pluginConfigEntries = pluginConfigEntries; this.notesMigration = notesMigration; this.editUtil = editUtil; this.indexer = indexer; this.messageSender = new ReceivePackMessageSender(); ProjectState ps = projectControl.getProjectState(); this.newChangeForAllNotInTarget = ps.isCreateNewChangeForAllNotInTarget(); rp.setAllowCreates(true); rp.setAllowDeletes(true); rp.setAllowNonFastForwards(true); rp.setRefLogIdent(user.newRefLogIdent()); rp.setTimeout(transferConfig.getTimeout()); rp.setMaxObjectSizeLimit( transferConfig.getEffectiveMaxObjectSizeLimit(projectControl.getProjectState())); rp.setCheckReceivedObjects(ps.getConfig().getCheckReceivedObjects()); rp.setRefFilter( new RefFilter() { @Override public Map<String, Ref> filter(Map<String, Ref> refs) { Map<String, Ref> filteredRefs = Maps.newHashMapWithExpectedSize(refs.size()); for (Map.Entry<String, Ref> e : refs.entrySet()) { String name = e.getKey(); if (!name.startsWith(REFS_CHANGES) && !name.startsWith(RefNames.REFS_CACHE_AUTOMERGE)) { filteredRefs.put(name, e.getValue()); } } return filteredRefs; } }); permissions = permissionBackend.user(user).project(project.getNameKey()); // If the user lacks READ permission, some references may be filtered and hidden from view. // Check objects mentioned inside the incoming pack file are reachable from visible refs. try { permissionBackend.user(user).project(project.getNameKey()).check(ProjectPermission.READ); } catch (AuthException e) { rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable); } rp.setAdvertiseRefsHook( new VisibleRefFilter(tagCache, notesFactory, changeCache, repo, projectControl, db, false)); List<AdvertiseRefsHook> advHooks = new ArrayList<>(3); advHooks.add( new AdvertiseRefsHook() { @Override public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException { allRefs = rp.getAdvertisedRefs(); if (allRefs == null) { try { allRefs = rp.getRepository().getRefDatabase().getRefs(ALL); } catch (ServiceMayNotContinueException e) { throw e; } catch (IOException e) { ServiceMayNotContinueException ex = new ServiceMayNotContinueException(); ex.initCause(e); throw ex; } } rp.setAdvertisedRefs(allRefs, rp.getAdvertisedObjects()); } @Override public void advertiseRefs(UploadPack uploadPack) {} }); advHooks.add(rp.getAdvertiseRefsHook()); advHooks.add( new ReceiveCommitsAdvertiseRefsHook( queryProvider, projectControl.getProject().getNameKey())); advHooks.add(new HackPushNegotiateHook()); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setPostReceiveHook(lazyPostReceive.get()); rp.setAllowPushOptions(true); } public void init() { for (ReceivePackInitializer i : initializers) { i.init(projectControl.getProject().getNameKey(), rp); } } /** 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(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; } /** 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 (ValidationMessage m : messages) { if (m.isError()) { messageSender.sendError(m.getMessage()); } else { messageSender.sendMessage(m.getMessage()); } } } void processCommands(Collection<ReceiveCommand> commands, MultiProgressMonitor progress) { newProgress = progress.beginSubTask("new", UNKNOWN); replaceProgress = progress.beginSubTask("updated", UNKNOWN); closeProgress = progress.beginSubTask("closed", UNKNOWN); commandProgress = progress.beginSubTask("refs", UNKNOWN); try { parseCommands(commands); } catch (PermissionBackendException err) { for (ReceiveCommand cmd : actualCommands) { if (cmd.getResult() == NOT_ATTEMPTED) { cmd.setResult(REJECTED_OTHER_REASON, "internal server error"); } } logError(String.format("Failed to process refs in %s", project.getName()), err); } if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) { selectNewAndReplacedChangesFromMagicBranch(); } preparePatchSetsForReplace(); insertChangesAndPatchSets(); newProgress.end(); replaceProgress.end(); if (!errors.isEmpty()) { logDebug("Handling error conditions: {}", errors.keySet()); for (Error error : errors.keySet()) { rp.sendMessage(buildError(error, errors.get(error))); } rp.sendMessage(String.format("User: %s", displayName(user))); rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER); } Set<Branch.NameKey> branches = new HashSet<>(); for (ReceiveCommand c : actualCommands) { // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that // should happen in this loop are things that can't happen within one BatchUpdate because they // involve kicking off an additional BatchUpdate. if (c.getResult() != OK) { continue; } if (isHead(c) || isConfig(c)) { switch (c.getType()) { case CREATE: case UPDATE: case UPDATE_NONFASTFORWARD: autoCloseChanges(c); branches.add(new Branch.NameKey(project.getNameKey(), c.getRefName())); break; case DELETE: break; } } } // Update superproject gitlinks if required. if (!branches.isEmpty()) { try (MergeOpRepoManager orm = ormProvider.get()) { orm.setContext(db, TimeUtil.nowTs(), user, receiveId); SubmoduleOp op = subOpFactory.create(branches, orm); op.updateSuperProjects(); } catch (SubmoduleException e) { logError("Can't update the superprojects", e); } } closeProgress.end(); commandProgress.end(); progress.end(); reportMessages(); } private void reportMessages() { List<CreateRequest> created = newChanges.stream().filter(r -> r.change != null).collect(toList()); if (!created.isEmpty()) { addMessage(""); addMessage("New Changes:"); for (CreateRequest c : created) { addMessage( formatChangeUrl( canonicalWebUrl, c.change, c.change.getSubject(), c.change.getStatus() == Change.Status.DRAFT, false)); } addMessage(""); } List<ReplaceRequest> updated = replaceByChange .values() .stream() .filter(r -> !r.skip && r.inputCommand.getResult() == OK) .sorted(comparingInt(r -> r.notes.getChangeId().get())) .collect(toList()); if (!updated.isEmpty()) { addMessage(""); addMessage("Updated Changes:"); boolean edit = magicBranch != null && magicBranch.edit; for (ReplaceRequest u : updated) { String subject; if (edit) { try { subject = rp.getRevWalk().parseCommit(u.newCommitId).getShortMessage(); } catch (IOException e) { // Log and fall back to original change subject logWarn("failed to get subject for edit patch set", e); subject = u.notes.getChange().getSubject(); } } else { subject = u.info.getSubject(); } addMessage( formatChangeUrl( canonicalWebUrl, u.notes.getChange(), subject, u.replaceOp != null && u.replaceOp.getPatchSet().isDraft(), edit)); } addMessage(""); } } private static String formatChangeUrl( String url, Change change, String subject, boolean draft, boolean edit) { StringBuilder m = new StringBuilder() .append(" ") .append(url) .append(change.getChangeId()) .append(" ") .append(ChangeUtil.cropSubject(subject)); if (draft) { m.append(" [DRAFT]"); } if (edit) { m.append(" [EDIT]"); } return m.toString(); } private void insertChangesAndPatchSets() { ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null; if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) { logWarn( String.format( "Skipping change updates on %s because ref update failed: %s %s", project.getName(), magicBranchCmd.getResult(), Strings.nullToEmpty(magicBranchCmd.getMessage()))); return; } try (BatchUpdate bu = batchUpdateFactory.create( db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs()); ObjectInserter ins = repo.newObjectInserter(); ObjectReader reader = ins.newReader(); RevWalk rw = new RevWalk(reader)) { bu.setRepository(repo, rw, ins).updateChangesInParallel(); bu.setRequestId(receiveId); bu.setRefLogMessage("push"); logDebug("Adding {} replace requests", newChanges.size()); for (ReplaceRequest replace : replaceByChange.values()) { replace.addOps(bu, replaceProgress); } logDebug("Adding {} create requests", newChanges.size()); for (CreateRequest create : newChanges) { create.addOps(bu); } logDebug("Adding {} group update requests", newChanges.size()); updateGroups.forEach(r -> r.addOps(bu)); logDebug("Adding {} additional ref updates", actualCommands.size()); actualCommands.forEach(c -> bu.addRepoOnlyOp(new UpdateOneRefOp(c))); logDebug("Executing batch"); try { bu.execute(); } catch (UpdateException e) { throw INSERT_EXCEPTION.apply(e); } if (magicBranchCmd != null) { magicBranchCmd.setResult(OK); } for (ReplaceRequest replace : replaceByChange.values()) { String rejectMessage = replace.getRejectMessage(); if (rejectMessage == null) { if (replace.inputCommand.getResult() == NOT_ATTEMPTED) { // Not necessarily the magic branch, so need to set OK on the original value. replace.inputCommand.setResult(OK); } } else { logDebug("Rejecting due to message from ReplaceOp"); reject(replace.inputCommand, rejectMessage); } } } catch (ResourceConflictException e) { addMessage(e.getMessage()); reject(magicBranchCmd, "conflict"); } catch (RestApiException | IOException err) { logError("Can't insert change/patch set for " + project.getName(), err); reject(magicBranchCmd, "internal server error: " + err.getMessage()); } if (magicBranch != null && magicBranch.submit) { try { submit(newChanges, replaceByChange.values()); } catch (ResourceConflictException e) { addMessage(e.getMessage()); reject(magicBranchCmd, "conflict"); } catch (RestApiException | OrmException e) { logError("Error submitting changes to " + project.getName(), e); reject(magicBranchCmd, "error during submit"); } } } 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(Collection<ReceiveCommand> commands) throws PermissionBackendException { List<String> optionList = rp.getPushOptions(); if (optionList != null) { for (String option : optionList) { int e = option.indexOf('='); if (e > 0) { pushOptions.put(option.substring(0, e), option.substring(e + 1)); } else { pushOptions.put(option, ""); } } } logDebug("Parsing {} commands", commands.size()); for (ReceiveCommand cmd : commands) { if (cmd.getResult() != NOT_ATTEMPTED) { // Already rejected by the core receive process. logDebug("Already processed by core: {} {}", cmd.getResult(), cmd); continue; } if (!Repository.isValidRefName(cmd.getRefName()) || cmd.getRefName().contains("//")) { reject(cmd, "not valid ref"); continue; } if (MagicBranch.isMagicBranch(cmd.getRefName())) { parseMagicBranch(cmd); continue; } if (projectControl.getProjectState().isAllUsers() && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) { String newName = RefNames.refsUsers(user.getAccountId()); logDebug("Swapping out command for {} to {}", RefNames.REFS_USERS_SELF, newName); final ReceiveCommand orgCmd = cmd; cmd = new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), newName, cmd.getType()) { @Override public void setResult(Result s, String m) { super.setResult(s, m); orgCmd.setResult(s, m); } }; } Matcher m = NEW_PATCHSET.matcher(cmd.getRefName()); if (m.matches()) { // The referenced change must exist and must still be open. // 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, "prohibited by Gerrit: unknown command type " + cmd.getType()); continue; } if (cmd.getResult() != NOT_ATTEMPTED) { continue; } if (isConfig(cmd)) { logDebug("Processing {} command", cmd.getRefName()); 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(rp.getRevWalk(), cmd.getNewId()); if (!cfg.getValidationErrors().isEmpty()) { addError("Invalid project configuration:"); for (ValidationError err : cfg.getValidationErrors()) { addError(" " + err.getMessage()); } reject(cmd, "invalid project configuration"); logError( "User " + user.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)) { try { permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); } catch (AuthException e) { 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; } } for (Entry<ProjectConfigEntry> e : pluginConfigEntries) { PluginConfig pluginCfg = cfg.getPluginConfig(e.getPluginName()); ProjectConfigEntry configEntry = e.getProvider().get(); String value = pluginCfg.getString(e.getExportName()); String oldValue = projectControl .getProjectState() .getConfig() .getPluginConfig(e.getPluginName()) .getString(e.getExportName()); if (configEntry.getType() == ProjectConfigEntryType.ARRAY) { oldValue = Arrays.stream( projectControl .getProjectState() .getConfig() .getPluginConfig(e.getPluginName()) .getStringList(e.getExportName())) .collect(joining("\n")); } if ((value == null ? oldValue != null : !value.equals(oldValue)) && !configEntry.isEditable(projectControl.getProjectState())) { reject( cmd, String.format( "invalid project configuration: Not allowed to set parameter" + " '%s' of plugin '%s' on project '%s'.", e.getExportName(), e.getPluginName(), project.getName())); continue; } if (ProjectConfigEntryType.LIST.equals(configEntry.getType()) && value != null && !configEntry.getPermittedValues().contains(value)) { reject( cmd, String.format( "invalid project configuration: The value '%s' is " + "not permitted for parameter '%s' of plugin '%s'.", value, e.getExportName(), e.getPluginName())); } } } catch (Exception e) { reject(cmd, "invalid project configuration"); logError( "User " + user.getUserName() + " tried to push invalid project configuration " + cmd.getNewId().name() + " for " + project.getName(), e); continue; } break; case DELETE: break; default: reject( cmd, "prohibited by Gerrit: don't know how to handle config update of type " + cmd.getType()); continue; } } } } private void parseCreate(ReceiveCommand cmd) { RevObject obj; try { obj = rp.getRevWalk().parseAny(cmd.getNewId()); } catch (IOException err) { logError( "Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " creation", err); reject(cmd, "invalid object"); return; } logDebug("Creating {}", cmd); if (isHead(cmd) && !isCommit(cmd)) { return; } RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (ctl.canCreate(db, rp.getRepository(), obj)) { if (!validRefOperation(cmd)) { return; } validateNewCommits(ctl, cmd); actualCommands.add(cmd); } else { reject(cmd, "prohibited by Gerrit: create access denied for " + cmd.getRefName()); } } private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException { logDebug("Updating {}", cmd); boolean ok; try { permissions.ref(cmd.getRefName()).check(RefPermission.UPDATE); ok = true; } catch (AuthException err) { ok = false; } if (ok) { if (isHead(cmd) && !isCommit(cmd)) { return; } if (!validRefOperation(cmd)) { return; } validateNewCommits(projectControl.controlForRef(cmd.getRefName()), cmd); actualCommands.add(cmd); } else { if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) { errors.put(Error.CONFIG_UPDATE, RefNames.REFS_CONFIG); } else { errors.put(Error.UPDATE, cmd.getRefName()); } reject(cmd, "prohibited by Gerrit: ref update access denied"); } } private boolean isCommit(ReceiveCommand cmd) { RevObject obj; try { obj = rp.getRevWalk().parseAny(cmd.getNewId()); } catch (IOException err) { logError("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName(), err); reject(cmd, "invalid object"); return false; } if (obj instanceof RevCommit) { return true; } reject(cmd, "not a commit"); return false; } private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException { logDebug("Deleting {}", cmd); if (cmd.getRefName().startsWith(REFS_CHANGES)) { errors.put(Error.DELETE_CHANGES, cmd.getRefName()); reject(cmd, "cannot delete changes"); } else if (canDelete(cmd)) { if (!validRefOperation(cmd)) { return; } actualCommands.add(cmd); } else if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) { reject(cmd, "cannot delete project configuration"); } else { errors.put(Error.DELETE, cmd.getRefName()); reject(cmd, "cannot delete references"); } } private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException { try { permissions.ref(cmd.getRefName()).check(RefPermission.DELETE); return true; } catch (AuthException e) { return false; } } private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException { RevCommit newObject; try { newObject = rp.getRevWalk().parseCommit(cmd.getNewId()); } catch (IncorrectObjectTypeException notCommit) { newObject = null; } catch (IOException err) { logError( "Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " forced update", err); reject(cmd, "invalid object"); return; } logDebug("Rewinding {}", cmd); RefControl ctl = projectControl.controlForRef(cmd.getRefName()); if (newObject != null) { validateNewCommits(ctl, cmd); if (cmd.getResult() != NOT_ATTEMPTED) { return; } } boolean ok; try { permissions.ref(cmd.getRefName()).check(RefPermission.FORCE_UPDATE); ok = true; } catch (AuthException err) { ok = false; } if (ok) { if (!validRefOperation(cmd)) { return; } actualCommands.add(cmd); } else { cmd.setResult( REJECTED_NONFASTFORWARD, " need '" + PermissionRule.FORCE_PUSH + "' privilege."); } } static class MagicBranchInput { private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings(); final ReceiveCommand cmd; final LabelTypes labelTypes; final NotesMigration notesMigration; Branch.NameKey dest; RefControl ctl; Set<Account.Id> reviewer = Sets.newLinkedHashSet(); Set<Account.Id> cc = Sets.newLinkedHashSet(); Map<String, Short> labels = new HashMap<>(); String message; List<RevCommit> baseCommit; CmdLineParser clp; Set<String> hashtags = new HashSet<>(); @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes") List<ObjectId> base; @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 = "--private", usage = "mark new/updated change as private") boolean isPrivate; @Option(name = "--remove-private", usage = "remove privacy flag from updated change") boolean removePrivate; @Option( name = "--wip", aliases = {"-work-in-progress"}, usage = "mark change as work in progress" ) boolean workInProgress; @Option(name = "--ready", usage = "mark change as ready") boolean ready; @Option( name = "--edit", aliases = {"-e"}, usage = "upload as change edit" ) boolean edit; @Option(name = "--submit", usage = "immediately submit the change") boolean submit; @Option(name = "--merged", usage = "create single change for a merged commit") boolean merged; @Option( name = "--notify", usage = "Notify handling that defines to whom email notifications " + "should be sent. Allowed values are NONE, OWNER, " + "OWNER_REVIEWERS, ALL. If not set, the default is ALL." ) NotifyHandling notify = NotifyHandling.ALL; @Option(name = "--notify-to", metaVar = "USER", usage = "user that should be notified") List<Account.Id> tos = new ArrayList<>(); @Option(name = "--notify-cc", metaVar = "USER", usage = "user that should be CC'd") List<Account.Id> ccs = new ArrayList<>(); @Option(name = "--notify-bcc", metaVar = "USER", usage = "user that should be BCC'd") List<Account.Id> bccs = new ArrayList<>(); @Option( name = "--reviewer", aliases = {"-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; } @Option( name = "--label", aliases = {"-l"}, metaVar = "LABEL+VALUE", usage = "label(s) to assign (defaults to +1 if no value provided" ) void addLabel(String token) throws CmdLineException { LabelVote v = LabelVote.parse(token); try { LabelType.checkName(v.label()); ApprovalsUtil.checkLabel(labelTypes, v.label(), v.value()); } catch (BadRequestException e) { throw clp.reject(e.getMessage()); } labels.put(v.label(), v.value()); } @Option( name = "--message", aliases = {"-m"}, metaVar = "MESSAGE", usage = "Comment message to apply to the review" ) void addMessage(final String token) { // git push does not allow spaces in refs. message = token.replace("_", " "); } @Option( name = "--hashtag", aliases = {"-t"}, metaVar = "HASHTAG", usage = "add hashtag to changes" ) void addHashtag(String token) throws CmdLineException { if (!notesMigration.readChanges()) { throw clp.reject("cannot add hashtags; noteDb is disabled"); } String hashtag = cleanupHashtag(token); if (!hashtag.isEmpty()) { hashtags.add(hashtag); } //TODO(dpursehouse): validate hashtags } MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes, NotesMigration notesMigration) { this.cmd = cmd; this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE); this.labelTypes = labelTypes; this.notesMigration = notesMigration; } MailRecipients getMailRecipients() { return new MailRecipients(reviewer, cc); } ListMultimap<RecipientType, Account.Id> getAccountsToNotify() { ListMultimap<RecipientType, Account.Id> accountsToNotify = MultimapBuilder.hashKeys().arrayListValues().build(); accountsToNotify.putAll(RecipientType.TO, tos); accountsToNotify.putAll(RecipientType.CC, ccs); accountsToNotify.putAll(RecipientType.BCC, bccs); return accountsToNotify; } String parse( CmdLineParser clp, Repository repo, Set<String> refs, ListMultimap<String, String> pushOptions) throws CmdLineException { String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName())); ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions); int optionStart = ref.indexOf('%'); if (0 < optionStart) { 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, ""); } } ref = ref.substring(0, optionStart); } if (!options.isEmpty()) { clp.parseOptionMap(options); } // 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); } } /** * Gets an unmodifiable view of the pushOptions. * * <p>The collection is empty if the client does not support push options, or if the client did * not send any options. * * @return an unmodifiable view of pushOptions. */ @Nullable public ListMultimap<String, String> getPushOptions() { return ImmutableListMultimap.copyOf(pushOptions); } private void parseMagicBranch(ReceiveCommand cmd) { // Permit exactly one new change request per push. if (magicBranch != null) { reject(cmd, "duplicate request"); return; } logDebug("Found magic branch {}", cmd.getRefName()); magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration); magicBranch.reviewer.addAll(reviewersFromCommandLine); magicBranch.cc.addAll(ccFromCommandLine); String ref; CmdLineParser clp = optionParserFactory.create(magicBranch); magicBranch.clp = clp; try { ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet(), pushOptions); } catch (CmdLineException e) { if (!clp.wasHelpRequestedByOption()) { logDebug("Invalid branch syntax"); 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 (projectControl.getProjectState().isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) { logDebug("Handling {}", RefNames.REFS_USERS_SELF); ref = RefNames.refsUsers(user.getAccountId()); } if (!rp.getAdvertisedRefs().containsKey(ref) && !ref.equals(readHEAD(repo))) { logDebug("Ref {} not found", ref); 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 (projectControl.getProject().getState() != com.google.gerrit.extensions.client.ProjectState.ACTIVE) { reject(cmd, "project is read only"); return; } if (magicBranch.draft) { if (!receiveConfig.allowDrafts) { errors.put(Error.CODE_REVIEW, ref); reject(cmd, "draft workflow is disabled"); return; } else if (projectControl .controlForRef(MagicBranch.NEW_DRAFT_CHANGE + ref) .isBlocked(Permission.PUSH)) { errors.put(Error.CODE_REVIEW, ref); reject(cmd, "cannot upload drafts"); return; } } if (!magicBranch.ctl.canUpload()) { errors.put(Error.CODE_REVIEW, ref); reject(cmd, "cannot upload review"); return; } if (magicBranch.isPrivate && magicBranch.removePrivate) { reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive"); return; } if (magicBranch.workInProgress && magicBranch.ready) { reject(cmd, "the options 'wip' and 'ready' are mutually exclusive"); return; } if (magicBranch.draft && magicBranch.submit) { reject(cmd, "cannot submit draft"); return; } if (magicBranch.submit && !projectControl.controlForRef(MagicBranch.NEW_CHANGE + ref).canSubmit(true)) { reject(cmd, "submit not allowed"); return; } RevWalk walk = rp.getRevWalk(); RevCommit tip; try { tip = walk.parseCommit(magicBranch.cmd.getNewId()); logDebug("Tip of push: {}", tip.name()); } catch (IOException ex) { magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT); logError("Invalid pack upload; one or more objects weren't sent", ex); return; } String destBranch = magicBranch.dest.get(); try { if (magicBranch.merged) { if (magicBranch.draft) { reject(cmd, "cannot be draft & merged"); return; } if (magicBranch.base != null) { reject(cmd, "cannot use merged with base"); return; } RevCommit branchTip = readBranchTip(cmd, magicBranch.dest); if (branchTip == null) { return; // readBranchTip already rejected cmd. } if (!walk.isMergedInto(tip, branchTip)) { reject(cmd, "not merged into branch"); return; } } // If tip is a merge commit, or the root commit or // if %base or %merged was specified, ignore newChangeForAllNotInTarget. if (tip.getParentCount() > 1 || magicBranch.base != null || magicBranch.merged || tip.getParentCount() == 0) { logDebug("Forcing newChangeForAllNotInTarget = false"); newChangeForAllNotInTarget = false; } if (magicBranch.base != null) { logDebug("Handling %base: {}", magicBranch.base); magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size()); for (ObjectId id : magicBranch.base) { try { magicBranch.baseCommit.add(walk.parseCommit(id)); } catch (IncorrectObjectTypeException notCommit) { reject(cmd, "base must be a commit"); return; } catch (MissingObjectException e) { reject(cmd, "base not found"); return; } catch (IOException e) { logWarn(String.format("Project %s cannot read %s", project.getName(), id.name()), e); reject(cmd, "internal server error"); return; } } } else if (newChangeForAllNotInTarget) { RevCommit branchTip = readBranchTip(cmd, magicBranch.dest); if (branchTip == null) { return; // readBranchTip already rejected cmd. } magicBranch.baseCommit = Collections.singletonList(branchTip); logDebug("Set baseCommit = {}", magicBranch.baseCommit.get(0).name()); } } catch (IOException ex) { logWarn( String.format("Error walking to %s in project %s", destBranch, project.getName()), ex); reject(cmd, "internal server error"); 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 { 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. logDebug("Branch is unborn"); return; } RevCommit h = walk.parseCommit(targetRef.getObjectId()); logDebug("Current branch tip: {}", h.name()); 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"); } } finally { walk.reset(); walk.setRevFilter(oldRevFilter); } } catch (IOException e) { magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT); logError("Invalid pack upload; one or more objects weren't sent", e); } } private static String readHEAD(Repository repo) { try { return repo.getFullBranch(); } catch (IOException e) { log.error("Cannot read HEAD symref", e); return null; } } private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch) throws IOException { Ref r = allRefs.get(branch.get()); if (r == null) { reject(cmd, branch.get() + " not found"); return null; } return rp.getRevWalk().parseCommit(r.getObjectId()); } private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) { logDebug("Parsing replace command"); if (cmd.getType() != ReceiveCommand.Type.CREATE) { reject(cmd, "invalid usage"); return; } RevCommit newCommit; try { newCommit = rp.getRevWalk().parseCommit(cmd.getNewId()); logDebug("Replacing with {}", newCommit); } catch (IOException e) { logError("Cannot parse " + cmd.getNewId().name() + " as commit", e); reject(cmd, "invalid commit"); return; } Change changeEnt; try { changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId).getChange(); } catch (NoSuchChangeException e) { logError("Change not found " + changeId, e); reject(cmd, "change " + changeId + " not found"); return; } catch (OrmException e) { logError("Cannot lookup existing change " + changeId, e); reject(cmd, "database error"); return; } if (!project.getNameKey().equals(changeEnt.getProject())) { reject(cmd, "change " + changeId + " does not belong to project " + project.getName()); return; } logDebug("Replacing change {}", changeEnt.getId()); requestReplace(cmd, true, changeEnt, newCommit); } private boolean requestReplace( ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) { if (change.getStatus().isClosed()) { reject(cmd, "change " + canonicalWebUrl + change.getId() + " closed"); return false; } ReplaceRequest req = new ReplaceRequest(change.getId(), newCommit, cmd, checkMergedInto); if (replaceByChange.containsKey(req.ontoChange)) { reject(cmd, "duplicate request"); return false; } replaceByChange.put(req.ontoChange, req); return true; } private void selectNewAndReplacedChangesFromMagicBranch() { logDebug("Finding new and replaced changes"); newChanges = new ArrayList<>(); ListMultimap<ObjectId, Ref> existing = changeRefsById(); GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil, notesFactory, project.getNameKey()); try { RevCommit start = setUpWalkForSelectingChanges(); if (start == null) { return; } LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>(); Set<Change.Key> newChangeIds = new HashSet<>(); int maxBatchChanges = receiveConfig.getEffectiveMaxBatchChangesLimit(user); int total = 0; int alreadyTracked = 0; boolean rejectImplicitMerges = start.getParentCount() == 1 && projectCache.get(project.getNameKey()).isRejectImplicitMerges() // Don't worry about implicit merges when creating changes for // already-merged commits; they're already in history, so it's too // late. && !magicBranch.merged; Set<RevCommit> mergedParents; if (rejectImplicitMerges) { mergedParents = new HashSet<>(); } else { mergedParents = null; } for (; ; ) { RevCommit c = rp.getRevWalk().next(); if (c == null) { break; } total++; rp.getRevWalk().parseBody(c); String name = c.name(); groupCollector.visit(c); Collection<Ref> existingRefs = existing.get(c); if (rejectImplicitMerges) { Collections.addAll(mergedParents, c.getParents()); mergedParents.remove(c); } boolean commitAlreadyTracked = !existingRefs.isEmpty(); if (commitAlreadyTracked) { alreadyTracked++; // Corner cases where an existing commit might need a new group: // A) Existing commit has a null group; wasn't assigned during schema // upgrade, or schema upgrade is performed on a running server. // B) Let A<-B<-C, then: // 1. Push A to refs/heads/master // 2. Push B to refs/for/master // 3. Force push A~ to refs/heads/master // 4. Push C to refs/for/master. // B will be in existing so we aren't replacing the patch set. It // used to have its own group, but now needs to to be changed to // A's group. // C) Commit is a PatchSet of a pre-existing change uploaded with a // different target branch. for (Ref ref : existingRefs) { updateGroups.add(new UpdateGroupsRequest(ref, c)); } if (!(newChangeForAllNotInTarget || magicBranch.base != null)) { continue; } } List<String> idList = c.getFooterLines(CHANGE_ID); String idStr = !idList.isEmpty() ? idList.get(idList.size() - 1).trim() : null; if (idStr != null) { pending.put(c, new ChangeLookup(c, new Change.Key(idStr))); } else { pending.put(c, new ChangeLookup(c)); } int n = pending.size() + newChanges.size(); if (maxBatchChanges != 0 && n > maxBatchChanges) { logDebug("{} changes exceeds limit of {}", n, maxBatchChanges); reject( magicBranch.cmd, "the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges); newChanges = Collections.emptyList(); return; } if (commitAlreadyTracked) { boolean changeExistsOnDestBranch = false; for (ChangeData cd : pending.get(c).destChanges) { if (cd.change().getDest().equals(magicBranch.dest)) { changeExistsOnDestBranch = true; break; } } if (changeExistsOnDestBranch) { continue; } logDebug("Creating new change for {} even though it is already tracked", name); } if (!validCommit(rp.getRevWalk(), magicBranch.ctl, magicBranch.cmd, c)) { // Not a change the user can propose? Abort as early as possible. newChanges = Collections.emptyList(); logDebug("Aborting early due to invalid commit"); return; } // Don't allow merges to be uploaded in commit chain via all-not-in-target if (newChangeForAllNotInTarget && c.getParentCount() > 1) { reject( magicBranch.cmd, "Pushing merges in commit chains with 'all not in target' is not allowed,\n" + "to override please set the base manually"); logDebug("Rejecting merge commit {} with newChangeForAllNotInTarget", name); // TODO(dborowitz): Should we early return here? } if (idList.isEmpty()) { newChanges.add(new CreateRequest(c, magicBranch.dest.get())); continue; } } logDebug( "Finished initial RevWalk with {} commits total: {} already" + " tracked, {} new changes with no Change-Id, and {} deferred" + " lookups", total, alreadyTracked, newChanges.size(), pending.size()); if (rejectImplicitMerges) { rejectImplicitMerges(mergedParents); } for (Iterator<ChangeLookup> itr = pending.values().iterator(); itr.hasNext(); ) { ChangeLookup p = itr.next(); if (p.changeKey == null) { continue; } if (newChangeIds.contains(p.changeKey)) { logDebug("Multiple commits with Change-Id {}", p.changeKey); reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES); newChanges = Collections.emptyList(); return; } List<ChangeData> changes = p.destChanges; if (changes.size() > 1) { logDebug( "Multiple changes in branch {} with Change-Id {}: {}", magicBranch.dest, p.changeKey, changes.stream().map(cd -> cd.getId().toString()).collect(joining())); // WTF, multiple changes in this branch 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 per branch. // reject(magicBranch.cmd, p.changeKey.get() + " has duplicates"); newChanges = Collections.emptyList(); return; } if (changes.size() == 1) { // Schedule as a replacement to this one matching change. // RevId currentPs = changes.get(0).currentPatchSet().getRevision(); // If Commit is already current PatchSet of target Change. if (p.commit.name().equals(currentPs.get())) { if (pending.size() == 1) { // There are no commits left to check, all commits in pending were already // current PatchSet of the corresponding target changes. reject(magicBranch.cmd, "commit(s) already exists (as current patchset)"); } else { // Commit is already current PatchSet. // Remove from pending and try next commit. itr.remove(); continue; } } if (requestReplace(magicBranch.cmd, false, changes.get(0).change(), p.commit)) { continue; } newChanges = Collections.emptyList(); return; } if (changes.size() == 0) { if (!isValidChangeId(p.changeKey.get())) { reject(magicBranch.cmd, "invalid Change-Id"); newChanges = Collections.emptyList(); return; } // In case the change look up from the index failed, // double check against the existing refs if (foundInExistingRef(existing.get(p.commit))) { if (pending.size() == 1) { reject(magicBranch.cmd, "commit(s) already exists (as current patchset)"); newChanges = Collections.emptyList(); return; } itr.remove(); continue; } newChangeIds.add(p.changeKey); } newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get())); } logDebug( "Finished deferred lookups with {} updates and {} new changes", replaceByChange.size(), newChanges.size()); } 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); logError("Invalid pack upload; one or more objects weren't sent", e); newChanges = Collections.emptyList(); return; } catch (OrmException e) { logError("Cannot query database to locate prior changes", e); reject(magicBranch.cmd, "database error"); newChanges = Collections.emptyList(); return; } if (newChanges.isEmpty() && replaceByChange.isEmpty()) { reject(magicBranch.cmd, "no new changes"); return; } if (!newChanges.isEmpty() && magicBranch.edit) { reject(magicBranch.cmd, "edit is not supported for new changes"); return; } try { SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups(); List<Integer> newIds = seq.nextChangeIds(newChanges.size()); for (int i = 0; i < newChanges.size(); i++) { CreateRequest create = newChanges.get(i); create.setChangeId(newIds.get(i)); create.groups = ImmutableList.copyOf(groups.get(create.commit)); } for (ReplaceRequest replace : replaceByChange.values()) { replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId)); } for (UpdateGroupsRequest update : updateGroups) { update.groups = ImmutableList.copyOf((groups.get(update.commit))); } logDebug("Finished updating groups from GroupCollector"); } catch (OrmException e) { logError("Error collecting groups for changes", e); reject(magicBranch.cmd, "internal server error"); return; } } private boolean foundInExistingRef(Collection<Ref> existingRefs) throws OrmException { for (Ref ref : existingRefs) { ChangeNotes notes = notesFactory.create(db, project.getNameKey(), Change.Id.fromRef(ref.getName())); Change change = notes.getChange(); if (change.getDest().equals(magicBranch.dest)) { logDebug("Found change {} from existing refs.", change.getKey()); // Reindex the change asynchronously, ignoring errors. @SuppressWarnings("unused") Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId()); return true; } } return false; } private RevCommit setUpWalkForSelectingChanges() throws IOException { RevWalk rw = rp.getRevWalk(); RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId()); rw.reset(); rw.sort(RevSort.TOPO); rw.sort(RevSort.REVERSE, true); rp.getRevWalk().markStart(start); if (magicBranch.baseCommit != null) { markExplicitBasesUninteresting(); } else if (magicBranch.merged) { logDebug("Marking parents of merged commit {} uninteresting", start.name()); for (RevCommit c : start.getParents()) { rw.markUninteresting(c); } } else { markHeadsAsUninteresting(rw, magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null); } return start; } private void markExplicitBasesUninteresting() throws IOException { logDebug("Marking {} base commits uninteresting", magicBranch.baseCommit.size()); for (RevCommit c : magicBranch.baseCommit) { rp.getRevWalk().markUninteresting(c); } Ref targetRef = allRefs.get(magicBranch.ctl.getRefName()); if (targetRef != null) { logDebug( "Marking target ref {} ({}) uninteresting", magicBranch.ctl.getRefName(), targetRef.getObjectId().name()); rp.getRevWalk().markUninteresting(rp.getRevWalk().parseCommit(targetRef.getObjectId())); } } private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException { if (!mergedParents.isEmpty()) { Ref targetRef = allRefs.get(magicBranch.ctl.getRefName()); if (targetRef != null) { RevWalk rw = rp.getRevWalk(); RevCommit tip = rw.parseCommit(targetRef.getObjectId()); boolean containsImplicitMerges = true; for (RevCommit p : mergedParents) { containsImplicitMerges &= !rw.isMergedInto(p, tip); } if (containsImplicitMerges) { rw.reset(); for (RevCommit p : mergedParents) { rw.markStart(p); } rw.markUninteresting(tip); RevCommit c; while ((c = rw.next()) != null) { rw.parseBody(c); messages.add( new CommitValidationMessage( "ERROR: Implicit Merge of " + c.abbreviate(7).name() + " " + c.getShortMessage(), false)); } reject(magicBranch.cmd, "implicit merges detected"); } } } } private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) { int i = 0; for (Ref ref : allRefs.values()) { if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef)) && ref.getObjectId() != null) { try { rw.markUninteresting(rw.parseCommit(ref.getObjectId())); i++; } catch (IOException e) { logWarn(String.format("Invalid ref %s in %s", ref.getName(), project.getName()), e); } } } logDebug("Marked {} heads as uninteresting", i); } 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 List<ChangeData> destChanges; ChangeLookup(RevCommit c, Change.Key key) throws OrmException { commit = c; changeKey = key; destChanges = queryProvider.get().byBranchKey(magicBranch.dest, key); } ChangeLookup(RevCommit c) throws OrmException { commit = c; destChanges = queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()); changeKey = null; } } private class CreateRequest { final RevCommit commit; private final String refName; Change.Id changeId; ReceiveCommand cmd; ChangeInserter ins; List<String> groups = ImmutableList.of(); Change change; CreateRequest(RevCommit commit, String refName) { this.commit = commit; this.refName = refName; } private void setChangeId(int id) { changeId = new Change.Id(id); ins = changeInserterFactory .create(changeId, commit, refName) .setTopic(magicBranch.topic) .setPrivate(magicBranch.isPrivate) .setWorkInProgress(magicBranch.workInProgress) // Changes already validated in validateNewCommits. .setValidate(false); if (magicBranch.draft) { ins.setDraft(magicBranch.draft); } else if (magicBranch.merged) { ins.setStatus(Change.Status.MERGED); } cmd = new ReceiveCommand(ObjectId.zeroId(), commit, ins.getPatchSetId().toRefName()); if (rp.getPushCertificate() != null) { ins.setPushCertificate(rp.getPushCertificate().toTextWithSignature()); } } private void addOps(BatchUpdate bu) throws RestApiException { checkState(changeId != null, "must call setChangeId before addOps"); try { RevWalk rw = rp.getRevWalk(); rw.parseBody(commit); final PatchSet.Id psId = ins.setGroups(groups).getPatchSetId(); Account.Id me = user.getAccountId(); List<FooterLine> footerLines = commit.getFooterLines(); MailRecipients recipients = new MailRecipients(); Map<String, Short> approvals = new HashMap<>(); checkNotNull(magicBranch); recipients.add(magicBranch.getMailRecipients()); approvals = magicBranch.labels; recipients.add( getRecipientsFromFooters(db, accountResolver, magicBranch.draft, footerLines)); recipients.remove(me); StringBuilder msg = new StringBuilder( ApprovalsUtil.renderMessageWithApprovals( psId.get(), approvals, Collections.<String, PatchSetApproval>emptyMap())); msg.append('.'); if (!Strings.isNullOrEmpty(magicBranch.message)) { msg.append("\n").append(magicBranch.message); } bu.insertChange( ins.setReviewers(recipients.getReviewers()) .setExtraCC(recipients.getCcOnly()) .setApprovals(approvals) .setMessage(msg.toString()) .setNotify(magicBranch.notify) .setAccountsToNotify(magicBranch.getAccountsToNotify()) .setRequestScopePropagator(requestScopePropagator) .setSendMail(true) .setPatchSetDescription(magicBranch.message)); if (!magicBranch.hashtags.isEmpty()) { // Any change owner is allowed to add hashtags when creating a change. bu.addOp( changeId, hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags)).setFireEvent(false)); } if (!Strings.isNullOrEmpty(magicBranch.topic)) { bu.addOp( changeId, new BatchUpdateOp() { @Override public boolean updateChange(ChangeContext ctx) { ctx.getUpdate(psId).setTopic(magicBranch.topic); return true; } }); } bu.addOp( changeId, new BatchUpdateOp() { @Override public boolean updateChange(ChangeContext ctx) { change = ctx.getChange(); return false; } }); bu.addOp(changeId, new ChangeProgressOp(newProgress)); } catch (Exception e) { throw INSERT_EXCEPTION.apply(e); } } } private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace) throws OrmException, RestApiException { Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size()); for (CreateRequest r : create) { checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId); bySha.put(r.commit, r.change); } for (ReplaceRequest r : replace) { bySha.put(r.newCommitId, r.notes.getChange()); } Change tipChange = bySha.get(magicBranch.cmd.getNewId()); checkNotNull( tipChange, "tip of push does not correspond to a change; found these changes: %s", bySha); logDebug( "Processing submit with tip change {} ({})", tipChange.getId(), magicBranch.cmd.getNewId()); try (MergeOp op = mergeOpProvider.get()) { op.merge(db, tipChange, user, false, new SubmitInput(), false); } } private void preparePatchSetsForReplace() { try { readChangesForReplace(); for (Iterator<ReplaceRequest> itr = replaceByChange.values().iterator(); itr.hasNext(); ) { ReplaceRequest req = itr.next(); if (req.inputCommand.getResult() == NOT_ATTEMPTED) { req.validate(false); if (req.skip && req.cmd == null) { itr.remove(); } } } } catch (OrmException err) { logError( String.format( "Cannot read database before replacement for project %s", project.getName()), err); for (ReplaceRequest req : replaceByChange.values()) { if (req.inputCommand.getResult() == NOT_ATTEMPTED) { req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error"); } } } catch (IOException | PermissionBackendException err) { logError( String.format( "Cannot read repository before replacement for project %s", project.getName()), err); for (ReplaceRequest req : replaceByChange.values()) { if (req.inputCommand.getResult() == NOT_ATTEMPTED) { req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error"); } } } logDebug("Read {} changes to replace", replaceByChange.size()); 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 { Collection<ChangeNotes> allNotes = notesFactory.create( db, replaceByChange.values().stream().map(r -> r.ontoChange).collect(toList())); for (ChangeNotes notes : allNotes) { replaceByChange.get(notes.getChangeId()).notes = notes; } } private class ReplaceRequest { final Change.Id ontoChange; final ObjectId newCommitId; final ReceiveCommand inputCommand; final boolean checkMergedInto; ChangeNotes notes; BiMap<RevCommit, PatchSet.Id> revisions; PatchSet.Id psId; ReceiveCommand prev; ReceiveCommand cmd; PatchSetInfo info; boolean skip; private PatchSet.Id priorPatchSet; List<String> groups = ImmutableList.of(); private ReplaceOp replaceOp; ReplaceRequest( Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) { this.ontoChange = toChange; this.newCommitId = newCommit.copy(); this.inputCommand = checkNotNull(cmd); this.checkMergedInto = checkMergedInto; revisions = HashBiMap.create(); for (Ref ref : refs(toChange)) { try { revisions.forcePut( rp.getRevWalk().parseCommit(ref.getObjectId()), PatchSet.Id.fromRef(ref.getName())); } catch (IOException err) { logWarn( String.format( "Project %s contains invalid change ref %s", project.getName(), ref.getName()), err); } } } /** * Validate the new patch set commit for this change. * * <p><strong>Side effects:</strong> * * <ul> * <li>May add error or warning messages to the progress monitor * <li>Will reject {@code cmd} prior to returning false * <li>May reset {@code rp.getRevWalk()}; do not call in the middle of a walk. * </ul> * * @param autoClose whether the caller intends to auto-close the change after adding a new patch * set. * @return whether the new commit is valid * @throws IOException * @throws OrmException * @throws PermissionBackendException */ boolean validate(boolean autoClose) throws IOException, OrmException, PermissionBackendException { if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) { return false; } else if (notes == null) { reject(inputCommand, "change " + ontoChange + " not found"); return false; } Change change = notes.getChange(); priorPatchSet = change.currentPatchSetId(); if (!revisions.containsValue(priorPatchSet)) { reject(inputCommand, "change " + ontoChange + " missing revisions"); return false; } RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId); RevCommit priorCommit = revisions.inverse().get(priorPatchSet); try { permissions.change(notes).database(db).check(ChangePermission.ADD_PATCH_SET); } catch (AuthException no) { String locked = "."; if (projectControl.controlFor(notes).isPatchSetLocked(db)) { locked = ". Change is patch set locked."; } reject(inputCommand, "cannot add patch set to " + ontoChange + locked); return false; } if (change.getStatus().isClosed()) { reject(inputCommand, "change " + ontoChange + " closed"); return false; } else if (revisions.containsKey(newCommit)) { reject(inputCommand, "commit already exists (in the change)"); return false; } for (Ref r : rp.getRepository().getRefDatabase().getRefs("refs/changes").values()) { if (r.getObjectId().equals(newCommit)) { reject(inputCommand, "commit already exists (in the project)"); return false; } } for (RevCommit prior : revisions.keySet()) { // 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, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES); return false; } } ChangeControl changeCtl = projectControl.controlFor(notes); if (!validCommit(rp.getRevWalk(), changeCtl.getRefControl(), inputCommand, newCommit)) { return false; } rp.getRevWalk().parseBody(priorCommit); // 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 (newCommit.getTree().equals(priorCommit.getTree())) { boolean messageEq = eq(newCommit.getFullMessage(), priorCommit.getFullMessage()); boolean parentsEq = parentsEqual(newCommit, priorCommit); boolean authorEq = authorEqual(newCommit, priorCommit); ObjectReader reader = rp.getRevWalk().getObjectReader(); if (messageEq && parentsEq && authorEq && !autoClose) { addMessage( String.format( "(W) No changes between prior commit %s and new commit %s", reader.abbreviate(priorCommit).name(), reader.abbreviate(newCommit).name())); } else { StringBuilder msg = new StringBuilder(); msg.append("(I) "); 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()); } } if (magicBranch != null && magicBranch.edit) { return newEdit(); } newPatchSet(); return true; } private boolean newEdit() { psId = notes.getChange().currentPatchSetId(); Optional<ChangeEdit> edit = null; try { edit = editUtil.byChange(projectControl.controlFor(notes)); } catch (AuthException | IOException e) { logError("Cannot retrieve edit", e); return false; } if (edit.isPresent()) { if (edit.get().getBasePatchSet().getId().equals(psId)) { // replace edit cmd = new ReceiveCommand(edit.get().getEditCommit(), newCommitId, edit.get().getRefName()); } else { // delete old edit ref on rebase prev = new ReceiveCommand( edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName()); createEditCommand(); } } else { createEditCommand(); } return true; } private void createEditCommand() { // create new edit cmd = new ReceiveCommand( ObjectId.zeroId(), newCommitId, RefNames.refsEdit(user.getAccountId(), notes.getChangeId(), psId)); } private void newPatchSet() throws IOException { RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId); psId = ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs, notes.getChange().currentPatchSetId()); info = patchSetInfoFactory.get(rp.getRevWalk(), newCommit, psId); cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName()); } void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException { if (magicBranch != null && magicBranch.edit) { bu.addOp(notes.getChangeId(), new ReindexOnlyOp()); if (prev != null) { bu.addRepoOnlyOp(new UpdateOneRefOp(prev)); } bu.addRepoOnlyOp(new UpdateOneRefOp(cmd)); return; } RevWalk rw = rp.getRevWalk(); // TODO(dborowitz): Move to ReplaceOp#updateRepo. RevCommit newCommit = rw.parseCommit(newCommitId); rw.parseBody(newCommit); RevCommit priorCommit = revisions.inverse().get(priorPatchSet); replaceOp = replaceOpFactory .create( projectControl, notes.getChange().getDest(), checkMergedInto, priorPatchSet, priorCommit, psId, newCommit, info, groups, magicBranch, rp.getPushCertificate()) .setRequestScopePropagator(requestScopePropagator); bu.addOp(notes.getChangeId(), replaceOp); if (progress != null) { bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress)); } } String getRejectMessage() { return replaceOp != null ? replaceOp.getRejectMessage() : null; } } private class UpdateGroupsRequest { private final PatchSet.Id psId; private final RevCommit commit; List<String> groups = ImmutableList.of(); UpdateGroupsRequest(Ref ref, RevCommit commit) { this.psId = checkNotNull(PatchSet.Id.fromRef(ref.getName())); this.commit = commit; } private void addOps(BatchUpdate bu) { bu.addOp( psId.getParentKey(), new BatchUpdateOp() { @Override public boolean updateChange(ChangeContext ctx) throws OrmException { PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); List<String> oldGroups = ps.getGroups(); if (oldGroups == null) { if (groups == null) { return false; } } else if (sameGroups(oldGroups, groups)) { return false; } psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, groups); return true; } }); } private boolean sameGroups(List<String> a, List<String> b) { return Sets.newHashSet(a).equals(Sets.newHashSet(b)); } } private class UpdateOneRefOp implements RepoOnlyOp { private final ReceiveCommand cmd; private UpdateOneRefOp(ReceiveCommand cmd) { this.cmd = checkNotNull(cmd); } @Override public void updateRepo(RepoContext ctx) throws IOException { ctx.addRefUpdate(cmd); } @Override public void postUpdate(Context ctx) { String refName = cmd.getRefName(); if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward logDebug("Updating tag cache on fast-forward of {}", cmd.getRefName()); tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId()); } if (isConfig(cmd)) { logDebug("Reloading project in cache"); projectCache.evict(project); ProjectState ps = projectCache.get(project.getNameKey()); try { logDebug("Updating project description"); repo.setGitwebDescription(ps.getProject().getDescription()); } catch (IOException e) { log.warn("cannot update description of " + project.getName(), e); } } } } private static class ReindexOnlyOp implements BatchUpdateOp { @Override public boolean updateChange(ChangeContext ctx) { // Trigger reindexing even though change isn't actually updated. return true; } } private List<Ref> refs(Change.Id changeId) { return refsByChange().get(changeId); } private void initChangeRefMaps() { if (refsByChange == null) { int estRefsPerChange = 4; refsById = MultimapBuilder.hashKeys().arrayListValues().build(); refsByChange = MultimapBuilder.hashKeys(allRefs.size() / estRefsPerChange) .arrayListValues(estRefsPerChange) .build(); for (Ref ref : allRefs.values()) { ObjectId obj = ref.getObjectId(); if (obj != null) { PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); if (psId != null) { refsById.put(obj, ref); refsByChange.put(psId.getParentKey(), ref); } } } } } private ListMultimap<Change.Id, Ref> refsByChange() { initChangeRefMaps(); return refsByChange; } private ListMultimap<ObjectId, Ref> changeRefsById() { initChangeRefMaps(); return refsById; } 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).equals(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 boolean validRefOperation(ReceiveCommand cmd) { RefOperationValidators refValidators = refValidatorsFactory.create(getProject(), user, cmd); try { messages.addAll(refValidators.validateForRefOperation()); } catch (RefOperationValidationException e) { messages.addAll(Lists.newArrayList(e.getMessages())); reject(cmd, e.getMessage()); return false; } return true; } private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) { if (ctl.canForgeAuthor() && ctl.canForgeCommitter() && ctl.canForgeGerritServerIdentity() && ctl.canUploadMerges() && !projectControl.getProjectState().isUseSignedOffBy() && Iterables.isEmpty(rejectCommits) && !RefNames.REFS_CONFIG.equals(ctl.getRefName()) && !(MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) { logDebug("Short-circuiting new commit validation"); return; } boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName()); RevWalk walk = rp.getRevWalk(); walk.reset(); walk.sort(RevSort.NONE); try { RevObject parsedObject = walk.parseAny(cmd.getNewId()); if (!(parsedObject instanceof RevCommit)) { return; } ListMultimap<ObjectId, Ref> existing = changeRefsById(); walk.markStart((RevCommit) parsedObject); markHeadsAsUninteresting(walk, cmd.getRefName()); int i = 0; for (RevCommit c; (c = walk.next()) != null; ) { i++; if (existing.keySet().contains(c)) { continue; } else if (!validCommit(walk, ctl, cmd, c)) { break; } if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) { try { Account a = db.accounts().get(user.getAccountId()); if (a != null && Strings.isNullOrEmpty(a.getFullName())) { a.setFullName(c.getCommitterIdent().getName()); db.accounts().update(Collections.singleton(a)); user.getAccount().setFullName(a.getFullName()); accountCache.evict(a.getId()); } } catch (OrmException e) { logWarn("Cannot default full_name", e); } finally { defaultName = false; } } } logDebug("Validated {} new commits", i); } catch (IOException err) { cmd.setResult(REJECTED_MISSING_OBJECT); logError("Invalid pack upload; one or more objects weren't sent", err); } } private boolean validCommit(RevWalk rw, RefControl ctl, ReceiveCommand cmd, ObjectId id) throws IOException { if (validCommits.contains(id)) { return true; } RevCommit c = rw.parseCommit(id); rw.parseBody(c); try (CommitReceivedEvent receiveEvent = new CommitReceivedEvent(cmd, project, ctl.getRefName(), rw.getObjectReader(), c, user)) { boolean isMerged = magicBranch != null && cmd.getRefName().equals(magicBranch.cmd.getRefName()) && magicBranch.merged; CommitValidators validators = isMerged ? commitValidatorsFactory.forMergedCommits(ctl) : commitValidatorsFactory.forReceiveCommits(ctl, sshInfo, repo, rw); messages.addAll(validators.validate(receiveEvent)); } catch (CommitValidationException e) { logDebug("Commit validation failed on {}", c.name()); messages.addAll(e.getMessages()); reject(cmd, e.getMessage()); return false; } validCommits.add(c.copy()); return true; } private void autoCloseChanges(final ReceiveCommand cmd) { logDebug("Starting auto-closing of changes"); String refName = cmd.getRefName(); checkState( !MagicBranch.isMagicBranch(refName), "shouldn't be auto-closing changes on magic branch %s", refName); // TODO(dborowitz): Combine this BatchUpdate with the main one in // insertChangesAndPatchSets. try (BatchUpdate bu = batchUpdateFactory.create( db, projectControl.getProject().getNameKey(), user, TimeUtil.nowTs()); ObjectInserter ins = repo.newObjectInserter(); ObjectReader reader = ins.newReader(); RevWalk rw = new RevWalk(reader)) { bu.setRepository(repo, rw, ins).updateChangesInParallel(); bu.setRequestId(receiveId); // TODO(dborowitz): Teach BatchUpdate to ignore missing changes. RevCommit newTip = rw.parseCommit(cmd.getNewId()); Branch.NameKey branch = new Branch.NameKey(project.getNameKey(), refName); rw.reset(); rw.markStart(newTip); if (!ObjectId.zeroId().equals(cmd.getOldId())) { rw.markUninteresting(rw.parseCommit(cmd.getOldId())); } ListMultimap<ObjectId, Ref> byCommit = changeRefsById(); Map<Change.Key, ChangeNotes> byKey = null; List<ReplaceRequest> replaceAndClose = new ArrayList<>(); int existingPatchSets = 0; int newPatchSets = 0; COMMIT: for (RevCommit c; (c = rw.next()) != null; ) { rw.parseBody(c); for (Ref ref : byCommit.get(c.copy())) { existingPatchSets++; PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); bu.addOp( psId.getParentKey(), mergedByPushOpFactory.create(requestScopePropagator, psId, refName)); continue COMMIT; } for (String changeId : c.getFooterLines(CHANGE_ID)) { if (byKey == null) { byKey = openChangesByBranch(branch); } ChangeNotes onto = byKey.get(new Change.Key(changeId.trim())); if (onto != null) { newPatchSets++; // Hold onto this until we're done with the walk, as the call to // req.validate below calls isMergedInto which resets the walk. ReplaceRequest req = new ReplaceRequest(onto.getChangeId(), c, cmd, false); req.notes = onto; replaceAndClose.add(req); continue COMMIT; } } } for (final ReplaceRequest req : replaceAndClose) { Change.Id id = req.notes.getChangeId(); if (!req.validate(true)) { logDebug("Not closing {} because validation failed", id); continue; } req.addOps(bu, null); bu.addOp( id, mergedByPushOpFactory .create(requestScopePropagator, req.psId, refName) .setPatchSetProvider( new Provider<PatchSet>() { @Override public PatchSet get() { return req.replaceOp.getPatchSet(); } })); bu.addOp(id, new ChangeProgressOp(closeProgress)); } logDebug( "Auto-closing {} changes with existing patch sets and {} with new patch sets", existingPatchSets, newPatchSets); bu.execute(); } catch (RestApiException e) { logError("Can't insert patchset", e); } catch (IOException | OrmException | UpdateException | PermissionBackendException e) { logError("Can't scan for changes to close", e); } } private Map<Change.Key, ChangeNotes> openChangesByBranch(Branch.NameKey branch) throws OrmException { Map<Change.Key, ChangeNotes> r = new HashMap<>(); for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) { r.put(cd.change().getKey(), cd.notes()); } return r; } private void reject(@Nullable ReceiveCommand cmd, String why) { if (cmd != null) { cmd.setResult(REJECTED_OTHER_REASON, why); commandProgress.update(1); } } private static boolean isHead(ReceiveCommand cmd) { return cmd.getRefName().startsWith(Constants.R_HEADS); } private static boolean isConfig(ReceiveCommand cmd) { return cmd.getRefName().equals(RefNames.REFS_CONFIG); } private void logDebug(String msg, Object... args) { if (log.isDebugEnabled()) { log.debug(receiveId + msg, args); } } private void logWarn(String msg, Throwable t) { if (log.isWarnEnabled()) { if (t != null) { log.warn(receiveId + msg, t); } else { log.warn(receiveId + msg); } } } private void logWarn(String msg) { logWarn(msg, null); } private void logError(String msg, Throwable t) { if (log.isErrorEnabled()) { if (t != null) { log.error(receiveId + msg, t); } else { log.error(receiveId + msg); } } } private void logError(String msg) { logError(msg, null); } }