2222import java .io .File ;
2323import java .io .IOException ;
2424import java .text .MessageFormat ;
25+ import java .util .ArrayList ;
2526import java .util .Collection ;
27+ import java .util .LinkedHashMap ;
2628import java .util .LinkedHashSet ;
2729import java .util .List ;
30+ import java .util .Map ;
2831import java .util .Set ;
32+ import java .util .SortedMap ;
33+ import java .util .TreeMap ;
2934import java .util .concurrent .TimeUnit ;
3035
36+ import org .eclipse .jgit .lib .AnyObjectId ;
3137import org .eclipse .jgit .lib .BatchRefUpdate ;
3238import org .eclipse .jgit .lib .NullProgressMonitor ;
39+ import org .eclipse .jgit .lib .ObjectId ;
3340import org .eclipse .jgit .lib .PersonIdent ;
3441import org .eclipse .jgit .lib .ProgressMonitor ;
42+ import org .eclipse .jgit .lib .Ref ;
43+ import org .eclipse .jgit .lib .RefUpdate ;
3544import org .eclipse .jgit .lib .Repository ;
3645import org .eclipse .jgit .revwalk .RevCommit ;
46+ import org .eclipse .jgit .revwalk .RevWalk ;
3747import org .eclipse .jgit .transport .PostReceiveHook ;
3848import org .eclipse .jgit .transport .PreReceiveHook ;
3949import org .eclipse .jgit .transport .ReceiveCommand ;
5060import com .gitblit .extensions .ReceiveHook ;
5161import com .gitblit .manager .IGitblit ;
5262import com .gitblit .models .RepositoryModel ;
63+ import com .gitblit .models .TicketModel ;
5364import com .gitblit .models .UserModel ;
65+ import com .gitblit .models .TicketModel .Change ;
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 ;
5471import com .gitblit .tickets .BranchTicketService ;
72+ import com .gitblit .tickets .ITicketService ;
73+ import com .gitblit .tickets .TicketNotifier ;
5574import com .gitblit .utils .ArrayUtils ;
5675import com .gitblit .utils .ClientLogger ;
5776import com .gitblit .utils .CommitCache ;
5877import com .gitblit .utils .JGitUtils ;
5978import com .gitblit .utils .RefLogUtils ;
6079import com .gitblit .utils .StringUtils ;
80+ import com .google .common .collect .Lists ;
6181
6282
6383/**
@@ -92,6 +112,11 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
92112 protected final IStoredSettings settings ;
93113
94114 protected final IGitblit gitblit ;
115+
116+ protected final ITicketService ticketService ;
117+
118+ protected final TicketNotifier ticketNotifier ;
119+
95120
96121 public GitblitReceivePack (
97122 IGitblit gitblit ,
@@ -114,6 +139,14 @@ public GitblitReceivePack(
114139 } catch (IOException e ) {
115140 }
116141
142+ if (gitblit .getTicketService ().isAcceptingTicketUpdates (repository )) {
143+ this .ticketService = gitblit .getTicketService ();
144+ this .ticketNotifier = this .ticketService .createNotifier ();
145+ } else {
146+ this .ticketService = null ;
147+ this .ticketNotifier = null ;
148+ }
149+
117150 // set advanced ref permissions
118151 setAllowCreates (user .canCreateRef (repository ));
119152 setAllowDeletes (user .canDeleteRef (repository ));
@@ -500,6 +533,104 @@ protected void executeCommands() {
500533 }
501534 }
502535 }
536+
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+ }
503634 }
504635
505636 protected void setGitblitUrl (String url ) {
@@ -616,4 +747,116 @@ public RepositoryModel getRepositoryModel() {
616747 public UserModel getUserModel () {
617748 return user ;
618749 }
750+
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+ }
619862}
0 commit comments