Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -30,7 +30,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
@@ -51,6 +50,7 @@
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;
@@ -59,6 +59,8 @@
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;
@@ -72,6 +74,7 @@
import com.gitblit.utils.JGitUtils.MergeStatus;
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.google.common.collect.Lists;
/**
@@ -149,6 +152,11 @@
         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);
@@ -163,11 +171,11 @@
   /** 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());
         if (ref.indexOf('%') > -1) {
            ref = ref.substring(0, ref.indexOf('%'));
         }
         try {
            return Long.parseLong(ref);
         } catch (Exception e) {
@@ -308,6 +316,7 @@
         }
         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;
@@ -345,10 +354,27 @@
               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);
@@ -380,13 +406,18 @@
            // watcher verification
            List<String> 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);
                     continue;
                     verified = false;
                     break;
                  }
               }
               if (!verified) {
                  continue;
               }
            }
@@ -429,7 +460,10 @@
            patchsetRefCmd.setResult(Result.OK);
            // update the ticket branch ref
            RefUpdate ru = updateRef(patchsetCmd.getTicketBranch(), patchsetCmd.getNewId());
            RefUpdate ru = updateRef(
                  patchsetCmd.getTicketBranch(),
                  patchsetCmd.getNewId(),
                  patchsetCmd.getPatchsetType());
            updateReflog(ru);
            TicketModel ticket = processPatchset(patchsetCmd);
@@ -452,9 +486,27 @@
            switch (cmd.getType()) {
            case CREATE:
            case UPDATE:
               if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
                  Collection<TicketModel> tickets = processReferencedTickets(cmd);
                  ticketsProcessed += tickets.size();
                  for (TicketModel ticket : tickets) {
                     ticketNotifier.queueMailing(ticket);
                  }
               }
               break;
            case UPDATE_NONFASTFORWARD:
               if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
                  Collection<TicketModel> tickets = processMergedTickets(cmd);
                  String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
                  List<TicketLink> 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<TicketModel> tickets = processReferencedTickets(cmd);
                  ticketsProcessed += tickets.size();
                  for (TicketModel ticket : tickets) {
                     ticketNotifier.queueMailing(ticket);
@@ -484,8 +536,6 @@
    * @return the patchset command
    */
   private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
      LOGGER.info(MessageFormat.format("Preparing {0} patchset command for \"{1}\" received from {2}",
            repository.name, cmd.getRefName(), user.username));
      String branch = getIntegrationBranch(cmd.getRefName());
      long number = getTicketId(cmd.getRefName());
@@ -511,8 +561,10 @@
                  break;
               }
            }
            sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
            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)) {
@@ -571,15 +623,17 @@
            return null;
         }
      }
      // check to see if this commit is already linked to a ticket
      long id = identifyTicket(tipCommit, false);
      if (id > 0) {
         sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, id);
      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<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
      PatchsetCommand psCmd;
      if (ticket == null) {
         /*
@@ -594,14 +648,102 @@
         if (patchset.commits > 1) {
            sendError("");
            sendError("To create a proposal ticket, please squash your commits and");
            sendError("provide a meaningful commit message with a short title &");
            sendError("an optional description/body.");
            sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!",
                  forBranch, patchset.commits);
            sendError("");
            sendError(minTitle);
            sendError(maxTitle);
            // 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("");
            sendRejection(cmd, "please squash to one commit");
            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<String> 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;
         }
@@ -681,6 +823,10 @@
         }
         break;
      }
      Change change = psCmd.getChange();
      change.pendingLinks = ticketLinks;
      return psCmd;
   }
@@ -706,6 +852,15 @@
            // 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 {
@@ -734,6 +889,20 @@
            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 {
@@ -746,11 +915,11 @@
   /**
    * Automatically closes open tickets that have been merged to their integration
    * branch by a client.
    * branch by a client and adds references to tickets if made in the commit message.
    *
    * @param cmd
    */
   private Collection<TicketModel> processMergedTickets(ReceiveCommand cmd) {
   private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
      Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
      final RevWalk rw = getRevWalk();
      try {
@@ -763,102 +932,151 @@
         RevCommit c;
         while ((c = rw.next()) != null) {
            rw.parseBody(c);
            long ticketNumber = identifyTicket(c, true);
            if (ticketNumber == 0L || mergedTickets.containsKey(ticketNumber)) {
            List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
            if (ticketLinks == null) {
               continue;
            }
            TicketModel ticket = ticketService.getTicket(repository, ticketNumber);
            String integrationBranch;
            if (StringUtils.isEmpty(ticket.mergeTo)) {
               // unspecified integration branch
               integrationBranch = null;
            } else {
               // specified integration branch
               integrationBranch = Constants.R_HEADS + ticket.mergeTo;
            }
            // 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<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
            if (refs != null) {
               for (Ref ref : refs) {
                  if (ref.getName().startsWith(baseRef)) {
                     knownPatchset = true;
                     break;
                  }
               }
            }
            String mergeSha = c.getName();
            String mergeTo = Repository.shortenRefName(cmd.getRefName());
            Change change;
            Patchset patchset;
            if (knownPatchset) {
               // identify merged patchset by the patchset tip
               patchset = null;
               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);
            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());
               // 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);
               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<Ref> 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));
               // create a ticket patchset ref
               updateRef(psCmd.getPatchsetBranch(), c.getId());
               RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId());
               updateReflog(ru);
                  switch (link.action) {
                     case Commit: {
                        sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
                     }
                     break;
               // create a change from the patchset command
               change = psCmd.getChange();
            }
                     case Close: {
                        sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
                        mergedTickets.put(ticket.number, ticket);
                     }
                     break;
            // set the common change data about the merge
            change.setField(Field.status, Status.Merged);
            change.setField(Field.mergeSha, mergeSha);
            change.setField(Field.mergeTo, mergeTo);
                     default: {
                     }
                  }
            if (StringUtils.isEmpty(ticket.responsible)) {
               // unassigned tickets are assigned to the closer
               change.setField(Field.responsible, user.username);
            }
                  sendInfo(ticketService.getTicketUrl(ticket));
                  sendInfo("");
            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));
               sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
               sendInfo(ticketService.getTicketUrl(ticket));
               sendInfo("");
               mergedTickets.put(ticket.number, ticket);
            } else {
               String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
               sendError("FAILED to close ticket {0,number,0} by push of {1}", ticketNumber, shortid);
               } 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 close", e);
         LOGGER.error("Can't scan for changes to reference or close", e);
      } finally {
         rw.reset();
      }
