Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2013 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.git;
17
18 import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
19
20 import java.io.IOException;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.LinkedHashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.concurrent.TimeUnit;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.eclipse.jgit.lib.BatchRefUpdate;
34 import org.eclipse.jgit.lib.NullProgressMonitor;
35 import org.eclipse.jgit.lib.ObjectId;
36 import org.eclipse.jgit.lib.PersonIdent;
37 import org.eclipse.jgit.lib.ProgressMonitor;
38 import org.eclipse.jgit.lib.Ref;
39 import org.eclipse.jgit.lib.RefUpdate;
40 import org.eclipse.jgit.lib.Repository;
41 import org.eclipse.jgit.revwalk.RevCommit;
42 import org.eclipse.jgit.revwalk.RevSort;
43 import org.eclipse.jgit.revwalk.RevWalk;
44 import org.eclipse.jgit.transport.ReceiveCommand;
45 import org.eclipse.jgit.transport.ReceiveCommand.Result;
46 import org.eclipse.jgit.transport.ReceiveCommand.Type;
47 import org.eclipse.jgit.transport.ReceivePack;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import com.gitblit.Constants;
52 import com.gitblit.Keys;
aa89be 53 import com.gitblit.extensions.PatchsetHook;
5e3521 54 import com.gitblit.manager.IGitblit;
JM 55 import com.gitblit.models.RepositoryModel;
56 import com.gitblit.models.TicketModel;
57 import com.gitblit.models.TicketModel.Change;
58 import com.gitblit.models.TicketModel.Field;
59 import com.gitblit.models.TicketModel.Patchset;
60 import com.gitblit.models.TicketModel.PatchsetType;
61 import com.gitblit.models.TicketModel.Status;
c2188a 62 import com.gitblit.models.TicketModel.TicketAction;
PM 63 import com.gitblit.models.TicketModel.TicketLink;
5e3521 64 import com.gitblit.models.UserModel;
988334 65 import com.gitblit.tickets.BranchTicketService;
5e3521 66 import com.gitblit.tickets.ITicketService;
JM 67 import com.gitblit.tickets.TicketMilestone;
68 import com.gitblit.tickets.TicketNotifier;
69 import com.gitblit.utils.ArrayUtils;
70 import com.gitblit.utils.DiffUtils;
71 import com.gitblit.utils.DiffUtils.DiffStat;
72 import com.gitblit.utils.JGitUtils;
73 import com.gitblit.utils.JGitUtils.MergeResult;
74 import com.gitblit.utils.JGitUtils.MergeStatus;
75 import com.gitblit.utils.RefLogUtils;
76 import com.gitblit.utils.StringUtils;
2e73ef 77 import com.google.common.collect.Lists;
5e3521 78
JM 79
80 /**
81  * PatchsetReceivePack processes receive commands and allows for creating, updating,
82  * and closing Gitblit tickets.  It also executes Groovy pre- and post- receive
83  * hooks.
84  *
85  * The patchset mechanism defined in this class is based on the ReceiveCommits class
86  * from the Gerrit code review server.
87  *
88  * The general execution flow is:
89  * <ol>
90  *    <li>onPreReceive()</li>
91  *    <li>executeCommands()</li>
92  *    <li>onPostReceive()</li>
93  * </ol>
94  *
95  * @author Android Open Source Project
96  * @author James Moger
97  *
98  */
99 public class PatchsetReceivePack extends GitblitReceivePack {
100
101     protected static final List<String> MAGIC_REFS = Arrays.asList(Constants.R_FOR, Constants.R_TICKET);
102
103     protected static final Pattern NEW_PATCHSET =
104             Pattern.compile("^refs/tickets/(?:[0-9a-zA-Z][0-9a-zA-Z]/)?([1-9][0-9]*)(?:/new)?$");
105
106     private static final Logger LOGGER = LoggerFactory.getLogger(PatchsetReceivePack.class);
107
108     protected final ITicketService ticketService;
109
110     protected final TicketNotifier ticketNotifier;
111
988334 112     private boolean requireMergeablePatchset;
5e3521 113
JM 114     public PatchsetReceivePack(IGitblit gitblit, Repository db, RepositoryModel repository, UserModel user) {
115         super(gitblit, db, repository, user);
116         this.ticketService = gitblit.getTicketService();
117         this.ticketNotifier = ticketService.createNotifier();
118     }
119
120     /** Returns the patchset ref root from the ref */
121     private String getPatchsetRef(String refName) {
122         for (String patchRef : MAGIC_REFS) {
123             if (refName.startsWith(patchRef)) {
124                 return patchRef;
125             }
126         }
127         return null;
128     }
129
130     /** Checks if the supplied ref name is a patchset ref */
131     private boolean isPatchsetRef(String refName) {
132         return !StringUtils.isEmpty(getPatchsetRef(refName));
133     }
134
135     /** Checks if the supplied ref name is a change ref */
136     private boolean isTicketRef(String refName) {
137         return refName.startsWith(Constants.R_TICKETS_PATCHSETS);
138     }
139
140     /** Extracts the integration branch from the ref name */
141     private String getIntegrationBranch(String refName) {
142         String patchsetRef = getPatchsetRef(refName);
143         String branch = refName.substring(patchsetRef.length());
144         if (branch.indexOf('%') > -1) {
145             branch = branch.substring(0, branch.indexOf('%'));
146         }
147
148         String defaultBranch = "master";
149         try {
150             defaultBranch = getRepository().getBranch();
151         } catch (Exception e) {
152             LOGGER.error("failed to determine default branch for " + repository.name, e);
153         }
154
600d43 155         if (!StringUtils.isEmpty(getRepositoryModel().mergeTo)) {
JM 156             // repository settings specifies a default integration branch
157             defaultBranch = Repository.shortenRefName(getRepositoryModel().mergeTo);
158         }
159
5e3521 160         long ticketId = 0L;
JM 161         try {
162             ticketId = Long.parseLong(branch);
163         } catch (Exception e) {
164             // not a number
165         }
166         if (ticketId > 0 || branch.equalsIgnoreCase("default") || branch.equalsIgnoreCase("new")) {
167             return defaultBranch;
168         }
169         return branch;
170     }
171
172     /** Extracts the ticket id from the ref name */
173     private long getTicketId(String refName) {
120794 174         if (refName.indexOf('%') > -1) {
JM 175             refName = refName.substring(0, refName.indexOf('%'));
176         }
5e3521 177         if (refName.startsWith(Constants.R_FOR)) {
JM 178             String ref = refName.substring(Constants.R_FOR.length());
179             try {
180                 return Long.parseLong(ref);
181             } catch (Exception e) {
182                 // not a number
183             }
184         } else if (refName.startsWith(Constants.R_TICKET) ||
185                 refName.startsWith(Constants.R_TICKETS_PATCHSETS)) {
186             return PatchsetCommand.getTicketNumber(refName);
187         }
188         return 0L;
189     }
190
191     /** Returns true if the ref namespace exists */
192     private boolean hasRefNamespace(String ref) {
193         Map<String, Ref> blockingFors;
194         try {
195             blockingFors = getRepository().getRefDatabase().getRefs(ref);
196         } catch (IOException err) {
197             sendError("Cannot scan refs in {0}", repository.name);
198             LOGGER.error("Error!", err);
199             return true;
200         }
201         if (!blockingFors.isEmpty()) {
202             sendError("{0} needs the following refs removed to receive patchsets: {1}",
203                     repository.name, blockingFors.keySet());
204             return true;
205         }
206         return false;
207     }
208
209     /** Removes change ref receive commands */
210     private List<ReceiveCommand> excludeTicketCommands(Collection<ReceiveCommand> commands) {
211         List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
212         for (ReceiveCommand cmd : commands) {
213             if (!isTicketRef(cmd.getRefName())) {
214                 // this is not a ticket ref update
215                 filtered.add(cmd);
216             }
217         }
218         return filtered;
219     }
220
221     /** Removes patchset receive commands for pre- and post- hook integrations */
222     private List<ReceiveCommand> excludePatchsetCommands(Collection<ReceiveCommand> commands) {
223         List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
224         for (ReceiveCommand cmd : commands) {
225             if (!isPatchsetRef(cmd.getRefName())) {
226                 // this is a non-patchset ref update
227                 filtered.add(cmd);
228             }
229         }
230         return filtered;
231     }
232
233     /**    Process receive commands EXCEPT for Patchset commands. */
234     @Override
235     public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
236         Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
237         super.onPreReceive(rp, filtered);
238     }
239
240     /**    Process receive commands EXCEPT for Patchset commands. */
241     @Override
242     public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
243         Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
244         super.onPostReceive(rp, filtered);
245
246         // send all queued ticket notifications after processing all patchsets
247         ticketNotifier.sendAll();
248     }
249
250     @Override
251     protected void validateCommands() {
252         // workaround for JGit's awful scoping choices
253         //
254         // set the patchset refs to OK to bypass checks in the super implementation
255         for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
256             if (isPatchsetRef(cmd.getRefName())) {
257                 if (cmd.getType() == ReceiveCommand.Type.CREATE) {
258                     cmd.setResult(Result.OK);
259                 }
260             }
261         }
262
263         super.validateCommands();
264     }
265
266     /** Execute commands to update references. */
267     @Override
268     protected void executeCommands() {
988334 269         // we process patchsets unless the user is pushing something special
JM 270         boolean processPatchsets = true;
271         for (ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
272             if (ticketService instanceof BranchTicketService
273                     && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
274                 // the user is pushing an update to the BranchTicketService data
275                 processPatchsets = false;
276             }
277         }
278
5e3521 279         // workaround for JGit's awful scoping choices
JM 280         //
281         // reset the patchset refs to NOT_ATTEMPTED (see validateCommands)
282         for (ReceiveCommand cmd : filterCommands(Result.OK)) {
283             if (isPatchsetRef(cmd.getRefName())) {
284                 cmd.setResult(Result.NOT_ATTEMPTED);
988334 285             } else if (ticketService instanceof BranchTicketService
JM 286                     && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
287                 // the user is pushing an update to the BranchTicketService data
288                 processPatchsets = false;
5e3521 289             }
JM 290         }
291
292         List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
293         if (toApply.isEmpty()) {
294             return;
295         }
296
297         ProgressMonitor updating = NullProgressMonitor.INSTANCE;
298         boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
299         if (sideBand) {
300             SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
301             pm.setDelayStart(250, TimeUnit.MILLISECONDS);
302             updating = pm;
303         }
304
305         BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
306         batch.setAllowNonFastForwards(isAllowNonFastForwards());
307         batch.setRefLogIdent(getRefLogIdent());
308         batch.setRefLogMessage("push", true);
309
310         ReceiveCommand patchsetRefCmd = null;
311         PatchsetCommand patchsetCmd = null;
312         for (ReceiveCommand cmd : toApply) {
313             if (Result.NOT_ATTEMPTED != cmd.getResult()) {
314                 // Already rejected by the core receive process.
315                 continue;
316             }
317
988334 318             if (isPatchsetRef(cmd.getRefName()) && processPatchsets) {
f254ee 319
5e3521 320                 if (ticketService == null) {
JM 321                     sendRejection(cmd, "Sorry, the ticket service is unavailable and can not accept patchsets at this time.");
322                     continue;
323                 }
324
325                 if (!ticketService.isReady()) {
326                     sendRejection(cmd, "Sorry, the ticket service can not accept patchsets at this time.");
327                     continue;
328                 }
329
330                 if (UserModel.ANONYMOUS.equals(user)) {
331                     // server allows anonymous pushes, but anonymous patchset
332                     // contributions are prohibited by design
333                     sendRejection(cmd, "Sorry, anonymous patchset contributions are prohibited.");
334                     continue;
335                 }
336
337                 final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
338                 if (m.matches()) {
339                     // prohibit pushing directly to a patchset ref
340                     long id = getTicketId(cmd.getRefName());
341                     sendError("You may not directly push directly to a patchset ref!");
342                     sendError("Instead, please push to one the following:");
343                     sendError(" - {0}{1,number,0}", Constants.R_FOR, id);
344                     sendError(" - {0}{1,number,0}", Constants.R_TICKET, id);
345                     sendRejection(cmd, "protected ref");
346                     continue;
347                 }
348
349                 if (hasRefNamespace(Constants.R_FOR)) {
350                     // the refs/for/ namespace exists and it must not
351                     LOGGER.error("{} already has refs in the {} namespace",
352                             repository.name, Constants.R_FOR);
353                     sendRejection(cmd, "Sorry, a repository administrator will have to remove the {} namespace", Constants.R_FOR);
354                     continue;
355                 }
356
f254ee 357                 if (cmd.getNewId().equals(ObjectId.zeroId())) {
JM 358                     // ref deletion request
359                     if (cmd.getRefName().startsWith(Constants.R_TICKET)) {
360                         if (user.canDeleteRef(repository)) {
361                             batch.addCommand(cmd);
362                         } else {
363                             sendRejection(cmd, "Sorry, you do not have permission to delete {}", cmd.getRefName());
364                         }
365                     } else {
366                         sendRejection(cmd, "Sorry, you can not delete {}", cmd.getRefName());
367                     }
368                     continue;
369                 }
370
5e3521 371                 if (patchsetRefCmd != null) {
JM 372                     sendRejection(cmd, "You may only push one patchset at a time.");
373                     continue;
374                 }
375
cecc39 376                 LOGGER.info(MessageFormat.format("Verifying {0} push ref \"{1}\" received from {2}",
JM 377                         repository.name, cmd.getRefName(), user.username));
378
5e3521 379                 // responsible verification
JM 380                 String responsible = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.RESPONSIBLE);
381                 if (!StringUtils.isEmpty(responsible)) {
382                     UserModel assignee = gitblit.getUserModel(responsible);
383                     if (assignee == null) {
384                         // no account by this name
385                         sendRejection(cmd, "{0} can not be assigned any tickets because there is no user account by that name", responsible);
386                         continue;
387                     } else if (!assignee.canPush(repository)) {
388                         // account does not have RW permissions
389                         sendRejection(cmd, "{0} ({1}) can not be assigned any tickets because the user does not have RW permissions for {2}",
390                                 assignee.getDisplayName(), assignee.username, repository.name);
391                         continue;
392                     }
393                 }
394
395                 // milestone verification
396                 String milestone = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.MILESTONE);
397                 if (!StringUtils.isEmpty(milestone)) {
398                     TicketMilestone milestoneModel = ticketService.getMilestone(repository, milestone);
399                     if (milestoneModel == null) {
400                         // milestone does not exist
401                         sendRejection(cmd, "Sorry, \"{0}\" is not a valid milestone!", milestone);
402                         continue;
403                     }
404                 }
405
406                 // watcher verification
407                 List<String> watchers = PatchsetCommand.getOptions(cmd, PatchsetCommand.WATCH);
408                 if (!ArrayUtils.isEmpty(watchers)) {
cecc39 409                     boolean verified = true;
5e3521 410                     for (String watcher : watchers) {
JM 411                         UserModel user = gitblit.getUserModel(watcher);
412                         if (user == null) {
413                             // watcher does not exist
414                             sendRejection(cmd, "Sorry, \"{0}\" is not a valid username for the watch list!", watcher);
cecc39 415                             verified = false;
JM 416                             break;
5e3521 417                         }
cecc39 418                     }
JM 419                     if (!verified) {
420                         continue;
5e3521 421                     }
JM 422                 }
423
424                 patchsetRefCmd = cmd;
425                 patchsetCmd = preparePatchset(cmd);
426                 if (patchsetCmd != null) {
427                     batch.addCommand(patchsetCmd);
428                 }
429                 continue;
430             }
431
432             batch.addCommand(cmd);
433         }
434
435         if (!batch.getCommands().isEmpty()) {
436             try {
437                 batch.execute(getRevWalk(), updating);
438             } catch (IOException err) {
439                 for (ReceiveCommand cmd : toApply) {
440                     if (cmd.getResult() == Result.NOT_ATTEMPTED) {
441                         sendRejection(cmd, "lock error: {0}", err.getMessage());
988334 442                         LOGGER.error(MessageFormat.format("failed to lock {0}:{1}",
JM 443                                 repository.name, cmd.getRefName()), err);
5e3521 444                     }
JM 445                 }
446             }
447         }
448
449         //
450         // set the results into the patchset ref receive command
451         //
452         if (patchsetRefCmd != null && patchsetCmd != null) {
453             if (!patchsetCmd.getResult().equals(Result.OK)) {
454                 // patchset command failed!
455                 LOGGER.error(patchsetCmd.getType() + " " + patchsetCmd.getRefName()
456                         + " " + patchsetCmd.getResult());
457                 patchsetRefCmd.setResult(patchsetCmd.getResult(), patchsetCmd.getMessage());
458             } else {
459                 // all patchset commands were applied
460                 patchsetRefCmd.setResult(Result.OK);
461
462                 // update the ticket branch ref
2dcec5 463                 RefUpdate ru = updateRef(
JM 464                         patchsetCmd.getTicketBranch(),
465                         patchsetCmd.getNewId(),
466                         patchsetCmd.getPatchsetType());
5e3521 467                 updateReflog(ru);
JM 468
469                 TicketModel ticket = processPatchset(patchsetCmd);
470                 if (ticket != null) {
471                     ticketNotifier.queueMailing(ticket);
472                 }
473             }
474         }
475
476         //
477         // if there are standard ref update receive commands that were
478         // successfully processed, process referenced tickets, if any
479         //
480         List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
481         List<ReceiveCommand> refUpdates = excludePatchsetCommands(allUpdates);
482         List<ReceiveCommand> stdUpdates = excludeTicketCommands(refUpdates);
483         if (!stdUpdates.isEmpty()) {
484             int ticketsProcessed = 0;
485             for (ReceiveCommand cmd : stdUpdates) {
486                 switch (cmd.getType()) {
487                 case CREATE:
488                 case UPDATE:
c2188a 489                     if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
PM 490                         Collection<TicketModel> tickets = processReferencedTickets(cmd);
491                         ticketsProcessed += tickets.size();
492                         for (TicketModel ticket : tickets) {
493                             ticketNotifier.queueMailing(ticket);
494                         }
495                     }
496                     break;
497                     
5e3521 498                 case UPDATE_NONFASTFORWARD:
e462bb 499                     if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
c2188a 500                         String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
PM 501                         List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
502                         for (TicketLink link : deletedRefs) {
503                             link.isDelete = true;
504                         }
505                         Change deletion = new Change(user.username);
506                         deletion.pendingLinks = deletedRefs;
507                         ticketService.updateTicket(repository, 0, deletion);
508
509                         Collection<TicketModel> tickets = processReferencedTickets(cmd);
e462bb 510                         ticketsProcessed += tickets.size();
JM 511                         for (TicketModel ticket : tickets) {
512                             ticketNotifier.queueMailing(ticket);
513                         }
5e3521 514                     }
JM 515                     break;
516                 default:
517                     break;
518                 }
519             }
520
521             if (ticketsProcessed == 1) {
522                 sendInfo("1 ticket updated");
523             } else if (ticketsProcessed > 1) {
524                 sendInfo("{0} tickets updated", ticketsProcessed);
525             }
526         }
527
528         // reset the ticket caches for the repository
529         ticketService.resetCaches(repository);
530     }
531
532     /**
533      * Prepares a patchset command.
534      *
535      * @param cmd
536      * @return the patchset command
537      */
538     private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
539         String branch = getIntegrationBranch(cmd.getRefName());
540         long number = getTicketId(cmd.getRefName());
541
542         TicketModel ticket = null;
543         if (number > 0 && ticketService.hasTicket(repository, number)) {
544             ticket = ticketService.getTicket(repository, number);
545         }
546
547         if (ticket == null) {
548             if (number > 0) {
549                 // requested ticket does not exist
550                 sendError("Sorry, {0} does not have ticket {1,number,0}!", repository.name, number);
551                 sendRejection(cmd, "Invalid ticket number");
552                 return null;
553             }
554         } else {
555             if (ticket.isMerged()) {
556                 // ticket already merged & resolved
557                 Change mergeChange = null;
558                 for (Change change : ticket.changes) {
559                     if (change.isMerge()) {
560                         mergeChange = change;
561                         break;
562                     }
563                 }
80c3ef 564                 if (mergeChange != null) {
JM 565                     sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
5e3521 566                         mergeChange.author, mergeChange.patchset, number, ticket.mergeTo);
80c3ef 567                 }
5e3521 568                 sendRejection(cmd, "Ticket {0,number,0} already resolved", number);
JM 569                 return null;
570             } else if (!StringUtils.isEmpty(ticket.mergeTo)) {
571                 // ticket specifies integration branch
572                 branch = ticket.mergeTo;
573             }
574         }
575
576         final int shortCommitIdLen = settings.getInteger(Keys.web.shortCommitIdLength, 6);
577         final String shortTipId = cmd.getNewId().getName().substring(0, shortCommitIdLen);
578         final RevCommit tipCommit = JGitUtils.getCommit(getRepository(), cmd.getNewId().getName());
579         final String forBranch = branch;
580         RevCommit mergeBase = null;
581         Ref forBranchRef = getAdvertisedRefs().get(Constants.R_HEADS + forBranch);
582         if (forBranchRef == null || forBranchRef.getObjectId() == null) {
583             // unknown integration branch
584             sendError("Sorry, there is no integration branch named ''{0}''.", forBranch);
585             sendRejection(cmd, "Invalid integration branch specified");
586             return null;
587         } else {
588             // determine the merge base for the patchset on the integration branch
589             String base = JGitUtils.getMergeBase(getRepository(), forBranchRef.getObjectId(), tipCommit.getId());
590             if (StringUtils.isEmpty(base)) {
591                 sendError("");
592                 sendError("There is no common ancestry between {0} and {1}.", forBranch, shortTipId);
593                 sendError("Please reconsider your proposed integration branch, {0}.", forBranch);
594                 sendError("");
595                 sendRejection(cmd, "no merge base for patchset and {0}", forBranch);
596                 return null;
597             }
598             mergeBase = JGitUtils.getCommit(getRepository(), base);
599         }
600
601         // ensure that the patchset can be cleanly merged right now
602         MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch);
603         switch (status) {
604         case ALREADY_MERGED:
605             sendError("");
606             sendError("You have already merged this patchset.", forBranch);
607             sendError("");
608             sendRejection(cmd, "everything up-to-date");
609             return null;
610         case MERGEABLE:
611             break;
612         default:
988334 613             if (ticket == null || requireMergeablePatchset) {
5e3521 614                 sendError("");
JM 615                 sendError("Your patchset can not be cleanly merged into {0}.", forBranch);
616                 sendError("Please rebase your patchset and push again.");
617                 sendError("NOTE:", number);
618                 sendError("You should push your rebase to refs/for/{0,number,0}", number);
619                 sendError("");
620                 sendError("  git push origin HEAD:refs/for/{0,number,0}", number);
621                 sendError("");
622                 sendRejection(cmd, "patchset not mergeable");
623                 return null;
624             }
625         }
c2188a 626         
5e3521 627         // check to see if this commit is already linked to a ticket
c2188a 628         if (ticket != null && 
PM 629                 JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {
630             sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);
5e3521 631             sendRejection(cmd, "everything up-to-date");
JM 632             return null;
633         }
c2188a 634         
PM 635         List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
636         
5e3521 637         PatchsetCommand psCmd;
JM 638         if (ticket == null) {
639             /*
640              *  NEW TICKET
641              */
642             Patchset patchset = newPatchset(null, mergeBase.getName(), tipCommit.getName());
643
644             int minLength = 10;
645             int maxLength = 100;
646             String minTitle = MessageFormat.format("  minimum length of a title is {0} characters.", minLength);
647             String maxTitle = MessageFormat.format("  maximum length of a title is {0} characters.", maxLength);
648
649             if (patchset.commits > 1) {
650                 sendError("");
2e73ef 651                 sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!",
JM 652                         forBranch, patchset.commits);
5e3521 653                 sendError("");
2e73ef 654                 // display an ellipsized log of the commits being pushed
JM 655                 RevWalk walk = getRevWalk();
656                 walk.reset();
657                 walk.sort(RevSort.TOPO);
658                 int boundary = 3;
659                 int count = 0;
660                 try {
661                     walk.markStart(tipCommit);
662                     walk.markUninteresting(mergeBase);
663
664                     for (;;) {
665
666                         RevCommit c = walk.next();
667                         if (c == null) {
668                             break;
669                         }
670
671                         if (count < boundary || count >= (patchset.commits - boundary)) {
672
673                             walk.parseBody(c);
674                             sendError("   {0}  {1}", c.getName().substring(0, shortCommitIdLen),
675                                 StringUtils.trimString(c.getShortMessage(), 60));
676
677                         } else if (count == boundary) {
678
679                             sendError("   ... more commits ...");
680
681                         }
682
683                         count++;
684                     }
685
686                 } catch (IOException e) {
687                     // Should never happen, the core receive process would have
688                     // identified the missing object earlier before we got control.
689                     LOGGER.error("failed to get commit count", e);
690                 } finally {
a1cee6 691                     walk.close();
2e73ef 692                 }
JM 693
5e3521 694                 sendError("");
2e73ef 695                 sendError("Possible Solutions:");
JM 696                 sendError("");
697                 int solution = 1;
698                 String forSpec = cmd.getRefName().substring(Constants.R_FOR.length());
699                 if (forSpec.equals("default") || forSpec.equals("new")) {
700                     try {
701                         // determine other possible integration targets
702                         List<String> bases = Lists.newArrayList();
703                         for (Ref ref : getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values()) {
704                             if (!ref.getName().startsWith(Constants.R_TICKET)
705                                     && !ref.getName().equals(forBranchRef.getName())) {
706                                 if (JGitUtils.isMergedInto(getRepository(), ref.getObjectId(), tipCommit)) {
707                                     bases.add(Repository.shortenRefName(ref.getName()));
708                                 }
709                             }
710                         }
711
712                         if (!bases.isEmpty()) {
713
714                             if (bases.size() == 1) {
715                                 // suggest possible integration targets
716                                 String base = bases.get(0);
717                                 sendError("{0}. Propose this change for the ''{1}'' branch.", solution++, base);
718                                 sendError("");
719                                 sendError("   git push origin HEAD:refs/for/{0}", base);
720                                 sendError("   pt propose {0}", base);
721                                 sendError("");
722                             } else {
723                                 // suggest possible integration targets
724                                 sendError("{0}. Propose this change for a different branch.", solution++);
725                                 sendError("");
726                                 for (String base : bases) {
727                                     sendError("   git push origin HEAD:refs/for/{0}", base);
728                                     sendError("   pt propose {0}", base);
729                                     sendError("");
730                                 }
731                             }
732
733                         }
734                     } catch (IOException e) {
735                         LOGGER.error(null, e);
736                     }
737                 }
738                 sendError("{0}. Squash your changes into a single commit with a meaningful message.", solution++);
739                 sendError("");
740                 sendError("{0}. Open a ticket for your changes and then push your {1} commits to the ticket.",
741                         solution++, patchset.commits);
742                 sendError("");
743                 sendError("   git push origin HEAD:refs/for/{id}");
744                 sendError("   pt propose {id}");
745                 sendError("");
746                 sendRejection(cmd, "too many commits");
5e3521 747                 return null;
JM 748             }
749
750             // require a reasonable title/subject
751             String title = tipCommit.getFullMessage().trim().split("\n")[0];
752             if (title.length() < minLength) {
753                 // reject, title too short
754                 sendError("");
755                 sendError("Please supply a longer title in your commit message!");
756                 sendError("");
757                 sendError(minTitle);
758                 sendError(maxTitle);
759                 sendError("");
760                 sendRejection(cmd, "ticket title is too short [{0}/{1}]", title.length(), maxLength);
761                 return null;
762             }
763             if (title.length() > maxLength) {
764                 // reject, title too long
765                 sendError("");
766                 sendError("Please supply a more concise title in your commit message!");
767                 sendError("");
768                 sendError(minTitle);
769                 sendError(maxTitle);
770                 sendError("");
771                 sendRejection(cmd, "ticket title is too long [{0}/{1}]", title.length(), maxLength);
772                 return null;
773             }
774
775             // assign new id
776             long ticketId = ticketService.assignNewId(repository);
777
778             // create the patchset command
779             psCmd = new PatchsetCommand(user.username, patchset);
780             psCmd.newTicket(tipCommit, forBranch, ticketId, cmd.getRefName());
781         } else {
782             /*
783              *  EXISTING TICKET
784              */
785             Patchset patchset = newPatchset(ticket, mergeBase.getName(), tipCommit.getName());
786             psCmd = new PatchsetCommand(user.username, patchset);
787             psCmd.updateTicket(tipCommit, forBranch, ticket, cmd.getRefName());
788         }
789
790         // confirm user can push the patchset
791         boolean pushPermitted = ticket == null
792                 || !ticket.hasPatchsets()
793                 || ticket.isAuthor(user.username)
794                 || ticket.isPatchsetAuthor(user.username)
795                 || ticket.isResponsible(user.username)
796                 || user.canPush(repository);
797
798         switch (psCmd.getPatchsetType()) {
799         case Proposal:
800             // proposals (first patchset) are always acceptable
801             break;
802         case FastForward:
803             // patchset updates must be permitted
804             if (!pushPermitted) {
805                 // reject
806                 sendError("");
807                 sendError("To push a patchset to this ticket one of the following must be true:");
808                 sendError("  1. you created the ticket");
809                 sendError("  2. you created the first patchset");
810                 sendError("  3. you are specified as responsible for the ticket");
6d298c 811                 sendError("  4. you have push (RW) permissions to {0}", repository.name);
5e3521 812                 sendError("");
JM 813                 sendRejection(cmd, "not permitted to push to ticket {0,number,0}", ticket.number);
814                 return null;
815             }
816             break;
817         default:
818             // non-fast-forward push
819             if (!pushPermitted) {
820                 // reject
821                 sendRejection(cmd, "non-fast-forward ({0})", psCmd.getPatchsetType());
822                 return null;
823             }
824             break;
825         }
c2188a 826
PM 827         Change change = psCmd.getChange();
828         change.pendingLinks = ticketLinks;
829
5e3521 830         return psCmd;
JM 831     }
832
833     /**
834      * Creates or updates an ticket with the specified patchset.
835      *
836      * @param cmd
837      * @return a ticket if the creation or update was successful
838      */
839     private TicketModel processPatchset(PatchsetCommand cmd) {
840         Change change = cmd.getChange();
841
842         if (cmd.isNewTicket()) {
843             // create the ticket object
844             TicketModel ticket = ticketService.createTicket(repository, cmd.getTicketId(), change);
845             if (ticket != null) {
846                 sendInfo("");
847                 sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
848                 sendInfo("created proposal ticket from patchset");
849                 sendInfo(ticketService.getTicketUrl(ticket));
850                 sendInfo("");
851
852                 // log the new patch ref
853                 RefLogUtils.updateRefLog(user, getRepository(),
854                         Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
855
aa89be 856                 // call any patchset hooks
JM 857                 for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
858                     try {
859                         hook.onNewPatchset(ticket);
860                     } catch (Exception e) {
861                         LOGGER.error("Failed to execute extension", e);
862                     }
863                 }
864
5e3521 865                 return ticket;
JM 866             } else {
867                 sendError("FAILED to create ticket");
868             }
869         } else {
870             // update an existing ticket
871             TicketModel ticket = ticketService.updateTicket(repository, cmd.getTicketId(), change);
872             if (ticket != null) {
873                 sendInfo("");
874                 sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
875                 if (change.patchset.rev == 1) {
876                     // new patchset
877                     sendInfo("uploaded patchset {0} ({1})", change.patchset.number, change.patchset.type.toString());
878                 } else {
879                     // updated patchset
880                     sendInfo("added {0} {1} to patchset {2}",
881                             change.patchset.added,
882                             change.patchset.added == 1 ? "commit" : "commits",
883                             change.patchset.number);
884                 }
885                 sendInfo(ticketService.getTicketUrl(ticket));
886                 sendInfo("");
887
888                 // log the new patchset ref
889                 RefLogUtils.updateRefLog(user, getRepository(),
890                     Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
aa89be 891
JM 892                 // call any patchset hooks
893                 final boolean isNewPatchset = change.patchset.rev == 1;
894                 for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
895                     try {
896                         if (isNewPatchset) {
897                             hook.onNewPatchset(ticket);
898                         } else {
899                             hook.onUpdatePatchset(ticket);
900                         }
901                     } catch (Exception e) {
902                         LOGGER.error("Failed to execute extension", e);
903                     }
904                 }
5e3521 905
JM 906                 // return the updated ticket
907                 return ticket;
908             } else {
909                 sendError("FAILED to upload {0} for ticket {1,number,0}", change.patchset, cmd.getTicketId());
910             }
911         }
912
913         return null;
914     }
915
916     /**
917      * Automatically closes open tickets that have been merged to their integration
c2188a 918      * branch by a client and adds references to tickets if made in the commit message.
5e3521 919      *
JM 920      * @param cmd
921      */
c2188a 922     private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
5e3521 923         Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
JM 924         final RevWalk rw = getRevWalk();
925         try {
926             rw.reset();
927             rw.markStart(rw.parseCommit(cmd.getNewId()));
928             if (!ObjectId.zeroId().equals(cmd.getOldId())) {
929                 rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
930             }
931
932             RevCommit c;
933             while ((c = rw.next()) != null) {
934                 rw.parseBody(c);
c2188a 935                 List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
PM 936                 if (ticketLinks == null) {
5e3521 937                     continue;
JM 938                 }
939
c2188a 940                 for (TicketLink link : ticketLinks) {
PM 941                     
942                     if (mergedTickets.containsKey(link.targetTicketId)) {
5e3521 943                         continue;
JM 944                     }
c2188a 945     
PM 946                     TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
947                     if (ticket == null) {
948                         continue;
949                     }
950                     String integrationBranch;
951                     if (StringUtils.isEmpty(ticket.mergeTo)) {
952                         // unspecified integration branch
953                         integrationBranch = null;
954                     } else {
955                         // specified integration branch
956                         integrationBranch = Constants.R_HEADS + ticket.mergeTo;
957                     }
958     
959                     Change change;
960                     Patchset patchset = null;
961                     String mergeSha = c.getName();
962                     String mergeTo = Repository.shortenRefName(cmd.getRefName());
5e3521 963
c2188a 964                     if (link.action == TicketAction.Commit) {
PM 965                         //A commit can reference a ticket in any branch even if the ticket is closed.
966                         //This allows developers to identify and communicate related issues
967                         change = new Change(user.username);
968                         change.referenceCommit(mergeSha);
969                     } else {
970                         // ticket must be open and, if specified, the ref must match the integration branch
971                         if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
972                             continue;
973                         }
974     
975                         String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
976                         boolean knownPatchset = false;
977                         Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
978                         if (refs != null) {
979                             for (Ref ref : refs) {
980                                 if (ref.getName().startsWith(baseRef)) {
981                                     knownPatchset = true;
982                                     break;
983                                 }
984                             }
985                         }
986     
987                         if (knownPatchset) {
988                             // identify merged patchset by the patchset tip
989                             for (Patchset ps : ticket.getPatchsets()) {
990                                 if (ps.tip.equals(mergeSha)) {
991                                     patchset = ps;
992                                     break;
993                                 }
994                             }
995     
996                             if (patchset == null) {
997                                 // should not happen - unless ticket has been hacked
998                                 sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
999                                         mergeSha, ticket.number);
1000                                 continue;
1001                             }
1002     
1003                             // create a new change
1004                             change = new Change(user.username);
1005                         } else {
1006                             // new patchset pushed by user
1007                             String base = cmd.getOldId().getName();
1008                             patchset = newPatchset(ticket, base, mergeSha);
1009                             PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
1010                             psCmd.updateTicket(c, mergeTo, ticket, null);
1011     
1012                             // create a ticket patchset ref
1013                             updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
1014                             RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
1015                             updateReflog(ru);
1016     
1017                             // create a change from the patchset command
1018                             change = psCmd.getChange();
1019                         }
1020     
1021                         // set the common change data about the merge
1022                         change.setField(Field.status, Status.Merged);
1023                         change.setField(Field.mergeSha, mergeSha);
1024                         change.setField(Field.mergeTo, mergeTo);
1025     
1026                         if (StringUtils.isEmpty(ticket.responsible)) {
1027                             // unassigned tickets are assigned to the closer
1028                             change.setField(Field.responsible, user.username);
1029                         }
1030                     }
1031     
1032                     ticket = ticketService.updateTicket(repository, ticket.number, change);
1033     
1034                     if (ticket != null) {
1035                         sendInfo("");
1036                         sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
5e3521 1037
c2188a 1038                         switch (link.action) {
PM 1039                             case Commit: {
1040                                 sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
1041                             }
1042                             break;
5e3521 1043
c2188a 1044                             case Close: {
PM 1045                                 sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
1046                                 mergedTickets.put(ticket.number, ticket);    
1047                             }
1048                             break;
5e3521 1049
c2188a 1050                             default: {
PM 1051                                 
1052                             }
1053                         }
5e3521 1054
c2188a 1055                         sendInfo(ticketService.getTicketUrl(ticket));
PM 1056                         sendInfo("");
5e3521 1057
c2188a 1058                     } else {
PM 1059                         String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
1060                         
1061                         switch (link.action) {
1062                             case Commit: {
1063                                 sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
1064                             }
1065                             break;
1066                             case Close: {
1067                                 sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);    
1068                             } break;
1069                             
1070                             default: {
1071                                 
1072                             }
1073                         }
1074                     }
5e3521 1075                 }
JM 1076             }
c2188a 1077                 
5e3521 1078         } catch (IOException e) {
c2188a 1079             LOGGER.error("Can't scan for changes to reference or close", e);
5e3521 1080         } finally {
JM 1081             rw.reset();
1082         }
1083
1084         return mergedTickets.values();
1085     }
1086
c2188a 1087     
5e3521 1088
c2188a 1089     
5e3521 1090
JM 1091     /**
1092      * Creates a new patchset with metadata.
1093      *
1094      * @param ticket
1095      * @param mergeBase
1096      * @param tip
1097      */
1098     private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
c2188a 1099         int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);
5e3521 1100
JM 1101         Patchset newPatchset = new Patchset();
1102         newPatchset.tip = tip;
1103         newPatchset.base = mergeBase;
1104         newPatchset.commits = totalCommits;
1105
1106         Patchset currPatchset = ticket == null ? null : ticket.getCurrentPatchset();
1107         if (currPatchset == null) {
1108             /*
1109              * PROPOSAL PATCHSET
1110              * patchset 1, rev 1
1111              */
1112             newPatchset.number = 1;
1113             newPatchset.rev = 1;
1114             newPatchset.type = PatchsetType.Proposal;
1115
1116             // diffstat from merge base
1117             DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
1118             newPatchset.insertions = diffStat.getInsertions();
1119             newPatchset.deletions = diffStat.getDeletions();
1120         } else {
1121             /*
1122              * PATCHSET UPDATE
1123              */
1124             int added = totalCommits - currPatchset.commits;
1125             boolean ff = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, tip);
1126             boolean squash = added < 0;
1127             boolean rebase = !currPatchset.base.equals(mergeBase);
1128
1129             // determine type, number and rev of the patchset
1130             if (ff) {
1131                 /*
1132                  * FAST-FORWARD
1133                  * patchset number preserved, rev incremented
1134                  */
1135
1136                 boolean merged = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, ticket.mergeTo);
1137                 if (merged) {
1138                     // current patchset was already merged
1139                     // new patchset, mark as rebase
1140                     newPatchset.type = PatchsetType.Rebase;
1141                     newPatchset.number = currPatchset.number + 1;
1142                     newPatchset.rev = 1;
1143
1144                     // diffstat from parent
1145                     DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
1146                     newPatchset.insertions = diffStat.getInsertions();
1147                     newPatchset.deletions = diffStat.getDeletions();
1148                 } else {
1149                     // FF update to patchset
1150                     newPatchset.type = PatchsetType.FastForward;
1151                     newPatchset.number = currPatchset.number;
1152                     newPatchset.rev = currPatchset.rev + 1;
1153                     newPatchset.parent = currPatchset.tip;
1154
1155                     // diffstat from parent
1156                     DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), currPatchset.tip, tip);
1157                     newPatchset.insertions = diffStat.getInsertions();
1158                     newPatchset.deletions = diffStat.getDeletions();
1159                 }
1160             } else {
1161                 /*
1162                  * NON-FAST-FORWARD
1163                  * new patchset, rev 1
1164                  */
1165                 if (rebase && squash) {
1166                     newPatchset.type = PatchsetType.Rebase_Squash;
1167                     newPatchset.number = currPatchset.number + 1;
1168                     newPatchset.rev = 1;
1169                 } else if (squash) {
1170                     newPatchset.type = PatchsetType.Squash;
1171                     newPatchset.number = currPatchset.number + 1;
1172                     newPatchset.rev = 1;
1173                 } else if (rebase) {
1174                     newPatchset.type = PatchsetType.Rebase;
1175                     newPatchset.number = currPatchset.number + 1;
1176                     newPatchset.rev = 1;
1177                 } else {
1178                     newPatchset.type = PatchsetType.Amend;
1179                     newPatchset.number = currPatchset.number + 1;
1180                     newPatchset.rev = 1;
1181                 }
1182
1183                 // diffstat from merge base
1184                 DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
1185                 newPatchset.insertions = diffStat.getInsertions();
1186                 newPatchset.deletions = diffStat.getDeletions();
1187             }
1188
1189             if (added > 0) {
1190                 // ignore squash (negative add)
1191                 newPatchset.added = added;
1192             }
1193         }
1194
1195         return newPatchset;
1196     }
1197
2dcec5 1198     private RefUpdate updateRef(String ref, ObjectId newId, PatchsetType type) {
5e3521 1199         ObjectId ticketRefId = ObjectId.zeroId();
JM 1200         try {
1201             ticketRefId = getRepository().resolve(ref);
1202         } catch (Exception e) {
1203             // ignore
1204         }
1205
1206         try {
1207             RefUpdate ru = getRepository().updateRef(ref,  false);
1208             ru.setRefLogIdent(getRefLogIdent());
2dcec5 1209             switch (type) {
JM 1210             case Amend:
1211             case Rebase:
1212             case Rebase_Squash:
1213             case Squash:
1214                 ru.setForceUpdate(true);
1215                 break;
1216             default:
1217                 break;
1218             }
1219
5e3521 1220             ru.setExpectedOldObjectId(ticketRefId);
JM 1221             ru.setNewObjectId(newId);
1222             RefUpdate.Result result = ru.update(getRevWalk());
1223             if (result == RefUpdate.Result.LOCK_FAILURE) {
1224                 sendError("Failed to obtain lock when updating {0}:{1}", repository.name, ref);
1225                 sendError("Perhaps an administrator should remove {0}/{1}.lock?", getRepository().getDirectory(), ref);
1226                 return null;
1227             }
1228             return ru;
1229         } catch (IOException e) {
1230             LOGGER.error("failed to update ref " + ref, e);
1231             sendError("There was an error updating ref {0}:{1}", repository.name, ref);
1232         }
1233         return null;
1234     }
1235
1236     private void updateReflog(RefUpdate ru) {
1237         if (ru == null) {
1238             return;
1239         }
1240
1241         ReceiveCommand.Type type = null;
1242         switch (ru.getResult()) {
1243         case NEW:
1244             type = Type.CREATE;
1245             break;
1246         case FAST_FORWARD:
1247             type = Type.UPDATE;
1248             break;
1249         case FORCED:
1250             type = Type.UPDATE_NONFASTFORWARD;
1251             break;
1252         default:
1253             LOGGER.error(MessageFormat.format("unexpected ref update type {0} for {1}",
1254                     ru.getResult(), ru.getName()));
1255             return;
1256         }
1257         ReceiveCommand cmd = new ReceiveCommand(ru.getOldObjectId(), ru.getNewObjectId(), ru.getName(), type);
1258         RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
1259     }
1260
1261     /**
1262      * Merge the specified patchset to the integration branch.
1263      *
1264      * @param ticket
1265      * @param patchset
1266      * @return true, if successful
1267      */
1268     public MergeStatus merge(TicketModel ticket) {
1269         PersonIdent committer = new PersonIdent(user.getDisplayName(), StringUtils.isEmpty(user.emailAddress) ? (user.username + "@gitblit") : user.emailAddress);
1270         Patchset patchset = ticket.getCurrentPatchset();
1271         String message = MessageFormat.format("Merged #{0,number,0} \"{1}\"", ticket.number, ticket.title);
1272         Ref oldRef = null;
1273         try {
1274             oldRef = getRepository().getRef(ticket.mergeTo);
1275         } catch (IOException e) {
1276             LOGGER.error("failed to get ref for " + ticket.mergeTo, e);
1277         }
1278         MergeResult mergeResult = JGitUtils.merge(
1279                 getRepository(),
1280                 patchset.tip,
1281                 ticket.mergeTo,
1282                 committer,
1283                 message);
1284
1285         if (StringUtils.isEmpty(mergeResult.sha)) {
1286             LOGGER.error("FAILED to merge {} to {} ({})", new Object [] { patchset, ticket.mergeTo, mergeResult.status.name() });
1287             return mergeResult.status;
1288         }
1289         Change change = new Change(user.username);
1290         change.setField(Field.status, Status.Merged);
1291         change.setField(Field.mergeSha, mergeResult.sha);
1292         change.setField(Field.mergeTo, ticket.mergeTo);
1293
1294         if (StringUtils.isEmpty(ticket.responsible)) {
1295             // unassigned tickets are assigned to the closer
1296             change.setField(Field.responsible, user.username);
1297         }
1298
1299         long ticketId = ticket.number;
1300         ticket = ticketService.updateTicket(repository, ticket.number, change);
1301         if (ticket != null) {
1302             ticketNotifier.queueMailing(ticket);
1303
1304             if (oldRef != null) {
1305                 ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
1306                         ObjectId.fromString(mergeResult.sha), oldRef.getName());
d62d88 1307                 cmd.setResult(Result.OK);
JM 1308                 List<ReceiveCommand> commands = Arrays.asList(cmd);
1309
1310                 logRefChange(commands);
1311                 updateIncrementalPushTags(commands);
1312                 updateGitblitRefLog(commands);
5e3521 1313             }
aa89be 1314
JM 1315             // call patchset hooks
1316             for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
1317                 try {
1318                     hook.onMergePatchset(ticket);
1319                 } catch (Exception e) {
1320                     LOGGER.error("Failed to execute extension", e);
1321                 }
1322             }
5e3521 1323             return mergeResult.status;
JM 1324         } else {
1325             LOGGER.error("FAILED to resolve ticket {} by merge from web ui", ticketId);
1326         }
1327         return mergeResult.status;
1328     }
1329
1330     public void sendAll() {
1331         ticketNotifier.sendAll();
1332     }
1333 }