Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
commit | author | age
75bca8 1 /*
234933 2  * Copyright 2013 gitblit.com.
75bca8 3  *
JM 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
234933 18 import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
75bca8 19 import groovy.lang.Binding;
JM 20 import groovy.util.GroovyScriptEngine;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.text.MessageFormat;
c2188a 25 import java.util.ArrayList;
75bca8 26 import java.util.Collection;
c2188a 27 import java.util.LinkedHashMap;
75bca8 28 import java.util.LinkedHashSet;
JM 29 import java.util.List;
c2188a 30 import java.util.Map;
75bca8 31 import java.util.Set;
c2188a 32 import java.util.SortedMap;
PM 33 import java.util.TreeMap;
234933 34 import java.util.concurrent.TimeUnit;
75bca8 35
c2188a 36 import org.eclipse.jgit.lib.AnyObjectId;
234933 37 import org.eclipse.jgit.lib.BatchRefUpdate;
JM 38 import org.eclipse.jgit.lib.NullProgressMonitor;
c2188a 39 import org.eclipse.jgit.lib.ObjectId;
75bca8 40 import org.eclipse.jgit.lib.PersonIdent;
234933 41 import org.eclipse.jgit.lib.ProgressMonitor;
c2188a 42 import org.eclipse.jgit.lib.Ref;
PM 43 import org.eclipse.jgit.lib.RefUpdate;
234933 44 import org.eclipse.jgit.lib.Repository;
75bca8 45 import org.eclipse.jgit.revwalk.RevCommit;
c2188a 46 import org.eclipse.jgit.revwalk.RevWalk;
75bca8 47 import org.eclipse.jgit.transport.PostReceiveHook;
JM 48 import org.eclipse.jgit.transport.PreReceiveHook;
49 import org.eclipse.jgit.transport.ReceiveCommand;
50 import org.eclipse.jgit.transport.ReceiveCommand.Result;
51 import org.eclipse.jgit.transport.ReceivePack;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
f01bc5 55 import com.gitblit.Constants;
75bca8 56 import com.gitblit.Constants.AccessRestrictionType;
db4f6b 57 import com.gitblit.IStoredSettings;
75bca8 58 import com.gitblit.Keys;
JM 59 import com.gitblit.client.Translation;
819efd 60 import com.gitblit.extensions.ReceiveHook;
3a9e76 61 import com.gitblit.manager.IGitblit;
75bca8 62 import com.gitblit.models.RepositoryModel;
c2188a 63 import com.gitblit.models.TicketModel;
75bca8 64 import com.gitblit.models.UserModel;
c2188a 65 import com.gitblit.models.TicketModel.Change;
PM 66 import com.gitblit.models.TicketModel.Field;
67 import com.gitblit.models.TicketModel.Patchset;
68 import com.gitblit.models.TicketModel.Status;
69 import com.gitblit.models.TicketModel.TicketAction;
70 import com.gitblit.models.TicketModel.TicketLink;
5e3521 71 import com.gitblit.tickets.BranchTicketService;
c2188a 72 import com.gitblit.tickets.ITicketService;
PM 73 import com.gitblit.tickets.TicketNotifier;
978820 74 import com.gitblit.utils.ArrayUtils;
75bca8 75 import com.gitblit.utils.ClientLogger;
f01bc5 76 import com.gitblit.utils.CommitCache;
75bca8 77 import com.gitblit.utils.JGitUtils;
ff7d3c 78 import com.gitblit.utils.RefLogUtils;
75bca8 79 import com.gitblit.utils.StringUtils;
c2188a 80 import com.google.common.collect.Lists;
75bca8 81
234933 82
75bca8 83 /**
234933 84  * GitblitReceivePack processes receive commands.  It also executes Groovy pre-
JM 85  * and post- receive hooks.
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
75bca8 95  * @author James Moger
234933 96  *
75bca8 97  */
234933 98 public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, PostReceiveHook {
75bca8 99
234933 100     private static final Logger LOGGER = LoggerFactory.getLogger(GitblitReceivePack.class);
75bca8 101
234933 102     protected final RepositoryModel repository;
JM 103
104     protected final UserModel user;
105
106     protected final File groovyDir;
75bca8 107
JM 108     protected String gitblitUrl;
109
234933 110     protected GroovyScriptEngine gse;
75bca8 111
d1cb28 112     protected final IStoredSettings settings;
cacf8b 113
d1cb28 114     protected final IGitblit gitblit;
c2188a 115     
PM 116     protected final ITicketService ticketService;
117
118     protected final TicketNotifier ticketNotifier;
119     
cacf8b 120
JM 121     public GitblitReceivePack(
3a9e76 122             IGitblit gitblit,
cacf8b 123             Repository db,
JM 124             RepositoryModel repository,
125             UserModel user) {
126
234933 127         super(db);
325396 128         this.settings = gitblit.getSettings();
JM 129         this.gitblit = gitblit;
234933 130         this.repository = repository;
f8f6aa 131         this.user = user;
325396 132         this.groovyDir = gitblit.getHooksFolder();
75bca8 133         try {
JM 134             // set Grape root
325396 135             File grapeRoot = gitblit.getGrapesFolder();
75bca8 136             grapeRoot.mkdirs();
JM 137             System.setProperty("grape.root", grapeRoot.getAbsolutePath());
234933 138             this.gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());
75bca8 139         } catch (IOException e) {
JM 140         }
234933 141
c2188a 142         if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) {
PM 143             this.ticketService = gitblit.getTicketService();
144             this.ticketNotifier = this.ticketService.createNotifier();
145         } else {
146             this.ticketService = null;
147             this.ticketNotifier = null;
148         }
149         
234933 150         // set advanced ref permissions
JM 151         setAllowCreates(user.canCreateRef(repository));
152         setAllowDeletes(user.canDeleteRef(repository));
153         setAllowNonFastForwards(user.canRewindRef(repository));
699e71 154
7baf2e 155         int maxObjectSz = settings.getInteger(Keys.git.maxObjectSizeLimit, -1);
JM 156         if (maxObjectSz >= 0) {
157             setMaxObjectSizeLimit(maxObjectSz);
158         }
159         int maxPackSz = settings.getInteger(Keys.git.maxPackSizeLimit, -1);
160         if (maxPackSz >= 0) {
161             setMaxPackSizeLimit(maxPackSz);
162         }
163         setCheckReceivedObjects(settings.getBoolean(Keys.git.checkReceivedObjects, true));
164         setCheckReferencedObjectsAreReachable(settings.getBoolean(Keys.git.checkReferencedObjectsAreReachable, true));
165
234933 166         // setup pre and post receive hook
JM 167         setPreReceiveHook(this);
168         setPostReceiveHook(this);
75bca8 169     }
JM 170
171     /**
a66312 172      * Returns true if the user is permitted to apply the receive commands to
JM 173      * the repository.
174      *
175      * @param commands
176      * @return true if the user may push these commands
177      */
178     protected boolean canPush(Collection<ReceiveCommand> commands) {
179         // TODO Consider supporting branch permissions here (issue-36)
180         // Not sure if that should be Gerrit-style, refs/meta/config, or
181         // gitolite-style, permissions in users.conf
182         //
183         // How could commands be empty?
184         //
185         // Because a subclass, like PatchsetReceivePack, filters receive
186         // commands before this method is called.  This makes it possible for
187         // this method to test an empty list.  In this case, we assume that the
188         // subclass receive pack properly enforces push restrictions. for the
189         // ref.
190         //
191         // The empty test is not explicitly required, it's written here to
192         // clarify special-case behavior.
193
194         return commands.isEmpty() ? true : user.canPush(repository);
195     }
196
197     /**
75bca8 198      * Instrumentation point where the incoming push event has been parsed,
JM 199      * validated, objects created BUT refs have not been updated. You might
200      * use this to enforce a branch-write permissions model.
201      */
202     @Override
203     public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
819efd 204
JM 205         if (commands.size() == 0) {
206             // no receive commands to process
207             // this can happen if receive pack subclasses intercept and filter
208             // the commands
209             LOGGER.debug("skipping pre-receive processing, no refs created, updated, or removed");
210             return;
211         }
234933 212
c44dd0 213         if (repository.isMirror) {
JM 214             // repository is a mirror
215             for (ReceiveCommand cmd : commands) {
216                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name);
217             }
218             return;
219         }
220
75bca8 221         if (repository.isFrozen) {
JM 222             // repository is frozen/readonly
223             for (ReceiveCommand cmd : commands) {
234933 224                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is frozen!", repository.name);
75bca8 225             }
JM 226             return;
227         }
234933 228
75bca8 229         if (!repository.isBare) {
JM 230             // repository has a working copy
231             for (ReceiveCommand cmd : commands) {
234933 232                 sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name);
75bca8 233             }
JM 234             return;
235         }
236
a66312 237         if (!canPush(commands)) {
75bca8 238             // user does not have push permissions
JM 239             for (ReceiveCommand cmd : commands) {
234933 240                 sendRejection(cmd, "User \"{0}\" does not have push permissions for \"{1}\"!", user.username, repository.name);
75bca8 241             }
JM 242             return;
243         }
244
245         if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
246             // enforce committer verification
247             if (StringUtils.isEmpty(user.emailAddress)) {
f19b78 248                 // reject the push because the pushing account does not have an email address
JM 249                 for (ReceiveCommand cmd : commands) {
250                     sendRejection(cmd, "Sorry, the account \"{0}\" does not have an email address set for committer verification!", user.username);
251                 }
252                 return;
75bca8 253             }
JM 254
234933 255             // Optionally enforce that the committer of first parent chain
75bca8 256             // match the account being used to push the commits.
234933 257             //
75bca8 258             // This requires all merge commits are executed with the "--no-ff"
JM 259             // option to force a merge commit even if fast-forward is possible.
234933 260             // This ensures that the chain first parents has the commit
75bca8 261             // identity of the merging user.
JM 262             boolean allRejected = false;
263             for (ReceiveCommand cmd : commands) {
234933 264                 String firstParent = null;
75bca8 265                 try {
JM 266                     List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
267                     for (RevCommit commit : commits) {
234933 268
JM 269                         if (firstParent != null) {
270                             if (!commit.getName().equals(firstParent)) {
d1dc77 271                                 // ignore: commit is right-descendant of a merge
JM 272                                 continue;
273                             }
274                         }
234933 275
d1dc77 276                         // update expected next commit id
JM 277                         if (commit.getParentCount() == 0) {
234933 278                             firstParent = null;
d1dc77 279                         } else {
234933 280                             firstParent = commit.getParents()[0].getId().getName();
d1dc77 281                         }
234933 282
75bca8 283                         PersonIdent committer = commit.getCommitterIdent();
JM 284                         if (!user.is(committer.getName(), committer.getEmailAddress())) {
f19b78 285                             // verification failed
JM 286                             String reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>",
287                                     commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);
234933 288                             LOGGER.warn(reason);
75bca8 289                             cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
JM 290                             allRejected &= true;
291                             break;
292                         } else {
293                             allRejected = false;
294                         }
295                     }
296                 } catch (Exception e) {
234933 297                     LOGGER.error("Failed to verify commits were made by pushing user", e);
75bca8 298                 }
JM 299             }
300
301             if (allRejected) {
302                 // all ref updates rejected, abort
303                 return;
304             }
305         }
234933 306
f01bc5 307         for (ReceiveCommand cmd : commands) {
JM 308             String ref = cmd.getRefName();
309             if (ref.startsWith(Constants.R_HEADS)) {
310                 switch (cmd.getType()) {
311                 case UPDATE_NONFASTFORWARD:
312                 case DELETE:
d1cb28 313                     // reset branch commit cache on REWIND and DELETE
f01bc5 314                     CommitCache.instance().clear(repository.name, ref);
JM 315                     break;
316                 default:
317                     break;
318                 }
5e3521 319             } else if (ref.equals(BranchTicketService.BRANCH)) {
JM 320                 // ensure pushing user is an administrator OR an owner
321                 // i.e. prevent ticket tampering
322                 boolean permitted = user.canAdmin() || repository.isOwner(user.username);
323                 if (!permitted) {
324                     sendRejection(cmd, "{0} is not permitted to push to {1}", user.username, ref);
325                 }
326             } else if (ref.startsWith(Constants.R_FOR)) {
327                 // prevent accidental push to refs/for
328                 sendRejection(cmd, "{0} is not configured to receive patchsets", repository.name);
f01bc5 329             }
JM 330         }
75bca8 331
819efd 332         // call pre-receive plugins
JM 333         for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
334             try {
335                 hook.onPreReceive(this, commands);
336             } catch (Exception e) {
337                 LOGGER.error("Failed to execute extension", e);
338             }
339         }
340
75bca8 341         Set<String> scripts = new LinkedHashSet<String>();
325396 342         scripts.addAll(gitblit.getPreReceiveScriptsInherited(repository));
978820 343         if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
JM 344             scripts.addAll(repository.preReceiveScripts);
345         }
234933 346         runGroovy(commands, scripts);
75bca8 347         for (ReceiveCommand cmd : commands) {
JM 348             if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
234933 349                 LOGGER.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
75bca8 350                         .getName(), cmd.getResult(), cmd.getMessage()));
JM 351             }
352         }
353     }
354
355     /**
356      * Instrumentation point where the incoming push has been applied to the
357      * repository. This is the point where we would trigger a Jenkins build
358      * or send an email.
359      */
360     @Override
361     public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
362         if (commands.size() == 0) {
819efd 363             LOGGER.debug("skipping post-receive processing, no refs created, updated, or removed");
75bca8 364             return;
JM 365         }
366
d62d88 367         logRefChange(commands);
JM 368         updateIncrementalPushTags(commands);
369         updateGitblitRefLog(commands);
370
371         // check for updates pushed to the BranchTicketService branch
372         // if the BranchTicketService is active it will reindex, as appropriate
373         for (ReceiveCommand cmd : commands) {
374             if (Result.OK.equals(cmd.getResult())
375                     && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
376                 rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
377             }
378         }
379
380         // call post-receive plugins
381         for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
382             try {
383                 hook.onPostReceive(this, commands);
384             } catch (Exception e) {
385                 LOGGER.error("Failed to execute extension", e);
386             }
387         }
388
389         // run Groovy hook scripts
390         Set<String> scripts = new LinkedHashSet<String>();
391         scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
392         if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
393             scripts.addAll(repository.postReceiveScripts);
394         }
395         runGroovy(commands, scripts);
396     }
397
398     /**
399      * Log the ref changes in the container log.
400      *
401      * @param commands
402      */
403     protected void logRefChange(Collection<ReceiveCommand> commands) {
ce07c4 404         boolean isRefCreationOrDeletion = false;
JM 405
75bca8 406         // log ref changes
JM 407         for (ReceiveCommand cmd : commands) {
234933 408
75bca8 409             if (Result.OK.equals(cmd.getResult())) {
JM 410                 // add some logging for important ref changes
411                 switch (cmd.getType()) {
412                 case DELETE:
234933 413                     LOGGER.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
ce07c4 414                     isRefCreationOrDeletion = true;
75bca8 415                     break;
JM 416                 case CREATE:
234933 417                     LOGGER.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
ce07c4 418                     isRefCreationOrDeletion = true;
75bca8 419                     break;
JM 420                 case UPDATE:
234933 421                     LOGGER.info(MessageFormat.format("{0} UPDATED {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
75bca8 422                     break;
JM 423                 case UPDATE_NONFASTFORWARD:
234933 424                     LOGGER.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
75bca8 425                     break;
JM 426                 default:
427                     break;
428                 }
429             }
430         }
431
ce07c4 432         if (isRefCreationOrDeletion) {
JM 433             gitblit.resetRepositoryCache(repository.name);
434         }
d62d88 435     }
ce07c4 436
d62d88 437     /**
JM 438      * Optionally update the incremental push tags.
439      *
440      * @param commands
441      */
442     protected void updateIncrementalPushTags(Collection<ReceiveCommand> commands) {
443         if (!repository.useIncrementalPushTags) {
444             return;
75bca8 445         }
JM 446
d62d88 447         // tag each pushed branch tip
JM 448         String emailAddress = user.emailAddress == null ? getRefLogIdent().getEmailAddress() : user.emailAddress;
449         PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
988334 450
e462bb 451         for (ReceiveCommand cmd : commands) {
d62d88 452             if (!cmd.getRefName().startsWith(Constants.R_HEADS)) {
JM 453                 // only tag branch ref changes
454                 continue;
455             }
456
457             if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
458                     && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
459                 String objectId = cmd.getNewId().getName();
460                 String branch = cmd.getRefName().substring(Constants.R_HEADS.length());
461                 // get translation based on the server's locale setting
462                 String template = Translation.get("gb.incrementalPushTagMessage");
463                 String msg = MessageFormat.format(template, branch);
464                 String prefix;
465                 if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
466                     prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
467                 } else {
468                     prefix = repository.incrementalPushTagPrefix;
469                 }
470
471                 JGitUtils.createIncrementalRevisionTag(
472                         getRepository(),
473                         objectId,
474                         userIdent,
475                         prefix,
476                         "0",
477                         msg);
e462bb 478             }
JM 479         }
d62d88 480     }
db4f6b 481
d62d88 482     /**
JM 483      * Update Gitblit's internal reflog.
484      *
485      * @param commands
486      */
487     protected void updateGitblitRefLog(Collection<ReceiveCommand> commands) {
488         try {
489             RefLogUtils.updateRefLog(user, getRepository(), commands);
490             LOGGER.debug(MessageFormat.format("{0} reflog updated", repository.name));
491         } catch (Exception e) {
492             LOGGER.error(MessageFormat.format("Failed to update {0} reflog", repository.name), e);
819efd 493         }
234933 494     }
JM 495
496     /** Execute commands to update references. */
497     @Override
498     protected void executeCommands() {
499         List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
500         if (toApply.isEmpty()) {
501             return;
502         }
503
504         ProgressMonitor updating = NullProgressMonitor.INSTANCE;
505         boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
506         if (sideBand) {
507             SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
508             pm.setDelayStart(250, TimeUnit.MILLISECONDS);
509             updating = pm;
510         }
511
512         BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
513         batch.setAllowNonFastForwards(isAllowNonFastForwards());
514         batch.setRefLogIdent(getRefLogIdent());
515         batch.setRefLogMessage("push", true);
516
517         for (ReceiveCommand cmd : toApply) {
518             if (Result.NOT_ATTEMPTED != cmd.getResult()) {
519                 // Already rejected by the core receive process.
520                 continue;
521             }
522             batch.addCommand(cmd);
523         }
524
525         if (!batch.getCommands().isEmpty()) {
526             try {
527                 batch.execute(getRevWalk(), updating);
528             } catch (IOException err) {
529                 for (ReceiveCommand cmd : toApply) {
530                     if (cmd.getResult() == Result.NOT_ATTEMPTED) {
531                         sendRejection(cmd, "lock error: {0}", err.getMessage());
532                     }
533                 }
534             }
535         }
c2188a 536         
PM 537         //
538         // if there are ref update receive commands that were
539         // successfully processed and there is an active ticket service for the repository
540         // then process any referenced tickets
541         //
542         if (ticketService != null) {
543             List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
544             if (!allUpdates.isEmpty()) {
545                 int ticketsProcessed = 0;
546                 for (ReceiveCommand cmd : allUpdates) {
547                     switch (cmd.getType()) {
548                     case CREATE:
549                     case UPDATE:
550                         if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
551                             Collection<TicketModel> tickets = processReferencedTickets(cmd);
552                             ticketsProcessed += tickets.size();
553                             for (TicketModel ticket : tickets) {
554                                 ticketNotifier.queueMailing(ticket);
555                             }
556                         }
557                         break;
558                         
559                     case UPDATE_NONFASTFORWARD:
560                         if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
561                             String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
562                             List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
563                             for (TicketLink link : deletedRefs) {
564                                 link.isDelete = true;
565                             }
566                             Change deletion = new Change(user.username);
567                             deletion.pendingLinks = deletedRefs;
568                             ticketService.updateTicket(repository, 0, deletion);
569                             
570                             Collection<TicketModel> tickets = processReferencedTickets(cmd);
571                             ticketsProcessed += tickets.size();
572                             for (TicketModel ticket : tickets) {
573                                 ticketNotifier.queueMailing(ticket);
574                             }
575                         }
576                         break;
577                     case DELETE:
578                         //Identify if the branch has been merged 
579                         SortedMap<Integer, String> bases =  new TreeMap<Integer, String>();
580                         try {
581                             ObjectId dObj = cmd.getOldId();
582                             Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values();
583                             for (Ref ref : tips) {
584                                 ObjectId iObj = ref.getObjectId();
585                                 String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj);
586                                 if (mergeBase != null) {
587                                     int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name());
588                                     bases.put(d, mergeBase);
589                                     //All commits have been merged into some other branch
590                                     if (d == 0) {
591                                         break;
592                                     }
593                                 }
594                             }
595                             
596                             if (bases.isEmpty()) {
597                                 //TODO: Handle orphan branch case
598                             } else {
599                                 if (bases.firstKey() > 0) {
600                                     //Delete references from the remaining commits that haven't been merged
601                                     String mergeBase = bases.get(bases.firstKey());
602                                     List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(),
603                                             settings, mergeBase, dObj.name());
604                                     
605                                     for (TicketLink link : deletedRefs) {
606                                         link.isDelete = true;
607                                     }
608                                     Change deletion = new Change(user.username);
609                                     deletion.pendingLinks = deletedRefs;
610                                     ticketService.updateTicket(repository, 0, deletion);
611                                 }
612                             }
613                             
614                         } catch (IOException e) {
615                             LOGGER.error(null, e);
616                         }
617                         break;
618                         
619                     default:
620                         break;
621                     }
622                 }
623     
624                 if (ticketsProcessed == 1) {
625                     sendInfo("1 ticket updated");
626                 } else if (ticketsProcessed > 1) {
627                     sendInfo("{0} tickets updated", ticketsProcessed);
628                 }
629             }
630     
631             // reset the ticket caches for the repository
632             ticketService.resetCaches(repository);
633         }
234933 634     }
JM 635
636     protected void setGitblitUrl(String url) {
637         this.gitblitUrl = url;
638     }
639
819efd 640     public void sendRejection(final ReceiveCommand cmd, final String why, Object... objects) {
234933 641         String text;
JM 642         if (ArrayUtils.isEmpty(objects)) {
643             text = why;
644         } else {
645             text = MessageFormat.format(why, objects);
646         }
647         cmd.setResult(Result.REJECTED_OTHER_REASON, text);
648         LOGGER.error(text + " (" + user.username + ")");
649     }
650
819efd 651     public void sendHeader(String msg, Object... objects) {
d1cb28 652         sendInfo("--> ", msg, objects);
4360b3 653     }
JM 654
819efd 655     public void sendInfo(String msg, Object... objects) {
d1cb28 656         sendInfo("    ", msg, objects);
4360b3 657     }
JM 658
819efd 659     private void sendInfo(String prefix, String msg, Object... objects) {
234933 660         String text;
JM 661         if (ArrayUtils.isEmpty(objects)) {
662             text = msg;
4360b3 663             super.sendMessage(prefix + msg);
234933 664         } else {
JM 665             text = MessageFormat.format(msg, objects);
4360b3 666             super.sendMessage(prefix + text);
234933 667         }
da9941 668         if (!StringUtils.isEmpty(msg)) {
JM 669             LOGGER.info(text + " (" + user.username + ")");
670         }
234933 671     }
JM 672
819efd 673     public void sendError(String msg, Object... objects) {
234933 674         String text;
JM 675         if (ArrayUtils.isEmpty(objects)) {
676             text = msg;
677             super.sendError(msg);
678         } else {
679             text = MessageFormat.format(msg, objects);
680             super.sendError(text);
681         }
da9941 682         if (!StringUtils.isEmpty(msg)) {
JM 683             LOGGER.error(text + " (" + user.username + ")");
684         }
75bca8 685     }
JM 686
687     /**
688      * Runs the specified Groovy hook scripts.
234933 689      *
75bca8 690      * @param repository
JM 691      * @param user
692      * @param commands
693      * @param scripts
694      */
4360b3 695     private void runGroovy(Collection<ReceiveCommand> commands, Set<String> scripts) {
75bca8 696         if (scripts == null || scripts.size() == 0) {
JM 697             // no Groovy scripts to execute
698             return;
699         }
700
701         Binding binding = new Binding();
325396 702         binding.setVariable("gitblit", gitblit);
75bca8 703         binding.setVariable("repository", repository);
234933 704         binding.setVariable("receivePack", this);
75bca8 705         binding.setVariable("user", user);
JM 706         binding.setVariable("commands", commands);
707         binding.setVariable("url", gitblitUrl);
234933 708         binding.setVariable("logger", LOGGER);
JM 709         binding.setVariable("clientLogger", new ClientLogger(this));
75bca8 710         for (String script : scripts) {
JM 711             if (StringUtils.isEmpty(script)) {
712                 continue;
713             }
714             // allow script to be specified without .groovy extension
715             // this is easier to read in the settings
716             File file = new File(groovyDir, script);
717             if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
718                 file = new File(groovyDir, script + ".groovy");
719                 if (file.exists()) {
720                     script = file.getName();
721                 }
722             }
723             try {
724                 Object result = gse.run(script, binding);
725                 if (result instanceof Boolean) {
726                     if (!((Boolean) result)) {
234933 727                         LOGGER.error(MessageFormat.format(
75bca8 728                                 "Groovy script {0} has failed!  Hook scripts aborted.", script));
JM 729                         break;
730                     }
731                 }
732             } catch (Exception e) {
234933 733                 LOGGER.error(
75bca8 734                         MessageFormat.format("Failed to execute Groovy script {0}", script), e);
JM 735             }
736         }
737     }
819efd 738
JM 739     public IGitblit getGitblit() {
740         return gitblit;
741     }
742
743     public RepositoryModel getRepositoryModel() {
744         return repository;
745     }
746
747     public UserModel getUserModel() {
748         return user;
749     }
c2188a 750     
PM 751     /**
752      * Automatically closes open tickets and adds references to tickets if made in the commit message.
753      *
754      * @param cmd
755      */
756     private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
757         Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();
758
759         final RevWalk rw = getRevWalk();
760         try {
761             rw.reset();
762             rw.markStart(rw.parseCommit(cmd.getNewId()));
763             if (!ObjectId.zeroId().equals(cmd.getOldId())) {
764                 rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
765             }
766
767             RevCommit c;
768             while ((c = rw.next()) != null) {
769                 rw.parseBody(c);
770                 List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
771                 if (ticketLinks == null) {
772                     continue;
773                 }
774
775                 for (TicketLink link : ticketLinks) {
776                     
777                     TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
778                     if (ticket == null) {
779                         continue;
780                     }
781                     
782                     Change change = null;
783                     String commitSha = c.getName();
784                     String branchName = Repository.shortenRefName(cmd.getRefName());
785                     
786                     switch (link.action) {
787                         case Commit: {
788                             //A commit can reference a ticket in any branch even if the ticket is closed.
789                             //This allows developers to identify and communicate related issues
790                             change = new Change(user.username);
791                             change.referenceCommit(commitSha);
792                         } break;
793                         
794                         case Close: {
795                             // As this isn't a patchset theres no merging taking place when closing a ticket
796                             if (ticket.isClosed()) {
797                                 continue;
798                             }
799                             
800                             change = new Change(user.username);
801                             change.setField(Field.status, Status.Fixed);
802                             
803                             if (StringUtils.isEmpty(ticket.responsible)) {
804                                 // unassigned tickets are assigned to the closer
805                                 change.setField(Field.responsible, user.username);
806                             }
807                         }
808                         
809                         default: {
810                             //No action
811                         } break;
812                     }
813                     
814                     if (change != null) {
815                         ticket = ticketService.updateTicket(repository, ticket.number, change);
816                     }
817     
818                     if (ticket != null) {
819                         sendInfo("");
820                         sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
821
822                         switch (link.action) {
823                             case Commit: {
824                                 sendInfo("referenced by push of {0} to {1}", commitSha, branchName);
825                                 changedTickets.put(ticket.number, ticket);
826                             } break;
827
828                             case Close: {
829                                 sendInfo("closed by push of {0} to {1}", commitSha, branchName);
830                                 changedTickets.put(ticket.number, ticket);
831                             } break;
832
833                             default: { }
834                         }
835
836                         sendInfo(ticketService.getTicketUrl(ticket));
837                         sendInfo("");
838                     } else {
839                         switch (link.action) {
840                             case Commit: {
841                                 sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);
842                             } break;
843                             
844                             case Close: {
845                                 sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha);    
846                             } break;
847                             
848                             default: { }
849                         }
850                     }
851                 }
852             }
853                 
854         } catch (IOException e) {
855             LOGGER.error("Can't scan for changes to reference or close", e);
856         } finally {
857             rw.reset();
858         }
859
860         return changedTickets.values();
861     }
234933 862 }