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