/* * Copyright 2013 gitblit.com. * * 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.gitblit.git; import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.eclipse.jgit.transport.ReceiveCommand.Type; import org.eclipse.jgit.transport.ReceivePack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Constants; import com.gitblit.Keys; import com.gitblit.extensions.PatchsetHook; import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.TicketModel; import com.gitblit.models.TicketModel.Change; import com.gitblit.models.TicketModel.Field; import com.gitblit.models.TicketModel.Patchset; import com.gitblit.models.TicketModel.PatchsetType; import com.gitblit.models.TicketModel.Status; import com.gitblit.models.TicketModel.TicketAction; import com.gitblit.models.TicketModel.TicketLink; import com.gitblit.models.UserModel; import com.gitblit.tickets.BranchTicketService; import com.gitblit.tickets.ITicketService; import com.gitblit.tickets.TicketMilestone; import com.gitblit.tickets.TicketNotifier; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.DiffUtils; import com.gitblit.utils.DiffUtils.DiffStat; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.JGitUtils.MergeResult; import com.gitblit.utils.JGitUtils.MergeStatus; import com.gitblit.utils.RefLogUtils; import com.gitblit.utils.StringUtils; import com.google.common.collect.Lists; /** * PatchsetReceivePack processes receive commands and allows for creating, updating, * and closing Gitblit tickets. It also executes Groovy pre- and post- receive * hooks. * * The patchset mechanism defined in this class is based on the ReceiveCommits class * from the Gerrit code review server. * * The general execution flow is: *
    *
  1. onPreReceive()
  2. *
  3. executeCommands()
  4. *
  5. onPostReceive()
  6. *
* * @author Android Open Source Project * @author James Moger * */ public class PatchsetReceivePack extends GitblitReceivePack { protected static final List MAGIC_REFS = Arrays.asList(Constants.R_FOR, Constants.R_TICKET); protected static final Pattern NEW_PATCHSET = Pattern.compile("^refs/tickets/(?:[0-9a-zA-Z][0-9a-zA-Z]/)?([1-9][0-9]*)(?:/new)?$"); private static final Logger LOGGER = LoggerFactory.getLogger(PatchsetReceivePack.class); protected final ITicketService ticketService; protected final TicketNotifier ticketNotifier; private boolean requireMergeablePatchset; public PatchsetReceivePack(IGitblit gitblit, Repository db, RepositoryModel repository, UserModel user) { super(gitblit, db, repository, user); this.ticketService = gitblit.getTicketService(); this.ticketNotifier = ticketService.createNotifier(); } /** Returns the patchset ref root from the ref */ private String getPatchsetRef(String refName) { for (String patchRef : MAGIC_REFS) { if (refName.startsWith(patchRef)) { return patchRef; } } return null; } /** Checks if the supplied ref name is a patchset ref */ private boolean isPatchsetRef(String refName) { return !StringUtils.isEmpty(getPatchsetRef(refName)); } /** Checks if the supplied ref name is a change ref */ private boolean isTicketRef(String refName) { return refName.startsWith(Constants.R_TICKETS_PATCHSETS); } /** Extracts the integration branch from the ref name */ private String getIntegrationBranch(String refName) { String patchsetRef = getPatchsetRef(refName); String branch = refName.substring(patchsetRef.length()); if (branch.indexOf('%') > -1) { branch = branch.substring(0, branch.indexOf('%')); } String defaultBranch = "master"; try { defaultBranch = getRepository().getBranch(); } catch (Exception e) { LOGGER.error("failed to determine default branch for " + repository.name, e); } if (!StringUtils.isEmpty(getRepositoryModel().mergeTo)) { // repository settings specifies a default integration branch defaultBranch = Repository.shortenRefName(getRepositoryModel().mergeTo); } long ticketId = 0L; try { ticketId = Long.parseLong(branch); } catch (Exception e) { // not a number } if (ticketId > 0 || branch.equalsIgnoreCase("default") || branch.equalsIgnoreCase("new")) { return defaultBranch; } return branch; } /** Extracts the ticket id from the ref name */ private long getTicketId(String refName) { if (refName.indexOf('%') > -1) { refName = refName.substring(0, refName.indexOf('%')); } if (refName.startsWith(Constants.R_FOR)) { String ref = refName.substring(Constants.R_FOR.length()); try { return Long.parseLong(ref); } catch (Exception e) { // not a number } } else if (refName.startsWith(Constants.R_TICKET) || refName.startsWith(Constants.R_TICKETS_PATCHSETS)) { return PatchsetCommand.getTicketNumber(refName); } return 0L; } /** Returns true if the ref namespace exists */ private boolean hasRefNamespace(String ref) { Map blockingFors; try { blockingFors = getRepository().getRefDatabase().getRefs(ref); } catch (IOException err) { sendError("Cannot scan refs in {0}", repository.name); LOGGER.error("Error!", err); return true; } if (!blockingFors.isEmpty()) { sendError("{0} needs the following refs removed to receive patchsets: {1}", repository.name, blockingFors.keySet()); return true; } return false; } /** Removes change ref receive commands */ private List excludeTicketCommands(Collection commands) { List filtered = new ArrayList(); for (ReceiveCommand cmd : commands) { if (!isTicketRef(cmd.getRefName())) { // this is not a ticket ref update filtered.add(cmd); } } return filtered; } /** Removes patchset receive commands for pre- and post- hook integrations */ private List excludePatchsetCommands(Collection commands) { List filtered = new ArrayList(); for (ReceiveCommand cmd : commands) { if (!isPatchsetRef(cmd.getRefName())) { // this is a non-patchset ref update filtered.add(cmd); } } return filtered; } /** Process receive commands EXCEPT for Patchset commands. */ @Override public void onPreReceive(ReceivePack rp, Collection commands) { Collection filtered = excludePatchsetCommands(commands); super.onPreReceive(rp, filtered); } /** Process receive commands EXCEPT for Patchset commands. */ @Override public void onPostReceive(ReceivePack rp, Collection commands) { Collection filtered = excludePatchsetCommands(commands); super.onPostReceive(rp, filtered); // send all queued ticket notifications after processing all patchsets ticketNotifier.sendAll(); } @Override protected void validateCommands() { // workaround for JGit's awful scoping choices // // set the patchset refs to OK to bypass checks in the super implementation for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) { if (isPatchsetRef(cmd.getRefName())) { if (cmd.getType() == ReceiveCommand.Type.CREATE) { cmd.setResult(Result.OK); } } } super.validateCommands(); } /** Execute commands to update references. */ @Override protected void executeCommands() { // we process patchsets unless the user is pushing something special boolean processPatchsets = true; for (ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) { if (ticketService instanceof BranchTicketService && BranchTicketService.BRANCH.equals(cmd.getRefName())) { // the user is pushing an update to the BranchTicketService data processPatchsets = false; } } // workaround for JGit's awful scoping choices // // reset the patchset refs to NOT_ATTEMPTED (see validateCommands) for (ReceiveCommand cmd : filterCommands(Result.OK)) { if (isPatchsetRef(cmd.getRefName())) { cmd.setResult(Result.NOT_ATTEMPTED); } else if (ticketService instanceof BranchTicketService && BranchTicketService.BRANCH.equals(cmd.getRefName())) { // the user is pushing an update to the BranchTicketService data processPatchsets = false; } } List toApply = filterCommands(Result.NOT_ATTEMPTED); if (toApply.isEmpty()) { return; } ProgressMonitor updating = NullProgressMonitor.INSTANCE; boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K); if (sideBand) { SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut); pm.setDelayStart(250, TimeUnit.MILLISECONDS); updating = pm; } BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate(); batch.setAllowNonFastForwards(isAllowNonFastForwards()); batch.setRefLogIdent(getRefLogIdent()); batch.setRefLogMessage("push", true); ReceiveCommand patchsetRefCmd = null; PatchsetCommand patchsetCmd = null; for (ReceiveCommand cmd : toApply) { if (Result.NOT_ATTEMPTED != cmd.getResult()) { // Already rejected by the core receive process. continue; } if (isPatchsetRef(cmd.getRefName()) && processPatchsets) { if (ticketService == null) { sendRejection(cmd, "Sorry, the ticket service is unavailable and can not accept patchsets at this time."); continue; } if (!ticketService.isReady()) { sendRejection(cmd, "Sorry, the ticket service can not accept patchsets at this time."); continue; } if (UserModel.ANONYMOUS.equals(user)) { // server allows anonymous pushes, but anonymous patchset // contributions are prohibited by design sendRejection(cmd, "Sorry, anonymous patchset contributions are prohibited."); continue; } final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName()); if (m.matches()) { // prohibit pushing directly to a patchset ref long id = getTicketId(cmd.getRefName()); sendError("You may not directly push directly to a patchset ref!"); sendError("Instead, please push to one the following:"); sendError(" - {0}{1,number,0}", Constants.R_FOR, id); sendError(" - {0}{1,number,0}", Constants.R_TICKET, id); sendRejection(cmd, "protected ref"); continue; } if (hasRefNamespace(Constants.R_FOR)) { // the refs/for/ namespace exists and it must not LOGGER.error("{} already has refs in the {} namespace", repository.name, Constants.R_FOR); sendRejection(cmd, "Sorry, a repository administrator will have to remove the {} namespace", Constants.R_FOR); continue; } if (cmd.getNewId().equals(ObjectId.zeroId())) { // ref deletion request if (cmd.getRefName().startsWith(Constants.R_TICKET)) { if (user.canDeleteRef(repository)) { batch.addCommand(cmd); } else { sendRejection(cmd, "Sorry, you do not have permission to delete {}", cmd.getRefName()); } } else { sendRejection(cmd, "Sorry, you can not delete {}", cmd.getRefName()); } continue; } if (patchsetRefCmd != null) { sendRejection(cmd, "You may only push one patchset at a time."); continue; } LOGGER.info(MessageFormat.format("Verifying {0} push ref \"{1}\" received from {2}", repository.name, cmd.getRefName(), user.username)); // responsible verification String responsible = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.RESPONSIBLE); if (!StringUtils.isEmpty(responsible)) { UserModel assignee = gitblit.getUserModel(responsible); if (assignee == null) { // no account by this name sendRejection(cmd, "{0} can not be assigned any tickets because there is no user account by that name", responsible); continue; } else if (!assignee.canPush(repository)) { // account does not have RW permissions sendRejection(cmd, "{0} ({1}) can not be assigned any tickets because the user does not have RW permissions for {2}", assignee.getDisplayName(), assignee.username, repository.name); continue; } } // milestone verification String milestone = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.MILESTONE); if (!StringUtils.isEmpty(milestone)) { TicketMilestone milestoneModel = ticketService.getMilestone(repository, milestone); if (milestoneModel == null) { // milestone does not exist sendRejection(cmd, "Sorry, \"{0}\" is not a valid milestone!", milestone); continue; } } // watcher verification List watchers = PatchsetCommand.getOptions(cmd, PatchsetCommand.WATCH); if (!ArrayUtils.isEmpty(watchers)) { boolean verified = true; for (String watcher : watchers) { UserModel user = gitblit.getUserModel(watcher); if (user == null) { // watcher does not exist sendRejection(cmd, "Sorry, \"{0}\" is not a valid username for the watch list!", watcher); verified = false; break; } } if (!verified) { continue; } } patchsetRefCmd = cmd; patchsetCmd = preparePatchset(cmd); if (patchsetCmd != null) { batch.addCommand(patchsetCmd); } continue; } batch.addCommand(cmd); } if (!batch.getCommands().isEmpty()) { try { batch.execute(getRevWalk(), updating); } catch (IOException err) { for (ReceiveCommand cmd : toApply) { if (cmd.getResult() == Result.NOT_ATTEMPTED) { sendRejection(cmd, "lock error: {0}", err.getMessage()); LOGGER.error(MessageFormat.format("failed to lock {0}:{1}", repository.name, cmd.getRefName()), err); } } } } // // set the results into the patchset ref receive command // if (patchsetRefCmd != null && patchsetCmd != null) { if (!patchsetCmd.getResult().equals(Result.OK)) { // patchset command failed! LOGGER.error(patchsetCmd.getType() + " " + patchsetCmd.getRefName() + " " + patchsetCmd.getResult()); patchsetRefCmd.setResult(patchsetCmd.getResult(), patchsetCmd.getMessage()); } else { // all patchset commands were applied patchsetRefCmd.setResult(Result.OK); // update the ticket branch ref RefUpdate ru = updateRef( patchsetCmd.getTicketBranch(), patchsetCmd.getNewId(), patchsetCmd.getPatchsetType()); updateReflog(ru); TicketModel ticket = processPatchset(patchsetCmd); if (ticket != null) { ticketNotifier.queueMailing(ticket); } } } // // if there are standard ref update receive commands that were // successfully processed, process referenced tickets, if any // List allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK); List refUpdates = excludePatchsetCommands(allUpdates); List stdUpdates = excludeTicketCommands(refUpdates); if (!stdUpdates.isEmpty()) { int ticketsProcessed = 0; for (ReceiveCommand cmd : stdUpdates) { switch (cmd.getType()) { case CREATE: case UPDATE: if (cmd.getRefName().startsWith(Constants.R_HEADS)) { Collection tickets = processReferencedTickets(cmd); ticketsProcessed += tickets.size(); for (TicketModel ticket : tickets) { ticketNotifier.queueMailing(ticket); } } break; case UPDATE_NONFASTFORWARD: if (cmd.getRefName().startsWith(Constants.R_HEADS)) { String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId()); List deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name()); for (TicketLink link : deletedRefs) { link.isDelete = true; } Change deletion = new Change(user.username); deletion.pendingLinks = deletedRefs; ticketService.updateTicket(repository, 0, deletion); Collection tickets = processReferencedTickets(cmd); ticketsProcessed += tickets.size(); for (TicketModel ticket : tickets) { ticketNotifier.queueMailing(ticket); } } break; default: break; } } if (ticketsProcessed == 1) { sendInfo("1 ticket updated"); } else if (ticketsProcessed > 1) { sendInfo("{0} tickets updated", ticketsProcessed); } } // reset the ticket caches for the repository ticketService.resetCaches(repository); } /** * Prepares a patchset command. * * @param cmd * @return the patchset command */ private PatchsetCommand preparePatchset(ReceiveCommand cmd) { String branch = getIntegrationBranch(cmd.getRefName()); long number = getTicketId(cmd.getRefName()); TicketModel ticket = null; if (number > 0 && ticketService.hasTicket(repository, number)) { ticket = ticketService.getTicket(repository, number); } if (ticket == null) { if (number > 0) { // requested ticket does not exist sendError("Sorry, {0} does not have ticket {1,number,0}!", repository.name, number); sendRejection(cmd, "Invalid ticket number"); return null; } } else { if (ticket.isMerged()) { // ticket already merged & resolved Change mergeChange = null; for (Change change : ticket.changes) { if (change.isMerge()) { mergeChange = change; break; } } if (mergeChange != null) { sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!", mergeChange.author, mergeChange.patchset, number, ticket.mergeTo); } sendRejection(cmd, "Ticket {0,number,0} already resolved", number); return null; } else if (!StringUtils.isEmpty(ticket.mergeTo)) { // ticket specifies integration branch branch = ticket.mergeTo; } } final int shortCommitIdLen = settings.getInteger(Keys.web.shortCommitIdLength, 6); final String shortTipId = cmd.getNewId().getName().substring(0, shortCommitIdLen); final RevCommit tipCommit = JGitUtils.getCommit(getRepository(), cmd.getNewId().getName()); final String forBranch = branch; RevCommit mergeBase = null; Ref forBranchRef = getAdvertisedRefs().get(Constants.R_HEADS + forBranch); if (forBranchRef == null || forBranchRef.getObjectId() == null) { // unknown integration branch sendError("Sorry, there is no integration branch named ''{0}''.", forBranch); sendRejection(cmd, "Invalid integration branch specified"); return null; } else { // determine the merge base for the patchset on the integration branch String base = JGitUtils.getMergeBase(getRepository(), forBranchRef.getObjectId(), tipCommit.getId()); if (StringUtils.isEmpty(base)) { sendError(""); sendError("There is no common ancestry between {0} and {1}.", forBranch, shortTipId); sendError("Please reconsider your proposed integration branch, {0}.", forBranch); sendError(""); sendRejection(cmd, "no merge base for patchset and {0}", forBranch); return null; } mergeBase = JGitUtils.getCommit(getRepository(), base); } // ensure that the patchset can be cleanly merged right now MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch); switch (status) { case ALREADY_MERGED: sendError(""); sendError("You have already merged this patchset.", forBranch); sendError(""); sendRejection(cmd, "everything up-to-date"); return null; case MERGEABLE: break; default: if (ticket == null || requireMergeablePatchset) { sendError(""); sendError("Your patchset can not be cleanly merged into {0}.", forBranch); sendError("Please rebase your patchset and push again."); sendError("NOTE:", number); sendError("You should push your rebase to refs/for/{0,number,0}", number); sendError(""); sendError(" git push origin HEAD:refs/for/{0,number,0}", number); sendError(""); sendRejection(cmd, "patchset not mergeable"); return null; } } // check to see if this commit is already linked to a ticket if (ticket != null && JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) { sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number); sendRejection(cmd, "everything up-to-date"); return null; } List ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit); PatchsetCommand psCmd; if (ticket == null) { /* * NEW TICKET */ Patchset patchset = newPatchset(null, mergeBase.getName(), tipCommit.getName()); int minLength = 10; int maxLength = 100; String minTitle = MessageFormat.format(" minimum length of a title is {0} characters.", minLength); String maxTitle = MessageFormat.format(" maximum length of a title is {0} characters.", maxLength); if (patchset.commits > 1) { sendError(""); sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!", forBranch, patchset.commits); sendError(""); // display an ellipsized log of the commits being pushed RevWalk walk = getRevWalk(); walk.reset(); walk.sort(RevSort.TOPO); int boundary = 3; int count = 0; try { walk.markStart(tipCommit); walk.markUninteresting(mergeBase); for (;;) { RevCommit c = walk.next(); if (c == null) { break; } if (count < boundary || count >= (patchset.commits - boundary)) { walk.parseBody(c); sendError(" {0} {1}", c.getName().substring(0, shortCommitIdLen), StringUtils.trimString(c.getShortMessage(), 60)); } else if (count == boundary) { sendError(" ... more commits ..."); } count++; } } catch (IOException e) { // Should never happen, the core receive process would have // identified the missing object earlier before we got control. LOGGER.error("failed to get commit count", e); } finally { walk.close(); } sendError(""); sendError("Possible Solutions:"); sendError(""); int solution = 1; String forSpec = cmd.getRefName().substring(Constants.R_FOR.length()); if (forSpec.equals("default") || forSpec.equals("new")) { try { // determine other possible integration targets List bases = Lists.newArrayList(); for (Ref ref : getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values()) { if (!ref.getName().startsWith(Constants.R_TICKET) && !ref.getName().equals(forBranchRef.getName())) { if (JGitUtils.isMergedInto(getRepository(), ref.getObjectId(), tipCommit)) { bases.add(Repository.shortenRefName(ref.getName())); } } } if (!bases.isEmpty()) { if (bases.size() == 1) { // suggest possible integration targets String base = bases.get(0); sendError("{0}. Propose this change for the ''{1}'' branch.", solution++, base); sendError(""); sendError(" git push origin HEAD:refs/for/{0}", base); sendError(" pt propose {0}", base); sendError(""); } else { // suggest possible integration targets sendError("{0}. Propose this change for a different branch.", solution++); sendError(""); for (String base : bases) { sendError(" git push origin HEAD:refs/for/{0}", base); sendError(" pt propose {0}", base); sendError(""); } } } } catch (IOException e) { LOGGER.error(null, e); } } sendError("{0}. Squash your changes into a single commit with a meaningful message.", solution++); sendError(""); sendError("{0}. Open a ticket for your changes and then push your {1} commits to the ticket.", solution++, patchset.commits); sendError(""); sendError(" git push origin HEAD:refs/for/{id}"); sendError(" pt propose {id}"); sendError(""); sendRejection(cmd, "too many commits"); return null; } // require a reasonable title/subject String title = tipCommit.getFullMessage().trim().split("\n")[0]; if (title.length() < minLength) { // reject, title too short sendError(""); sendError("Please supply a longer title in your commit message!"); sendError(""); sendError(minTitle); sendError(maxTitle); sendError(""); sendRejection(cmd, "ticket title is too short [{0}/{1}]", title.length(), maxLength); return null; } if (title.length() > maxLength) { // reject, title too long sendError(""); sendError("Please supply a more concise title in your commit message!"); sendError(""); sendError(minTitle); sendError(maxTitle); sendError(""); sendRejection(cmd, "ticket title is too long [{0}/{1}]", title.length(), maxLength); return null; } // assign new id long ticketId = ticketService.assignNewId(repository); // create the patchset command psCmd = new PatchsetCommand(user.username, patchset); psCmd.newTicket(tipCommit, forBranch, ticketId, cmd.getRefName()); } else { /* * EXISTING TICKET */ Patchset patchset = newPatchset(ticket, mergeBase.getName(), tipCommit.getName()); psCmd = new PatchsetCommand(user.username, patchset); psCmd.updateTicket(tipCommit, forBranch, ticket, cmd.getRefName()); } // confirm user can push the patchset boolean pushPermitted = ticket == null || !ticket.hasPatchsets() || ticket.isAuthor(user.username) || ticket.isPatchsetAuthor(user.username) || ticket.isResponsible(user.username) || user.canPush(repository); switch (psCmd.getPatchsetType()) { case Proposal: // proposals (first patchset) are always acceptable break; case FastForward: // patchset updates must be permitted if (!pushPermitted) { // reject sendError(""); sendError("To push a patchset to this ticket one of the following must be true:"); sendError(" 1. you created the ticket"); sendError(" 2. you created the first patchset"); sendError(" 3. you are specified as responsible for the ticket"); sendError(" 4. you have push (RW) permissions to {0}", repository.name); sendError(""); sendRejection(cmd, "not permitted to push to ticket {0,number,0}", ticket.number); return null; } break; default: // non-fast-forward push if (!pushPermitted) { // reject sendRejection(cmd, "non-fast-forward ({0})", psCmd.getPatchsetType()); return null; } break; } Change change = psCmd.getChange(); change.pendingLinks = ticketLinks; return psCmd; } /** * Creates or updates an ticket with the specified patchset. * * @param cmd * @return a ticket if the creation or update was successful */ private TicketModel processPatchset(PatchsetCommand cmd) { Change change = cmd.getChange(); if (cmd.isNewTicket()) { // create the ticket object TicketModel ticket = ticketService.createTicket(repository, cmd.getTicketId(), change); if (ticket != null) { sendInfo(""); sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG)); sendInfo("created proposal ticket from patchset"); sendInfo(ticketService.getTicketUrl(ticket)); sendInfo(""); // log the new patch ref RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName()))); // call any patchset hooks for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) { try { hook.onNewPatchset(ticket); } catch (Exception e) { LOGGER.error("Failed to execute extension", e); } } return ticket; } else { sendError("FAILED to create ticket"); } } else { // update an existing ticket TicketModel ticket = ticketService.updateTicket(repository, cmd.getTicketId(), change); if (ticket != null) { sendInfo(""); sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG)); if (change.patchset.rev == 1) { // new patchset sendInfo("uploaded patchset {0} ({1})", change.patchset.number, change.patchset.type.toString()); } else { // updated patchset sendInfo("added {0} {1} to patchset {2}", change.patchset.added, change.patchset.added == 1 ? "commit" : "commits", change.patchset.number); } sendInfo(ticketService.getTicketUrl(ticket)); sendInfo(""); // log the new patchset ref RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName()))); // call any patchset hooks final boolean isNewPatchset = change.patchset.rev == 1; for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) { try { if (isNewPatchset) { hook.onNewPatchset(ticket); } else { hook.onUpdatePatchset(ticket); } } catch (Exception e) { LOGGER.error("Failed to execute extension", e); } } // return the updated ticket return ticket; } else { sendError("FAILED to upload {0} for ticket {1,number,0}", change.patchset, cmd.getTicketId()); } } return null; } /** * Automatically closes open tickets that have been merged to their integration * branch by a client and adds references to tickets if made in the commit message. * * @param cmd */ private Collection processReferencedTickets(ReceiveCommand cmd) { Map mergedTickets = new LinkedHashMap(); final RevWalk rw = getRevWalk(); try { rw.reset(); rw.markStart(rw.parseCommit(cmd.getNewId())); if (!ObjectId.zeroId().equals(cmd.getOldId())) { rw.markUninteresting(rw.parseCommit(cmd.getOldId())); } RevCommit c; while ((c = rw.next()) != null) { rw.parseBody(c); List ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c); if (ticketLinks == null) { continue; } for (TicketLink link : ticketLinks) { if (mergedTickets.containsKey(link.targetTicketId)) { continue; } TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId); if (ticket == null) { continue; } String integrationBranch; if (StringUtils.isEmpty(ticket.mergeTo)) { // unspecified integration branch integrationBranch = null; } else { // specified integration branch integrationBranch = Constants.R_HEADS + ticket.mergeTo; } Change change; Patchset patchset = null; String mergeSha = c.getName(); String mergeTo = Repository.shortenRefName(cmd.getRefName()); if (link.action == TicketAction.Commit) { //A commit can reference a ticket in any branch even if the ticket is closed. //This allows developers to identify and communicate related issues change = new Change(user.username); change.referenceCommit(mergeSha); } else { // ticket must be open and, if specified, the ref must match the integration branch if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) { continue; } String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number); boolean knownPatchset = false; Set refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId()); if (refs != null) { for (Ref ref : refs) { if (ref.getName().startsWith(baseRef)) { knownPatchset = true; break; } } } if (knownPatchset) { // identify merged patchset by the patchset tip for (Patchset ps : ticket.getPatchsets()) { if (ps.tip.equals(mergeSha)) { patchset = ps; break; } } if (patchset == null) { // should not happen - unless ticket has been hacked sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!", mergeSha, ticket.number); continue; } // create a new change change = new Change(user.username); } else { // new patchset pushed by user String base = cmd.getOldId().getName(); patchset = newPatchset(ticket, base, mergeSha); PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset); psCmd.updateTicket(c, mergeTo, ticket, null); // create a ticket patchset ref updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type); RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type); updateReflog(ru); // create a change from the patchset command change = psCmd.getChange(); } // set the common change data about the merge change.setField(Field.status, Status.Merged); change.setField(Field.mergeSha, mergeSha); change.setField(Field.mergeTo, mergeTo); if (StringUtils.isEmpty(ticket.responsible)) { // unassigned tickets are assigned to the closer change.setField(Field.responsible, user.username); } } ticket = ticketService.updateTicket(repository, ticket.number, change); if (ticket != null) { sendInfo(""); sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG)); switch (link.action) { case Commit: { sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo); } break; case Close: { sendInfo("closed by push of {0} to {1}", patchset, mergeTo); mergedTickets.put(ticket.number, ticket); } break; default: { } } sendInfo(ticketService.getTicketUrl(ticket)); sendInfo(""); } else { String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6)); switch (link.action) { case Commit: { sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid); } break; case Close: { sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid); } break; default: { } } } } } } catch (IOException e) { LOGGER.error("Can't scan for changes to reference or close", e); } finally { rw.reset(); } return mergedTickets.values(); } /** * Creates a new patchset with metadata. * * @param ticket * @param mergeBase * @param tip */ private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) { int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip); Patchset newPatchset = new Patchset(); newPatchset.tip = tip; newPatchset.base = mergeBase; newPatchset.commits = totalCommits; Patchset currPatchset = ticket == null ? null : ticket.getCurrentPatchset(); if (currPatchset == null) { /* * PROPOSAL PATCHSET * patchset 1, rev 1 */ newPatchset.number = 1; newPatchset.rev = 1; newPatchset.type = PatchsetType.Proposal; // diffstat from merge base DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip); newPatchset.insertions = diffStat.getInsertions(); newPatchset.deletions = diffStat.getDeletions(); } else { /* * PATCHSET UPDATE */ int added = totalCommits - currPatchset.commits; boolean ff = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, tip); boolean squash = added < 0; boolean rebase = !currPatchset.base.equals(mergeBase); // determine type, number and rev of the patchset if (ff) { /* * FAST-FORWARD * patchset number preserved, rev incremented */ boolean merged = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, ticket.mergeTo); if (merged) { // current patchset was already merged // new patchset, mark as rebase newPatchset.type = PatchsetType.Rebase; newPatchset.number = currPatchset.number + 1; newPatchset.rev = 1; // diffstat from parent DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip); newPatchset.insertions = diffStat.getInsertions(); newPatchset.deletions = diffStat.getDeletions(); } else { // FF update to patchset newPatchset.type = PatchsetType.FastForward; newPatchset.number = currPatchset.number; newPatchset.rev = currPatchset.rev + 1; newPatchset.parent = currPatchset.tip; // diffstat from parent DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), currPatchset.tip, tip); newPatchset.insertions = diffStat.getInsertions(); newPatchset.deletions = diffStat.getDeletions(); } } else { /* * NON-FAST-FORWARD * new patchset, rev 1 */ if (rebase && squash) { newPatchset.type = PatchsetType.Rebase_Squash; newPatchset.number = currPatchset.number + 1; newPatchset.rev = 1; } else if (squash) { newPatchset.type = PatchsetType.Squash; newPatchset.number = currPatchset.number + 1; newPatchset.rev = 1; } else if (rebase) { newPatchset.type = PatchsetType.Rebase; newPatchset.number = currPatchset.number + 1; newPatchset.rev = 1; } else { newPatchset.type = PatchsetType.Amend; newPatchset.number = currPatchset.number + 1; newPatchset.rev = 1; } // diffstat from merge base DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip); newPatchset.insertions = diffStat.getInsertions(); newPatchset.deletions = diffStat.getDeletions(); } if (added > 0) { // ignore squash (negative add) newPatchset.added = added; } } return newPatchset; } private RefUpdate updateRef(String ref, ObjectId newId, PatchsetType type) { ObjectId ticketRefId = ObjectId.zeroId(); try { ticketRefId = getRepository().resolve(ref); } catch (Exception e) { // ignore } try { RefUpdate ru = getRepository().updateRef(ref, false); ru.setRefLogIdent(getRefLogIdent()); switch (type) { case Amend: case Rebase: case Rebase_Squash: case Squash: ru.setForceUpdate(true); break; default: break; } ru.setExpectedOldObjectId(ticketRefId); ru.setNewObjectId(newId); RefUpdate.Result result = ru.update(getRevWalk()); if (result == RefUpdate.Result.LOCK_FAILURE) { sendError("Failed to obtain lock when updating {0}:{1}", repository.name, ref); sendError("Perhaps an administrator should remove {0}/{1}.lock?", getRepository().getDirectory(), ref); return null; } return ru; } catch (IOException e) { LOGGER.error("failed to update ref " + ref, e); sendError("There was an error updating ref {0}:{1}", repository.name, ref); } return null; } private void updateReflog(RefUpdate ru) { if (ru == null) { return; } ReceiveCommand.Type type = null; switch (ru.getResult()) { case NEW: type = Type.CREATE; break; case FAST_FORWARD: type = Type.UPDATE; break; case FORCED: type = Type.UPDATE_NONFASTFORWARD; break; default: LOGGER.error(MessageFormat.format("unexpected ref update type {0} for {1}", ru.getResult(), ru.getName())); return; } ReceiveCommand cmd = new ReceiveCommand(ru.getOldObjectId(), ru.getNewObjectId(), ru.getName(), type); RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd)); } /** * Merge the specified patchset to the integration branch. * * @param ticket * @param patchset * @return true, if successful */ public MergeStatus merge(TicketModel ticket) { PersonIdent committer = new PersonIdent(user.getDisplayName(), StringUtils.isEmpty(user.emailAddress) ? (user.username + "@gitblit") : user.emailAddress); Patchset patchset = ticket.getCurrentPatchset(); String message = MessageFormat.format("Merged #{0,number,0} \"{1}\"", ticket.number, ticket.title); Ref oldRef = null; try { oldRef = getRepository().getRef(ticket.mergeTo); } catch (IOException e) { LOGGER.error("failed to get ref for " + ticket.mergeTo, e); } MergeResult mergeResult = JGitUtils.merge( getRepository(), patchset.tip, ticket.mergeTo, committer, message); if (StringUtils.isEmpty(mergeResult.sha)) { LOGGER.error("FAILED to merge {} to {} ({})", new Object [] { patchset, ticket.mergeTo, mergeResult.status.name() }); return mergeResult.status; } Change change = new Change(user.username); change.setField(Field.status, Status.Merged); change.setField(Field.mergeSha, mergeResult.sha); change.setField(Field.mergeTo, ticket.mergeTo); if (StringUtils.isEmpty(ticket.responsible)) { // unassigned tickets are assigned to the closer change.setField(Field.responsible, user.username); } long ticketId = ticket.number; ticket = ticketService.updateTicket(repository, ticket.number, change); if (ticket != null) { ticketNotifier.queueMailing(ticket); if (oldRef != null) { ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(), ObjectId.fromString(mergeResult.sha), oldRef.getName()); cmd.setResult(Result.OK); List commands = Arrays.asList(cmd); logRefChange(commands); updateIncrementalPushTags(commands); updateGitblitRefLog(commands); } // call patchset hooks for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) { try { hook.onMergePatchset(ticket); } catch (Exception e) { LOGGER.error("Failed to execute extension", e); } } return mergeResult.status; } else { LOGGER.error("FAILED to resolve ticket {} by merge from web ui", ticketId); } return mergeResult.status; } public void sendAll() { ticketNotifier.sendAll(); } }