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