@@ -866,66 +1084,9 @@
      return mergedTickets.values();
   }
   /**
    * Try to identify a ticket id from the commit.
    *
    * @param commit
    * @param parseMessage
    * @return a ticket id or 0
    */
   private long identifyTicket(RevCommit commit, boolean parseMessage) {
      // try lookup by change ref
      Map<AnyObjectId, Set<Ref>> map = getRepository().getAllRefsByPeeledObjectId();
      Set<Ref> refs = map.get(commit.getId());
      if (!ArrayUtils.isEmpty(refs)) {
         for (Ref ref : refs) {
            long number = PatchsetCommand.getTicketNumber(ref.getName());
            if (number > 0) {
               return number;
            }
         }
      }
      if (parseMessage) {
         // parse commit message looking for fixes/closes #n
         Pattern p = Pattern.compile("(?:fixes|closes)[\\s-]+#?(\\d+)", Pattern.CASE_INSENSITIVE);
         Matcher m = p.matcher(commit.getFullMessage());
         while (m.find()) {
            String val = m.group();
            return Long.parseLong(val);
         }
      }
      return 0L;
   }
   private int countCommits(String baseId, String tipId) {
      int count = 0;
      RevWalk walk = getRevWalk();
      walk.reset();
      walk.sort(RevSort.TOPO);
      walk.sort(RevSort.REVERSE, true);
      try {
         RevCommit tip = walk.parseCommit(getRepository().resolve(tipId));
         RevCommit base = walk.parseCommit(getRepository().resolve(baseId));
         walk.markStart(tip);
         walk.markUninteresting(base);
         for (;;) {
            RevCommit c = walk.next();
            if (c == null) {
               break;
            }
            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);
         return 0;
      } finally {
         walk.release();
      }
      return count;
   }
   /**
    * Creates a new patchset with metadata.
@@ -935,7 +1096,7 @@
    * @param tip
    */
   private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
      int totalCommits = countCommits(mergeBase, tip);
      int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);
      Patchset newPatchset = new Patchset();
      newPatchset.tip = tip;
@@ -1034,7 +1195,7 @@
      return newPatchset;
   }
   private RefUpdate updateRef(String ref, ObjectId newId) {
   private RefUpdate updateRef(String ref, ObjectId newId, PatchsetType type) {
      ObjectId ticketRefId = ObjectId.zeroId();
      try {
         ticketRefId = getRepository().resolve(ref);
@@ -1045,7 +1206,17 @@
      try {
         RefUpdate ru = getRepository().updateRef(ref,  false);
         ru.setRefLogIdent(getRefLogIdent());
         ru.setForceUpdate(true);
         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());
@@ -1130,11 +1301,24 @@
      if (ticket != null) {
         ticketNotifier.queueMailing(ticket);
         // update the reflog with the merge
         if (oldRef != null) {
            ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
                  ObjectId.fromString(mergeResult.sha), oldRef.getName());
            RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
            cmd.setResult(Result.OK);
            List<ReceiveCommand> 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 {