]> source.dussan.org Git - gitblit.git/commitdiff
Ticket Reference handling #1048
authorPaul Martin <paul@paulsputer.com>
Wed, 27 Apr 2016 22:58:06 +0000 (23:58 +0100)
committerPaul Martin <paul@paulsputer.com>
Wed, 27 Apr 2016 22:58:06 +0000 (23:58 +0100)
+ Supports referencing:
  + Tickets from other tickets via comments
  + Tickets from commits on any branch
+ Common TicketLink class used for both commits and tickets
  + TicketLink is temporary and persisted to ticket as a Reference
+ Support deletion of ticket references
  + Rebasing patchsets/branches will generate new references
  + Deleting old patchsets/branches will remove the relevant references
+ Substantial testing of use cases
  + With and without patchsets, deleting, amending
  + BranchTicketService used during testing to allow end-to-end ref testing
+ Relocated common git helper functions to JGitUtils

14 files changed:
src/main/distrib/data/defaults.properties
src/main/java/com/gitblit/git/GitblitReceivePack.java
src/main/java/com/gitblit/git/PatchsetReceivePack.java
src/main/java/com/gitblit/models/TicketModel.java
src/main/java/com/gitblit/tickets/ITicketService.java
src/main/java/com/gitblit/tickets/TicketNotifier.java
src/main/java/com/gitblit/utils/JGitUtils.java
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
src/main/java/com/gitblit/wicket/pages/TicketPage.html
src/main/java/com/gitblit/wicket/pages/TicketPage.java
src/main/resources/gitblit.css
src/test/config/test-gitblit.properties
src/test/java/com/gitblit/tests/GitBlitSuite.java
src/test/java/com/gitblit/tests/TicketReferenceTest.java [new file with mode: 0644]

index 403b74175c197d8b7ea5a6f755f4af1111fc0a68..0c7d6cd42abde6054636072361fce2ffcd6892e3 100644 (file)
@@ -574,6 +574,13 @@ tickets.requireApproval = false
 # SINCE 1.5.0
 tickets.closeOnPushCommitMessageRegex = (?:fixes|closes)[\\s-]+#?(\\d+)
 
+# The case-insensitive regular expression used to identify and link tickets on
+# push to the commits based on commit message.  In the case of a patchset 
+# self references are ignored 
+#
+# SINCE 1.8.0
+tickets.linkOnPushCommitMessageRegex = (?:ref|task|issue|bug)?[\\s-]*#(\\d+)
+
 # Specify the location of the Lucene Ticket index
 #
 # SINCE 1.4.0
index 34bbea27a24755915acaabee2074271c5d437c39..f271f6f19d136055f2bc7f3643feb6ad59c4c52c 100644 (file)
@@ -22,18 +22,28 @@ import groovy.util.GroovyScriptEngine;
 import java.io.File;\r
 import java.io.IOException;\r
 import java.text.MessageFormat;\r
+import java.util.ArrayList;\r
 import java.util.Collection;\r
+import java.util.LinkedHashMap;\r
 import java.util.LinkedHashSet;\r
 import java.util.List;\r
+import java.util.Map;\r
 import java.util.Set;\r
+import java.util.SortedMap;\r
+import java.util.TreeMap;\r
 import java.util.concurrent.TimeUnit;\r
 \r
+import org.eclipse.jgit.lib.AnyObjectId;\r
 import org.eclipse.jgit.lib.BatchRefUpdate;\r
 import org.eclipse.jgit.lib.NullProgressMonitor;\r
+import org.eclipse.jgit.lib.ObjectId;\r
 import org.eclipse.jgit.lib.PersonIdent;\r
 import org.eclipse.jgit.lib.ProgressMonitor;\r
+import org.eclipse.jgit.lib.Ref;\r
+import org.eclipse.jgit.lib.RefUpdate;\r
 import org.eclipse.jgit.lib.Repository;\r
 import org.eclipse.jgit.revwalk.RevCommit;\r
+import org.eclipse.jgit.revwalk.RevWalk;\r
 import org.eclipse.jgit.transport.PostReceiveHook;\r
 import org.eclipse.jgit.transport.PreReceiveHook;\r
 import org.eclipse.jgit.transport.ReceiveCommand;\r
@@ -50,14 +60,24 @@ import com.gitblit.client.Translation;
 import com.gitblit.extensions.ReceiveHook;\r
 import com.gitblit.manager.IGitblit;\r
 import com.gitblit.models.RepositoryModel;\r
+import com.gitblit.models.TicketModel;\r
 import com.gitblit.models.UserModel;\r
+import com.gitblit.models.TicketModel.Change;\r
+import com.gitblit.models.TicketModel.Field;\r
+import com.gitblit.models.TicketModel.Patchset;\r
+import com.gitblit.models.TicketModel.Status;\r
+import com.gitblit.models.TicketModel.TicketAction;\r
+import com.gitblit.models.TicketModel.TicketLink;\r
 import com.gitblit.tickets.BranchTicketService;\r
+import com.gitblit.tickets.ITicketService;\r
+import com.gitblit.tickets.TicketNotifier;\r
 import com.gitblit.utils.ArrayUtils;\r
 import com.gitblit.utils.ClientLogger;\r
 import com.gitblit.utils.CommitCache;\r
 import com.gitblit.utils.JGitUtils;\r
 import com.gitblit.utils.RefLogUtils;\r
 import com.gitblit.utils.StringUtils;\r
+import com.google.common.collect.Lists;\r
 \r
 \r
 /**\r
@@ -92,6 +112,11 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
        protected final IStoredSettings settings;\r
 \r
        protected final IGitblit gitblit;\r
+       \r
+       protected final ITicketService ticketService;\r
+\r
+       protected final TicketNotifier ticketNotifier;\r
+       \r
 \r
        public GitblitReceivePack(\r
                        IGitblit gitblit,\r
@@ -114,6 +139,14 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
                } catch (IOException e) {\r
                }\r
 \r
+               if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) {\r
+                       this.ticketService = gitblit.getTicketService();\r
+                       this.ticketNotifier = this.ticketService.createNotifier();\r
+               } else {\r
+                       this.ticketService = null;\r
+                       this.ticketNotifier = null;\r
+               }\r
+               \r
                // set advanced ref permissions\r
                setAllowCreates(user.canCreateRef(repository));\r
                setAllowDeletes(user.canDeleteRef(repository));\r
@@ -500,6 +533,104 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
                                }\r
                        }\r
                }\r
+               \r
+               //\r
+               // if there are ref update receive commands that were\r
+               // successfully processed and there is an active ticket service for the repository\r
+               // then process any referenced tickets\r
+               //\r
+               if (ticketService != null) {\r
+                       List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);\r
+                       if (!allUpdates.isEmpty()) {\r
+                               int ticketsProcessed = 0;\r
+                               for (ReceiveCommand cmd : allUpdates) {\r
+                                       switch (cmd.getType()) {\r
+                                       case CREATE:\r
+                                       case UPDATE:\r
+                                               if (cmd.getRefName().startsWith(Constants.R_HEADS)) {\r
+                                                       Collection<TicketModel> tickets = processReferencedTickets(cmd);\r
+                                                       ticketsProcessed += tickets.size();\r
+                                                       for (TicketModel ticket : tickets) {\r
+                                                               ticketNotifier.queueMailing(ticket);\r
+                                                       }\r
+                                               }\r
+                                               break;\r
+                                               \r
+                                       case UPDATE_NONFASTFORWARD:\r
+                                               if (cmd.getRefName().startsWith(Constants.R_HEADS)) {\r
+                                                       String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());\r
+                                                       List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());\r
+                                                       for (TicketLink link : deletedRefs) {\r
+                                                               link.isDelete = true;\r
+                                                       }\r
+                                                       Change deletion = new Change(user.username);\r
+                                                       deletion.pendingLinks = deletedRefs;\r
+                                                       ticketService.updateTicket(repository, 0, deletion);\r
+                                                       \r
+                                                       Collection<TicketModel> tickets = processReferencedTickets(cmd);\r
+                                                       ticketsProcessed += tickets.size();\r
+                                                       for (TicketModel ticket : tickets) {\r
+                                                               ticketNotifier.queueMailing(ticket);\r
+                                                       }\r
+                                               }\r
+                                               break;\r
+                                       case DELETE:\r
+                                               //Identify if the branch has been merged \r
+                                               SortedMap<Integer, String> bases =  new TreeMap<Integer, String>();\r
+                                               try {\r
+                                                       ObjectId dObj = cmd.getOldId();\r
+                                                       Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values();\r
+                                                       for (Ref ref : tips) {\r
+                                                               ObjectId iObj = ref.getObjectId();\r
+                                                               String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj);\r
+                                                               if (mergeBase != null) {\r
+                                                                       int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name());\r
+                                                                       bases.put(d, mergeBase);\r
+                                                                       //All commits have been merged into some other branch\r
+                                                                       if (d == 0) {\r
+                                                                               break;\r
+                                                                       }\r
+                                                               }\r
+                                                       }\r
+                                                       \r
+                                                       if (bases.isEmpty()) {\r
+                                                               //TODO: Handle orphan branch case\r
+                                                       } else {\r
+                                                               if (bases.firstKey() > 0) {\r
+                                                                       //Delete references from the remaining commits that haven't been merged\r
+                                                                       String mergeBase = bases.get(bases.firstKey());\r
+                                                                       List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(),\r
+                                                                                       settings, mergeBase, dObj.name());\r
+                                                                       \r
+                                                                       for (TicketLink link : deletedRefs) {\r
+                                                                               link.isDelete = true;\r
+                                                                       }\r
+                                                                       Change deletion = new Change(user.username);\r
+                                                                       deletion.pendingLinks = deletedRefs;\r
+                                                                       ticketService.updateTicket(repository, 0, deletion);\r
+                                                               }\r
+                                                       }\r
+                                                       \r
+                                               } catch (IOException e) {\r
+                                                       LOGGER.error(null, e);\r
+                                               }\r
+                                               break;\r
+                                               \r
+                                       default:\r
+                                               break;\r
+                                       }\r
+                               }\r
+       \r
+                               if (ticketsProcessed == 1) {\r
+                                       sendInfo("1 ticket updated");\r
+                               } else if (ticketsProcessed > 1) {\r
+                                       sendInfo("{0} tickets updated", ticketsProcessed);\r
+                               }\r
+                       }\r
+       \r
+                       // reset the ticket caches for the repository\r
+                       ticketService.resetCaches(repository);\r
+               }\r
        }\r
 \r
        protected void setGitblitUrl(String url) {\r
@@ -616,4 +747,116 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
        public UserModel getUserModel() {\r
                return user;\r
        }\r
+       \r
+       /**\r
+        * Automatically closes open tickets and adds references to tickets if made in the commit message.\r
+        *\r
+        * @param cmd\r
+        */\r
+       private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {\r
+               Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();\r
+\r
+               final RevWalk rw = getRevWalk();\r
+               try {\r
+                       rw.reset();\r
+                       rw.markStart(rw.parseCommit(cmd.getNewId()));\r
+                       if (!ObjectId.zeroId().equals(cmd.getOldId())) {\r
+                               rw.markUninteresting(rw.parseCommit(cmd.getOldId()));\r
+                       }\r
+\r
+                       RevCommit c;\r
+                       while ((c = rw.next()) != null) {\r
+                               rw.parseBody(c);\r
+                               List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);\r
+                               if (ticketLinks == null) {\r
+                                       continue;\r
+                               }\r
+\r
+                               for (TicketLink link : ticketLinks) {\r
+                                       \r
+                                       TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);\r
+                                       if (ticket == null) {\r
+                                               continue;\r
+                                       }\r
+                                       \r
+                                       Change change = null;\r
+                                       String commitSha = c.getName();\r
+                                       String branchName = Repository.shortenRefName(cmd.getRefName());\r
+                                       \r
+                                       switch (link.action) {\r
+                                               case Commit: {\r
+                                                       //A commit can reference a ticket in any branch even if the ticket is closed.\r
+                                                       //This allows developers to identify and communicate related issues\r
+                                                       change = new Change(user.username);\r
+                                                       change.referenceCommit(commitSha);\r
+                                               } break;\r
+                                               \r
+                                               case Close: {\r
+                                                       // As this isn't a patchset theres no merging taking place when closing a ticket\r
+                                                       if (ticket.isClosed()) {\r
+                                                               continue;\r
+                                                       }\r
+                                                       \r
+                                                       change = new Change(user.username);\r
+                                                       change.setField(Field.status, Status.Fixed);\r
+                                                       \r
+                                                       if (StringUtils.isEmpty(ticket.responsible)) {\r
+                                                               // unassigned tickets are assigned to the closer\r
+                                                               change.setField(Field.responsible, user.username);\r
+                                                       }\r
+                                               }\r
+                                               \r
+                                               default: {\r
+                                                       //No action\r
+                                               } break;\r
+                                       }\r
+                                       \r
+                                       if (change != null) {\r
+                                               ticket = ticketService.updateTicket(repository, ticket.number, change);\r
+                                       }\r
+       \r
+                                       if (ticket != null) {\r
+                                               sendInfo("");\r
+                                               sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));\r
+\r
+                                               switch (link.action) {\r
+                                                       case Commit: {\r
+                                                               sendInfo("referenced by push of {0} to {1}", commitSha, branchName);\r
+                                                               changedTickets.put(ticket.number, ticket);\r
+                                                       } break;\r
+\r
+                                                       case Close: {\r
+                                                               sendInfo("closed by push of {0} to {1}", commitSha, branchName);\r
+                                                               changedTickets.put(ticket.number, ticket);\r
+                                                       } break;\r
+\r
+                                                       default: { }\r
+                                               }\r
+\r
+                                               sendInfo(ticketService.getTicketUrl(ticket));\r
+                                               sendInfo("");\r
+                                       } else {\r
+                                               switch (link.action) {\r
+                                                       case Commit: {\r
+                                                               sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);\r
+                                                       } break;\r
+                                                       \r
+                                                       case Close: {\r
+                                                               sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha); \r
+                                                       } break;\r
+                                                       \r
+                                                       default: { }\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+                               \r
+               } catch (IOException e) {\r
+                       LOGGER.error("Can't scan for changes to reference or close", e);\r
+               } finally {\r
+                       rw.reset();\r
+               }\r
+\r
+               return changedTickets.values();\r
+       }\r
 }\r
index ef0b409b66760ffd4082907af07ba8df032057ea..33fa47055d6c5a5d880eb50bcf200d2b74ff5fe1 100644 (file)
@@ -30,7 +30,6 @@ import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;\r
 import java.util.regex.Pattern;\r
 \r
-import org.eclipse.jgit.lib.AnyObjectId;\r
 import org.eclipse.jgit.lib.BatchRefUpdate;\r
 import org.eclipse.jgit.lib.NullProgressMonitor;\r
 import org.eclipse.jgit.lib.ObjectId;\r
@@ -60,6 +59,8 @@ import com.gitblit.models.TicketModel.Field;
 import com.gitblit.models.TicketModel.Patchset;\r
 import com.gitblit.models.TicketModel.PatchsetType;\r
 import com.gitblit.models.TicketModel.Status;\r
+import com.gitblit.models.TicketModel.TicketAction;\r
+import com.gitblit.models.TicketModel.TicketLink;\r
 import com.gitblit.models.UserModel;\r
 import com.gitblit.tickets.BranchTicketService;\r
 import com.gitblit.tickets.ITicketService;\r
@@ -485,9 +486,27 @@ public class PatchsetReceivePack extends GitblitReceivePack {
                                switch (cmd.getType()) {\r
                                case CREATE:\r
                                case UPDATE:\r
+                                       if (cmd.getRefName().startsWith(Constants.R_HEADS)) {\r
+                                               Collection<TicketModel> tickets = processReferencedTickets(cmd);\r
+                                               ticketsProcessed += tickets.size();\r
+                                               for (TicketModel ticket : tickets) {\r
+                                                       ticketNotifier.queueMailing(ticket);\r
+                                               }\r
+                                       }\r
+                                       break;\r
+                                       \r
                                case UPDATE_NONFASTFORWARD:\r
                                        if (cmd.getRefName().startsWith(Constants.R_HEADS)) {\r
-                                               Collection<TicketModel> tickets = processMergedTickets(cmd);\r
+                                               String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());\r
+                                               List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());\r
+                                               for (TicketLink link : deletedRefs) {\r
+                                                       link.isDelete = true;\r
+                                               }\r
+                                               Change deletion = new Change(user.username);\r
+                                               deletion.pendingLinks = deletedRefs;\r
+                                               ticketService.updateTicket(repository, 0, deletion);\r
+\r
+                                               Collection<TicketModel> tickets = processReferencedTickets(cmd);\r
                                                ticketsProcessed += tickets.size();\r
                                                for (TicketModel ticket : tickets) {\r
                                                        ticketNotifier.queueMailing(ticket);\r
@@ -604,15 +623,17 @@ public class PatchsetReceivePack extends GitblitReceivePack {
                                return null;\r
                        }\r
                }\r
-\r
+               \r
                // check to see if this commit is already linked to a ticket\r
-               long id = identifyTicket(tipCommit, false);\r
-               if (id > 0) {\r
-                       sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, id);\r
+               if (ticket != null && \r
+                               JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {\r
+                       sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);\r
                        sendRejection(cmd, "everything up-to-date");\r
                        return null;\r
                }\r
-\r
+               \r
+               List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);\r
+               \r
                PatchsetCommand psCmd;\r
                if (ticket == null) {\r
                        /*\r
@@ -802,6 +823,10 @@ public class PatchsetReceivePack extends GitblitReceivePack {
                        }\r
                        break;\r
                }\r
+\r
+               Change change = psCmd.getChange();\r
+               change.pendingLinks = ticketLinks;\r
+\r
                return psCmd;\r
        }\r
 \r
@@ -890,11 +915,11 @@ public class PatchsetReceivePack extends GitblitReceivePack {
 \r
        /**\r
         * Automatically closes open tickets that have been merged to their integration\r
-        * branch by a client.\r
+        * branch by a client and adds references to tickets if made in the commit message.\r
         *\r
         * @param cmd\r
         */\r
-       private Collection<TicketModel> processMergedTickets(ReceiveCommand cmd) {\r
+       private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {\r
                Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();\r
                final RevWalk rw = getRevWalk();\r
                try {\r
@@ -907,105 +932,151 @@ public class PatchsetReceivePack extends GitblitReceivePack {
                        RevCommit c;\r
                        while ((c = rw.next()) != null) {\r
                                rw.parseBody(c);\r
-                               long ticketNumber = identifyTicket(c, true);\r
-                               if (ticketNumber == 0L || mergedTickets.containsKey(ticketNumber)) {\r
-                                       continue;\r
-                               }\r
-\r
-                               TicketModel ticket = ticketService.getTicket(repository, ticketNumber);\r
-                               if (ticket == null) {\r
-                                       continue;\r
-                               }\r
-                               String integrationBranch;\r
-                               if (StringUtils.isEmpty(ticket.mergeTo)) {\r
-                                       // unspecified integration branch\r
-                                       integrationBranch = null;\r
-                               } else {\r
-                                       // specified integration branch\r
-                                       integrationBranch = Constants.R_HEADS + ticket.mergeTo;\r
-                               }\r
-\r
-                               // ticket must be open and, if specified, the ref must match the integration branch\r
-                               if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {\r
+                               List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);\r
+                               if (ticketLinks == null) {\r
                                        continue;\r
                                }\r
 \r
-                               String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);\r
-                               boolean knownPatchset = false;\r
-                               Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());\r
-                               if (refs != null) {\r
-                                       for (Ref ref : refs) {\r
-                                               if (ref.getName().startsWith(baseRef)) {\r
-                                                       knownPatchset = true;\r
-                                                       break;\r
-                                               }\r
-                                       }\r
-                               }\r
-\r
-                               String mergeSha = c.getName();\r
-                               String mergeTo = Repository.shortenRefName(cmd.getRefName());\r
-                               Change change;\r
-                               Patchset patchset;\r
-                               if (knownPatchset) {\r
-                                       // identify merged patchset by the patchset tip\r
-                                       patchset = null;\r
-                                       for (Patchset ps : ticket.getPatchsets()) {\r
-                                               if (ps.tip.equals(mergeSha)) {\r
-                                                       patchset = ps;\r
-                                                       break;\r
-                                               }\r
+                               for (TicketLink link : ticketLinks) {\r
+                                       \r
+                                       if (mergedTickets.containsKey(link.targetTicketId)) {\r
+                                               continue;\r
                                        }\r
-\r
-                                       if (patchset == null) {\r
-                                               // should not happen - unless ticket has been hacked\r
-                                               sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",\r
-                                                               mergeSha, ticket.number);\r
+       \r
+                                       TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);\r
+                                       if (ticket == null) {\r
                                                continue;\r
                                        }\r
+                                       String integrationBranch;\r
+                                       if (StringUtils.isEmpty(ticket.mergeTo)) {\r
+                                               // unspecified integration branch\r
+                                               integrationBranch = null;\r
+                                       } else {\r
+                                               // specified integration branch\r
+                                               integrationBranch = Constants.R_HEADS + ticket.mergeTo;\r
+                                       }\r
+       \r
+                                       Change change;\r
+                                       Patchset patchset = null;\r
+                                       String mergeSha = c.getName();\r
+                                       String mergeTo = Repository.shortenRefName(cmd.getRefName());\r
+\r
+                                       if (link.action == TicketAction.Commit) {\r
+                                               //A commit can reference a ticket in any branch even if the ticket is closed.\r
+                                               //This allows developers to identify and communicate related issues\r
+                                               change = new Change(user.username);\r
+                                               change.referenceCommit(mergeSha);\r
+                                       } else {\r
+                                               // ticket must be open and, if specified, the ref must match the integration branch\r
+                                               if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {\r
+                                                       continue;\r
+                                               }\r
+       \r
+                                               String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);\r
+                                               boolean knownPatchset = false;\r
+                                               Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());\r
+                                               if (refs != null) {\r
+                                                       for (Ref ref : refs) {\r
+                                                               if (ref.getName().startsWith(baseRef)) {\r
+                                                                       knownPatchset = true;\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+                                               }\r
+       \r
+                                               if (knownPatchset) {\r
+                                                       // identify merged patchset by the patchset tip\r
+                                                       for (Patchset ps : ticket.getPatchsets()) {\r
+                                                               if (ps.tip.equals(mergeSha)) {\r
+                                                                       patchset = ps;\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+       \r
+                                                       if (patchset == null) {\r
+                                                               // should not happen - unless ticket has been hacked\r
+                                                               sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",\r
+                                                                               mergeSha, ticket.number);\r
+                                                               continue;\r
+                                                       }\r
+       \r
+                                                       // create a new change\r
+                                                       change = new Change(user.username);\r
+                                               } else {\r
+                                                       // new patchset pushed by user\r
+                                                       String base = cmd.getOldId().getName();\r
+                                                       patchset = newPatchset(ticket, base, mergeSha);\r
+                                                       PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);\r
+                                                       psCmd.updateTicket(c, mergeTo, ticket, null);\r
+       \r
+                                                       // create a ticket patchset ref\r
+                                                       updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);\r
+                                                       RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);\r
+                                                       updateReflog(ru);\r
+       \r
+                                                       // create a change from the patchset command\r
+                                                       change = psCmd.getChange();\r
+                                               }\r
+       \r
+                                               // set the common change data about the merge\r
+                                               change.setField(Field.status, Status.Merged);\r
+                                               change.setField(Field.mergeSha, mergeSha);\r
+                                               change.setField(Field.mergeTo, mergeTo);\r
+       \r
+                                               if (StringUtils.isEmpty(ticket.responsible)) {\r
+                                                       // unassigned tickets are assigned to the closer\r
+                                                       change.setField(Field.responsible, user.username);\r
+                                               }\r
+                                       }\r
+       \r
+                                       ticket = ticketService.updateTicket(repository, ticket.number, change);\r
+       \r
+                                       if (ticket != null) {\r
+                                               sendInfo("");\r
+                                               sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));\r
+\r
+                                               switch (link.action) {\r
+                                                       case Commit: {\r
+                                                               sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);\r
+                                                       }\r
+                                                       break;\r
 \r
-                                       // create a new change\r
-                                       change = new Change(user.username);\r
-                               } else {\r
-                                       // new patchset pushed by user\r
-                                       String base = cmd.getOldId().getName();\r
-                                       patchset = newPatchset(ticket, base, mergeSha);\r
-                                       PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);\r
-                                       psCmd.updateTicket(c, mergeTo, ticket, null);\r
-\r
-                                       // create a ticket patchset ref\r
-                                       updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);\r
-                                       RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);\r
-                                       updateReflog(ru);\r
-\r
-                                       // create a change from the patchset command\r
-                                       change = psCmd.getChange();\r
-                               }\r
+                                                       case Close: {\r
+                                                               sendInfo("closed by push of {0} to {1}", patchset, mergeTo);\r
+                                                               mergedTickets.put(ticket.number, ticket);       \r
+                                                       }\r
+                                                       break;\r
 \r
-                               // set the common change data about the merge\r
-                               change.setField(Field.status, Status.Merged);\r
-                               change.setField(Field.mergeSha, mergeSha);\r
-                               change.setField(Field.mergeTo, mergeTo);\r
+                                                       default: {\r
+                                                               \r
+                                                       }\r
+                                               }\r
 \r
-                               if (StringUtils.isEmpty(ticket.responsible)) {\r
-                                       // unassigned tickets are assigned to the closer\r
-                                       change.setField(Field.responsible, user.username);\r
-                               }\r
+                                               sendInfo(ticketService.getTicketUrl(ticket));\r
+                                               sendInfo("");\r
 \r
-                               ticket = ticketService.updateTicket(repository, ticket.number, change);\r
-                               if (ticket != null) {\r
-                                       sendInfo("");\r
-                                       sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));\r
-                                       sendInfo("closed by push of {0} to {1}", patchset, mergeTo);\r
-                                       sendInfo(ticketService.getTicketUrl(ticket));\r
-                                       sendInfo("");\r
-                                       mergedTickets.put(ticket.number, ticket);\r
-                               } else {\r
-                                       String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));\r
-                                       sendError("FAILED to close ticket {0,number,0} by push of {1}", ticketNumber, shortid);\r
+                                       } else {\r
+                                               String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));\r
+                                               \r
+                                               switch (link.action) {\r
+                                                       case Commit: {\r
+                                                               sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);\r
+                                                       }\r
+                                                       break;\r
+                                                       case Close: {\r
+                                                               sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);  \r
+                                                       } break;\r
+                                                       \r
+                                                       default: {\r
+                                                               \r
+                                                       }\r
+                                               }\r
+                                       }\r
                                }\r
                        }\r
+                               \r
                } catch (IOException e) {\r
-                       LOGGER.error("Can't scan for changes to close", e);\r
+                       LOGGER.error("Can't scan for changes to reference or close", e);\r
                } finally {\r
                        rw.reset();\r
                }\r
@@ -1013,75 +1084,9 @@ public class PatchsetReceivePack extends GitblitReceivePack {
                return mergedTickets.values();\r
        }\r
 \r
-       /**\r
-        * Try to identify a ticket id from the commit.\r
-        *\r
-        * @param commit\r
-        * @param parseMessage\r
-        * @return a ticket id or 0\r
-        */\r
-       private long identifyTicket(RevCommit commit, boolean parseMessage) {\r
-               // try lookup by change ref\r
-               Map<AnyObjectId, Set<Ref>> map = getRepository().getAllRefsByPeeledObjectId();\r
-               Set<Ref> refs = map.get(commit.getId());\r
-               if (!ArrayUtils.isEmpty(refs)) {\r
-                       for (Ref ref : refs) {\r
-                               long number = PatchsetCommand.getTicketNumber(ref.getName());\r
-                               if (number > 0) {\r
-                                       return number;\r
-                               }\r
-                       }\r
-               }\r
-\r
-               if (parseMessage) {\r
-                       // parse commit message looking for fixes/closes #n\r
-                       String dx = "(?:fixes|closes)[\\s-]+#?(\\d+)";\r
-                       String x = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, dx);\r
-                       if (StringUtils.isEmpty(x)) {\r
-                               x = dx;\r
-                       }\r
-                       try {\r
-                               Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);\r
-                               Matcher m = p.matcher(commit.getFullMessage());\r
-                               while (m.find()) {\r
-                                       String val = m.group(1);\r
-                                       return Long.parseLong(val);\r
-                               }\r
-                       } catch (Exception e) {\r
-                               LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", x, commit.getName()), e);\r
-                       }\r
-               }\r
-               return 0L;\r
-       }\r
+       \r
 \r
-       private int countCommits(String baseId, String tipId) {\r
-               int count = 0;\r
-               RevWalk walk = getRevWalk();\r
-               walk.reset();\r
-               walk.sort(RevSort.TOPO);\r
-               walk.sort(RevSort.REVERSE, true);\r
-               try {\r
-                       RevCommit tip = walk.parseCommit(getRepository().resolve(tipId));\r
-                       RevCommit base = walk.parseCommit(getRepository().resolve(baseId));\r
-                       walk.markStart(tip);\r
-                       walk.markUninteresting(base);\r
-                       for (;;) {\r
-                               RevCommit c = walk.next();\r
-                               if (c == null) {\r
-                                       break;\r
-                               }\r
-                               count++;\r
-                       }\r
-               } catch (IOException e) {\r
-                       // Should never happen, the core receive process would have\r
-                       // identified the missing object earlier before we got control.\r
-                       LOGGER.error("failed to get commit count", e);\r
-                       return 0;\r
-               } finally {\r
-                       walk.close();\r
-               }\r
-               return count;\r
-       }\r
+       \r
 \r
        /**\r
         * Creates a new patchset with metadata.\r
@@ -1091,7 +1096,7 @@ public class PatchsetReceivePack extends GitblitReceivePack {
         * @param tip\r
         */\r
        private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {\r
-               int totalCommits = countCommits(mergeBase, tip);\r
+               int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);\r
 \r
                Patchset newPatchset = new Patchset();\r
                newPatchset.tip = tip;\r
index 7495448f228a866245946b5c8ef7af005bc99361..d5345891974982f3e2c52fc4030ba93b48adbee6 100644 (file)
@@ -107,6 +107,7 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                TicketModel ticket;
                List<Change> effectiveChanges = new ArrayList<Change>();
                Map<String, Change> comments = new HashMap<String, Change>();
+               Map<String, Change> references = new HashMap<String, Change>();
                Map<Integer, Integer> latestRevisions = new HashMap<Integer, Integer>();
                
                int latestPatchsetNumber = -1;
@@ -159,6 +160,18 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                                        
                                        effectiveChanges.add(change);
                                }
+                       } else if (change.reference != null){
+                               if (references.containsKey(change.reference.toString())) {
+                                       Change original = references.get(change.reference.toString());
+                                       Change clone = copy(original);
+                                       clone.reference.deleted = change.reference.deleted;
+                                       int idx = effectiveChanges.indexOf(original);
+                                       effectiveChanges.remove(original);
+                                       effectiveChanges.add(idx, clone);
+                               } else {
+                                       effectiveChanges.add(change);
+                                       references.put(change.reference.toString(), change);
+                               }
                        } else {
                                effectiveChanges.add(change);
                        }
@@ -167,10 +180,16 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                // effective ticket
                ticket = new TicketModel();
                for (Change change : effectiveChanges) {
+                       //Ensure deleted items are not included
                        if (!change.hasComment()) {
-                               // ensure we do not include a deleted comment
                                change.comment = null;
                        }
+                       if (!change.hasReference()) {
+                               change.reference = null;
+                       }
+                       if (!change.hasPatchset()) {
+                               change.patchset = null;
+                       }
                        ticket.applyChange(change);
                }
                return ticket;
@@ -354,6 +373,15 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                return false;
        }
 
+       public boolean hasReferences() {
+               for (Change change : changes) {
+                       if (change.hasReference()) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+       
        public List<Attachment> getAttachments() {
                List<Attachment> list = new ArrayList<Attachment>();
                for (Change change : changes) {
@@ -364,6 +392,16 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                return list;
        }
 
+       public List<Reference> getReferences() {
+               List<Reference> list = new ArrayList<Reference>();
+               for (Change change : changes) {
+                       if (change.hasReference()) {
+                               list.add(change.reference);
+                       }
+               }
+               return list;
+       }
+       
        public List<Patchset> getPatchsets() {
                List<Patchset> list = new ArrayList<Patchset>();
                for (Change change : changes) {
@@ -573,8 +611,12 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                        }
                }
 
-               // add the change to the ticket
-               changes.add(change);
+               // add real changes to the ticket and ensure deleted changes are removed
+               if (change.isEmptyChange()) {
+                       changes.remove(change);
+               } else {
+                       changes.add(change);
+               }
        }
 
        protected String toString(Object value) {
@@ -645,6 +687,8 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
 
                public Comment comment;
 
+               public Reference reference;
+
                public Map<Field, String> fields;
 
                public Set<Attachment> attachments;
@@ -655,6 +699,10 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
 
                private transient String id;
 
+               //Once links have been made they become a reference on the target ticket
+               //The ticket service handles promoting links to references
+               public transient List<TicketLink> pendingLinks;
+
                public Change(String author) {
                        this(author, new Date());
                }
@@ -678,7 +726,7 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                }
 
                public boolean hasPatchset() {
-                       return patchset != null;
+                       return patchset != null && !patchset.isDeleted();
                }
 
                public boolean hasReview() {
@@ -688,11 +736,42 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                public boolean hasComment() {
                        return comment != null && !comment.isDeleted() && comment.text != null;
                }
+               
+               public boolean hasReference() {
+                       return reference != null && !reference.isDeleted();
+               }
+
+               public boolean hasPendingLinks() {
+                       return pendingLinks != null && pendingLinks.size() > 0;
+               }
 
                public Comment comment(String text) {
                        comment = new Comment(text);
                        comment.id = TicketModel.getSHA1(date.toString() + author + text);
 
+                       // parse comment looking for ref #n
+                       //TODO: Ideally set via settings
+                       String x = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";
+
+                       try {
+                               Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
+                               Matcher m = p.matcher(text);
+                               while (m.find()) {
+                                       String val = m.group(1);
+                                       long targetTicketId = Long.parseLong(val);
+                                       
+                                       if (targetTicketId > 0) {
+                                               if (pendingLinks == null) {
+                                                       pendingLinks = new ArrayList<TicketLink>();
+                                               }
+                                               
+                                               pendingLinks.add(new TicketLink(targetTicketId, TicketAction.Comment));
+                                       }
+                               }
+                       } catch (Exception e) {
+                               // ignore
+                       }
+                       
                        try {
                                Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
                                Matcher m = mentions.matcher(text);
@@ -706,6 +785,16 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                        return comment;
                }
 
+               public Reference referenceCommit(String commitHash) {
+                       reference = new Reference(commitHash);
+                       return reference;
+               }
+
+               public Reference referenceTicket(long ticketId, String changeHash) {
+                       reference = new Reference(ticketId, changeHash);
+                       return reference;
+               }
+               
                public Review review(Patchset patchset, Score score, boolean addReviewer) {
                        if (addReviewer) {
                                plusList(Field.reviewers, author);
@@ -876,6 +965,17 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                        }
                        return false;
                }
+               
+               /*
+                * Identify if this is an empty change. i.e. only an author and date is defined.
+                * This can occur when items have been deleted
+                * @returns true if the change is empty
+                */
+               private boolean isEmptyChange() {
+                       return ((comment == null) && (reference == null) && 
+                                       (fields == null) && (attachments == null) && 
+                                       (patchset == null) && (review == null));
+               }
 
                @Override
                public String toString() {
@@ -885,6 +985,8 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                                sb.append(" commented on by ");
                        } else if (hasPatchset()) {
                                sb.append(MessageFormat.format(" {0} uploaded by ", patchset));
+                       } else if (hasReference()) {
+                               sb.append(MessageFormat.format(" referenced in {0} by ", reference));
                        } else {
                                sb.append(" changed by ");
                        }
@@ -1145,6 +1247,114 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
                        return text;
                }
        }
+       
+       
+       public static enum TicketAction {
+               Commit, Comment, Patchset, Close
+       }
+       
+       //Intentionally not serialized, links are persisted as "references"
+       public static class TicketLink {
+               public long targetTicketId;
+               public String hash;
+               public TicketAction action;
+               public boolean success;
+               public boolean isDelete;
+               
+               public TicketLink(long targetTicketId, TicketAction action) {
+                       this.targetTicketId = targetTicketId;
+                       this.action = action;
+                       success = false;
+                       isDelete = false;
+               }
+               
+               public TicketLink(long targetTicketId, TicketAction action, String hash) {
+                       this.targetTicketId = targetTicketId;
+                       this.action = action;
+                       this.hash = hash;
+                       success = false;
+                       isDelete = false;
+               }
+       }
+       
+       public static enum ReferenceType {
+               Undefined, Commit, Ticket;
+       
+               @Override
+               public String toString() {
+                       return name().toLowerCase().replace('_', ' ');
+               }
+               
+               public static ReferenceType fromObject(Object o, ReferenceType defaultType) {
+                       if (o instanceof ReferenceType) {
+                               // cast and return
+                               return (ReferenceType) o;
+                       } else if (o instanceof String) {
+                               // find by name
+                               for (ReferenceType type : values()) {
+                                       String str = o.toString();
+                                       if (type.name().equalsIgnoreCase(str)
+                                                       || type.toString().equalsIgnoreCase(str)) {
+                                               return type;
+                                       }
+                               }
+                       } else if (o instanceof Number) {
+                               // by ordinal
+                               int id = ((Number) o).intValue();
+                               if (id >= 0 && id < values().length) {
+                                       return values()[id];
+                               }
+                       }
+
+                       return defaultType;
+               }
+       }
+       
+       public static class Reference implements Serializable {
+       
+               private static final long serialVersionUID = 1L;
+               
+               public String hash;
+               public Long ticketId;
+               
+               public Boolean deleted;
+               
+               Reference(String commitHash) {
+                       this.hash = commitHash;
+               }
+               
+               Reference(long ticketId, String changeHash) {
+                       this.ticketId = ticketId;
+                       this.hash = changeHash;
+               }
+               
+               public ReferenceType getSourceType(){
+                       if (hash != null) {
+                               if (ticketId != null) {
+                                       return ReferenceType.Ticket;
+                               } else {
+                                       return ReferenceType.Commit;
+                               }
+                       }
+                       
+                       return ReferenceType.Undefined;
+               }
+               
+               public boolean isDeleted() {
+                       return deleted != null && deleted;
+               }
+               
+               @Override
+               public String toString() {
+                       switch (getSourceType()) {
+                               case Commit: return hash;
+                               case Ticket: return ticketId.toString() + "#" + hash;
+                               default: {} break;
+                       }
+                       
+                       return String.format("Unknown Reference Type");
+               }
+       }
 
        public static class Attachment implements Serializable {
 
index e8310039851a873674f7430da6e9e893cc64046d..20b6505b4bcd4f57c9aa401e3e514e4a48e292e0 100644 (file)
@@ -50,9 +50,11 @@ import com.gitblit.models.TicketModel.Field;
 import com.gitblit.models.TicketModel.Patchset;
 import com.gitblit.models.TicketModel.PatchsetType;
 import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.TicketModel.TicketLink;
 import com.gitblit.tickets.TicketIndexer.Lucene;
 import com.gitblit.utils.DeepCopier;
 import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.DiffUtils.DiffStat;
 import com.gitblit.utils.StringUtils;
 import com.google.common.cache.Cache;
@@ -1021,12 +1023,12 @@ public abstract class ITicketService implements IManager {
        }
 
        /**
-        * Updates a ticket.
+        * Updates a ticket and promotes pending links into references.
         *
         * @param repository
-        * @param ticketId
+        * @param ticketId, or 0 to action pending links in general
         * @param change
-        * @return the ticket model if successful
+        * @return the ticket model if successful, null if failure or using 0 ticketId
         * @since 1.4.0
         */
        public final TicketModel updateTicket(RepositoryModel repository, long ticketId, Change change) {
@@ -1038,28 +1040,78 @@ public abstract class ITicketService implements IManager {
                        throw new RuntimeException("must specify a change author!");
                }
 
-               TicketKey key = new TicketKey(repository, ticketId);
-               ticketsCache.invalidate(key);
-
-               boolean success = commitChangeImpl(repository, ticketId, change);
+               boolean success = true;
+               TicketModel ticket = null;
+               
+               if (ticketId > 0) {
+                       TicketKey key = new TicketKey(repository, ticketId);
+                       ticketsCache.invalidate(key);
+       
+                       success = commitChangeImpl(repository, ticketId, change);
+                       
+                       if (success) {
+                               ticket = getTicket(repository, ticketId);
+                               ticketsCache.put(key, ticket);
+                               indexer.index(ticket);
+       
+                               // call the ticket hooks
+                               if (pluginManager != null) {
+                                       for (TicketHook hook : pluginManager.getExtensions(TicketHook.class)) {
+                                               try {
+                                                       hook.onUpdateTicket(ticket, change);
+                                               } catch (Exception e) {
+                                                       log.error("Failed to execute extension", e);
+                                               }
+                                       }
+                               }
+                       }
+               }
+               
                if (success) {
-                       TicketModel ticket = getTicket(repository, ticketId);
-                       ticketsCache.put(key, ticket);
-                       indexer.index(ticket);
+                       //Now that the ticket has been successfully persisted add references to this ticket from linked tickets
+                       if (change.hasPendingLinks()) {
+                               for (TicketLink link : change.pendingLinks) {
+                                       TicketModel linkedTicket = getTicket(repository, link.targetTicketId);
+                                       Change dstChange = null;
+                                       
+                                       //Ignore if not available or self reference 
+                                       if (linkedTicket != null && link.targetTicketId != ticketId) {
+                                               dstChange = new Change(change.author, change.date);
+                                               
+                                               switch (link.action) {
+                                                       case Comment: {
+                                                               if (ticketId == 0) {
+                                                                       throw new RuntimeException("must specify a ticket when linking a comment!");
+                                                               }
+                                                               dstChange.referenceTicket(ticketId, change.comment.id);
+                                                       } break;
+                                                       
+                                                       case Commit: {
+                                                               dstChange.referenceCommit(link.hash);
+                                                       } break;
+                                                       
+                                                       default: {
+                                                               throw new RuntimeException(
+                                                                               String.format("must add persist logic for link of type %s", link.action));
+                                                       }
+                                               }
+                                       }
+                                       
+                                       if (dstChange != null) {
+                                               //If not deleted then remain null in journal
+                                               if (link.isDelete) {
+                                                       dstChange.reference.deleted = true;
+                                               }
 
-                       // call the ticket hooks
-                       if (pluginManager != null) {
-                               for (TicketHook hook : pluginManager.getExtensions(TicketHook.class)) {
-                                       try {
-                                               hook.onUpdateTicket(ticket, change);
-                                       } catch (Exception e) {
-                                               log.error("Failed to execute extension", e);
+                                               if (updateTicket(repository, link.targetTicketId, dstChange) != null) {
+                                                       link.success = true;
+                                               }
                                        }
                                }
                        }
-                       return ticket;
                }
-               return null;
+               
+               return ticket;
        }
 
        /**
@@ -1232,9 +1284,18 @@ public abstract class ITicketService implements IManager {
                deletion.patchset.number = patchset.number;
                deletion.patchset.rev = patchset.rev;
                deletion.patchset.type = PatchsetType.Delete;
+               //Find and delete references to tickets by the removed commits
+               List<TicketLink> patchsetTicketLinks = JGitUtils.identifyTicketsBetweenCommits(
+                               repositoryManager.getRepository(ticket.repository),
+                               settings, patchset.base, patchset.tip);
                
-               RepositoryModel repository = repositoryManager.getRepositoryModel(ticket.repository);
-               TicketModel revisedTicket = updateTicket(repository, ticket.number, deletion);
+               for (TicketLink link : patchsetTicketLinks) {
+                       link.isDelete = true;
+               }
+               deletion.pendingLinks = patchsetTicketLinks;
+               
+               RepositoryModel repositoryModel = repositoryManager.getRepositoryModel(ticket.repository);
+               TicketModel revisedTicket = updateTicket(repositoryModel, ticket.number, deletion);
                
                return revisedTicket;
        } 
index 5979cf26014536a723e26f7211f95625e4a2154d..8c7fe6d46a2f15c20449cd7cc59a0153dce8527c 100644 (file)
@@ -317,6 +317,19 @@ public class TicketNotifier {
                        // comment update
                        sb.append(MessageFormat.format("**{0}** commented on this ticket.", user.getDisplayName()));
                        sb.append(HARD_BRK);
+               } else if (lastChange.hasReference()) {
+                       // reference update
+                       String type = "?";
+
+                       switch (lastChange.reference.getSourceType()) {
+                               case Commit: { type = "commit"; } break;
+                               case Ticket: { type = "ticket"; } break;
+                               default: { } break;
+                       }
+                               
+                       sb.append(MessageFormat.format("**{0}** referenced this ticket in {1} {2}", type, lastChange.toString())); 
+                       sb.append(HARD_BRK);
+                       
                } else {
                        // general update
                        pattern = "**{0}** has updated this ticket.";
index adcbb4def626eaee5c27fafb5948ccb9537c1177..a02fc3fff798e66df9976a9d22bcf825cec86d65 100644 (file)
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.Iterator;\r
 import java.util.List;\r
 import java.util.Map;\r
+import java.util.Set;\r
 import java.util.Map.Entry;\r
 import java.util.regex.Matcher;\r
 import java.util.regex.Pattern;\r
@@ -46,12 +47,15 @@ import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawTextComparator;\r
 import org.eclipse.jgit.dircache.DirCache;\r
 import org.eclipse.jgit.dircache.DirCacheEntry;\r
+import org.eclipse.jgit.errors.AmbiguousObjectException;\r
 import org.eclipse.jgit.errors.ConfigInvalidException;\r
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;\r
 import org.eclipse.jgit.errors.LargeObjectException;\r
 import org.eclipse.jgit.errors.MissingObjectException;\r
+import org.eclipse.jgit.errors.RevisionSyntaxException;\r
 import org.eclipse.jgit.errors.StopWalkException;\r
 import org.eclipse.jgit.internal.JGitText;\r
+import org.eclipse.jgit.lib.AnyObjectId;\r
 import org.eclipse.jgit.lib.BlobBasedConfig;\r
 import org.eclipse.jgit.lib.CommitBuilder;\r
 import org.eclipse.jgit.lib.Constants;\r
@@ -91,19 +95,22 @@ import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
 import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;\r
 import org.eclipse.jgit.treewalk.filter.TreeFilter;\r
 import org.eclipse.jgit.util.FS;\r
+import org.jetbrains.annotations.NotNull;\r
 import org.slf4j.Logger;\r
 import org.slf4j.LoggerFactory;\r
 \r
-import com.gitblit.GitBlit;\r
 import com.gitblit.GitBlitException;\r
-import com.gitblit.manager.GitblitManager;\r
+import com.gitblit.IStoredSettings;\r
+import com.gitblit.Keys;\r
+import com.gitblit.git.PatchsetCommand;\r
 import com.gitblit.models.FilestoreModel;\r
 import com.gitblit.models.GitNote;\r
 import com.gitblit.models.PathModel;\r
 import com.gitblit.models.PathModel.PathChangeModel;\r
+import com.gitblit.models.TicketModel.TicketAction;\r
+import com.gitblit.models.TicketModel.TicketLink;\r
 import com.gitblit.models.RefModel;\r
 import com.gitblit.models.SubmoduleModel;\r
-import com.gitblit.servlet.FilestoreServlet;\r
 import com.google.common.base.Strings;\r
 \r
 /**\r
@@ -2740,5 +2747,163 @@ public class JGitUtils {
                }\r
                return false;\r
        }\r
+       \r
+       /*\r
+        * Identify ticket by considering the branch the commit is on\r
+        * \r
+        * @param repository\r
+        * @param commit \r
+        * @return ticket number, or 0 if no ticket\r
+        */\r
+       public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) {\r
+               // try lookup by change ref\r
+               Map<AnyObjectId, Set<Ref>> map = repository.getAllRefsByPeeledObjectId();\r
+               Set<Ref> refs = map.get(commit.getId());\r
+               if (!ArrayUtils.isEmpty(refs)) {\r
+                       for (Ref ref : refs) {\r
+                               long number = PatchsetCommand.getTicketNumber(ref.getName());\r
+                               \r
+                               if (number > 0) {\r
+                                       return number;\r
+                               }\r
+                       }\r
+               }\r
+               \r
+               return 0;\r
+       }\r
+       \r
+       \r
+       /**\r
+        * Try to identify all referenced tickets from the commit.\r
+        *\r
+        * @param commit\r
+        * @return a collection of TicketLinks\r
+        */\r
+       @NotNull\r
+       public static List<TicketLink> identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings,\r
+                       RevCommit commit) {\r
+               List<TicketLink> ticketLinks = new ArrayList<TicketLink>();\r
+               List<Long> linkedTickets = new ArrayList<Long>();\r
+\r
+               // parse commit message looking for fixes/closes #n\r
+               final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)";\r
+               String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault);\r
+               if (StringUtils.isEmpty(xFix)) {\r
+                       xFix = xFixDefault;\r
+               }\r
+               try {\r
+                       Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE);\r
+                       Matcher m = p.matcher(commit.getFullMessage());\r
+                       while (m.find()) {\r
+                               String val = m.group(1);\r
+                               long number = Long.parseLong(val); \r
+                               \r
+                               if (number > 0) {\r
+                                       ticketLinks.add(new TicketLink(number, TicketAction.Close));\r
+                                       linkedTickets.add(number);\r
+                               }\r
+                       }\r
+               } catch (Exception e) {\r
+                       LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e);\r
+               }\r
+               \r
+               // parse commit message looking for ref #n\r
+               final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";\r
+               String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault);\r
+               if (StringUtils.isEmpty(xRef)) {\r
+                       xRef = xRefDefault;\r
+               }\r
+               try {\r
+                       Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE);\r
+                       Matcher m = p.matcher(commit.getFullMessage());\r
+                       while (m.find()) {\r
+                               String val = m.group(1);\r
+                               long number = Long.parseLong(val); \r
+                               //Most generic case so don't included tickets more precisely linked\r
+                               if ((number > 0) && (!linkedTickets.contains(number))) {\r
+                                       ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName()));\r
+                                       linkedTickets.add(number);\r
+                               }\r
+                       }\r
+               } catch (Exception e) {\r
+                       LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e);\r
+               }\r
 \r
+               return ticketLinks;\r
+       }\r
+       \r
+       /**\r
+        * Try to identify all referenced tickets between two commits\r
+        *\r
+        * @param commit\r
+        * @param parseMessage\r
+        * @param currentTicketId, or 0 if not on a ticket branch\r
+        * @return a collection of TicketLink, or null if commit is already linked\r
+        */\r
+       public static List<TicketLink> identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings,\r
+                       String baseSha, String tipSha) {\r
+               List<TicketLink> links = new ArrayList<TicketLink>();\r
+               if (repository == null) { return links; }\r
+               \r
+               RevWalk walk = new RevWalk(repository);\r
+               walk.sort(RevSort.TOPO);\r
+               walk.sort(RevSort.REVERSE, true);\r
+               try {\r
+                       RevCommit tip = walk.parseCommit(repository.resolve(tipSha));\r
+                       RevCommit base = walk.parseCommit(repository.resolve(baseSha));\r
+                       walk.markStart(tip);\r
+                       walk.markUninteresting(base);\r
+                       for (;;) {\r
+                               RevCommit commit = walk.next();\r
+                               if (commit == null) {\r
+                                       break;\r
+                               }\r
+                               links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit));\r
+                       }\r
+               } catch (IOException e) {\r
+                       LOGGER.error("failed to identify tickets between commits.", e);\r
+               } finally {\r
+                       walk.dispose();\r
+               }\r
+               \r
+               return links;\r
+       }\r
+       \r
+       public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) {\r
+               int count = 0;\r
+               walk.reset();\r
+               walk.sort(RevSort.TOPO);\r
+               walk.sort(RevSort.REVERSE, true);\r
+               try {\r
+                       RevCommit tip = walk.parseCommit(tipId);\r
+                       RevCommit base = walk.parseCommit(baseId);\r
+                       walk.markStart(tip);\r
+                       walk.markUninteresting(base);\r
+                       for (;;) {\r
+                               RevCommit c = walk.next();\r
+                               if (c == null) {\r
+                                       break;\r
+                               }\r
+                               count++;\r
+                       }\r
+               } catch (IOException e) {\r
+                       // Should never happen, the core receive process would have\r
+                       // identified the missing object earlier before we got control.\r
+                       LOGGER.error("failed to get commit count", e);\r
+                       return 0;\r
+               } finally {\r
+                       walk.close();\r
+               }\r
+               return count;\r
+       }\r
+\r
+       public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) {\r
+               int count = 0;\r
+               try {\r
+                       count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId));\r
+               } catch (IOException e) {\r
+                       LOGGER.error("failed to get commit count", e);\r
+               }\r
+               return count;\r
+       }\r
 }\r
index cee7eaba04a1148e5dcbf695c995eb155f50b5ca..a215b4d6dbec180d032d1b425680d5c86afc7e0b 100644 (file)
@@ -778,4 +778,6 @@ gb.fileNotMergeable = Unable to commit {0}.  This file can not be automatically
 gb.fileCommitted = Successfully committed {0}.
 gb.deletePatchset = Delete Patchset {0}
 gb.deletePatchsetSuccess = Deleted Patchset {0}.
-gb.deletePatchsetFailure = Error deleting Patchset {0}.  
+gb.deletePatchsetFailure = Error deleting Patchset {0}.
+gb.referencedByCommit = Referenced by commit.
+gb.referencedByTicket = Referenced by ticket.
index 974dcc03a3255d8841b10280c30b1aa015afa634..46c0f7ee3a0de29fbc5947773109f027ca375838 100644 (file)
@@ -461,7 +461,7 @@ pt push</pre>
                                <td style="text-align:right;">\r
                                        <span wicket:id="patchsetType">[revision type]</span>                                   \r
                                </td>\r
-                               <td><span class="hidden-phone hidden-tablet aui-lozenge aui-lozenge-subtle" wicket:id="patchsetRevision">[R1]</span>\r
+                               <td><span class="hidden-phone hidden-tablet" wicket:id="patchsetRevision">[R1]</span>\r
                                        <span class="fa fa-fw" style="padding-left:15px;"><a wicket:id="deleteRevision" class="fa fa-fw fa-trash delete-patchset"></a></span>\r
                                        <span class="hidden-tablet hidden-phone" style="padding-left:15px;"><span wicket:id="patchsetDiffStat"></span></span>\r
                                </td>                   \r
index b2e63a60254ece83cbb0e9cf8e4ac183bbe2e21a..cd049f4d2308d92120041e0d162b8857ab42039b 100644 (file)
@@ -36,7 +36,6 @@ import org.apache.wicket.AttributeModifier;
 import org.apache.wicket.Component;\r
 import org.apache.wicket.MarkupContainer;\r
 import org.apache.wicket.PageParameters;\r
-import org.apache.wicket.RequestCycle;\r
 import org.apache.wicket.RestartResponseException;\r
 import org.apache.wicket.ajax.AjaxRequestTarget;\r
 import org.apache.wicket.behavior.SimpleAttributeModifier;\r
@@ -45,7 +44,6 @@ import org.apache.wicket.markup.html.image.ContextImage;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;\r
 import org.apache.wicket.markup.html.link.ExternalLink;\r
 import org.apache.wicket.markup.html.link.Link;\r
-import org.apache.wicket.markup.html.link.StatelessLink;\r
 import org.apache.wicket.markup.html.pages.RedirectPage;\r
 import org.apache.wicket.markup.html.panel.Fragment;\r
 import org.apache.wicket.markup.repeater.Item;\r
@@ -54,7 +52,6 @@ import org.apache.wicket.markup.repeater.data.ListDataProvider;
 import org.apache.wicket.model.Model;\r
 import org.apache.wicket.protocol.http.RequestUtils;\r
 import org.apache.wicket.protocol.http.WebRequest;\r
-import org.apache.wicket.request.target.basic.RedirectRequestTarget;\r
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;\r
 import org.eclipse.jgit.lib.PersonIdent;\r
 import org.eclipse.jgit.lib.Ref;\r
@@ -863,9 +860,6 @@ public class TicketPage extends RepositoryPage {
                                if (event.hasPatchset()) {\r
                                        // patchset\r
                                        Patchset patchset = event.patchset;\r
-                                       //In the case of using a cached change list\r
-                                       item.setVisible(!patchset.isDeleted());\r
-                                       \r
                                        String what;\r
                                        if (event.isStatusChange() && (Status.New == event.getStatus())) {\r
                                                what = getString("gb.proposedThisChange");\r
@@ -883,6 +877,7 @@ public class TicketPage extends RepositoryPage {
                                        LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,\r
                                                        ComparePage.class, WicketUtils.newRangeParameter(repositoryName, patchset.parent == null ? patchset.base : patchset.parent, patchset.tip), true);\r
                                        WicketUtils.setHtmlTooltip(psr, patchset.toString());\r
+                                       WicketUtils.setCssClass(psr, "aui-lozenge aui-lozenge-subtle");\r
                                        item.add(psr);\r
                                        String typeCss = getPatchsetTypeCss(patchset.type);\r
                                        Label typeLabel = new Label("patchsetType", patchset.type.toString());\r
@@ -907,6 +902,42 @@ public class TicketPage extends RepositoryPage {
                                        // comment\r
                                        item.add(new Label("what", getString("gb.commented")));\r
                                        item.add(new Label("patchsetRevision").setVisible(false));\r
+                                       item.add(new Label("patchsetType").setVisible(false));\r
+                                       item.add(new Label("deleteRevision").setVisible(false));\r
+                                       item.add(new Label("patchsetDiffStat").setVisible(false));\r
+                               } else if (event.hasReference()) {\r
+                                       // reference\r
+                                       switch (event.reference.getSourceType()) {\r
+                                               case Commit: {\r
+                                                       final int shaLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);\r
+                                                       \r
+                                                       item.add(new Label("what", getString("gb.referencedByCommit")));\r
+                                                       LinkPanel psr = new LinkPanel("patchsetRevision", null, event.reference.toString().substring(0, shaLen),\r
+                                                                       CommitPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.toString()), true);\r
+                                                       WicketUtils.setHtmlTooltip(psr, event.reference.toString());\r
+                                                       WicketUtils.setCssClass(psr, "ticketReference-commit shortsha1");\r
+                                                       item.add(psr);\r
+                                                       \r
+                                               } break;\r
+                                               \r
+                                               case Ticket: {\r
+                                                       final String text = MessageFormat.format("ticket/{0}", event.reference.ticketId);\r
+\r
+                                                       item.add(new Label("what", getString("gb.referencedByTicket")));\r
+                                                       //NOTE: Ideally reference the exact comment using reference.toString,\r
+                                                       //              however anchor hash is used and is escaped resulting in broken link\r
+                                                       LinkPanel psr = new LinkPanel("patchsetRevision", null,  text,\r
+                                                                       TicketsPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.ticketId.toString()), true);\r
+                                                       WicketUtils.setCssClass(psr, "ticketReference-comment");\r
+                                                       item.add(psr);\r
+                                               } break;\r
+                                       \r
+                                               default: {\r
+                                                       item.add(new Label("what").setVisible(false));\r
+                                                       item.add(new Label("patchsetRevision").setVisible(false));\r
+                                               }\r
+                                       }\r
+                                       \r
                                        item.add(new Label("patchsetType").setVisible(false));\r
                                        item.add(new Label("deleteRevision").setVisible(false));\r
                                        item.add(new Label("patchsetDiffStat").setVisible(false));\r
index 3318441a561e3efcbc3e6447f4924c59bbb08019..10c9a0e8888d15e0d04e00e3a11ac82e611fa37a 100644 (file)
@@ -2391,4 +2391,19 @@ table.filestore-status {
 .delete-patchset {\r
        color:#D51900;\r
        font-size: 1.2em;\r
+}\r
+\r
+.ticketReference-comment {\r
+    font-family: sans-serif;\r
+    font-weight: 200;\r
+    font-size: 1em;\r
+    font-variant: normal;\r
+    text-transform: none;\r
+}\r
+\r
+.ticketReference-commit {\r
+    font-family: monospace;\r
+    font-weight: 200;\r
+    font-size: 1em;\r
+    font-variant: normal;\r
 }
\ No newline at end of file
index 78e9ab957c926d4a37d132c9bc8c03eac163cdd9..ef6a6c5100f22d4bcf93fef21e4aea3dc7991fd9 100644 (file)
@@ -90,3 +90,5 @@ server.httpBindInterface = localhost
 server.httpsBindInterface = localhost
 server.storePassword = gitblit
 server.shutdownPort = 8081
+
+tickets.service = com.gitblit.tickets.BranchTicketService
index b01c82c469870988b50d1f11d273ea5be5e3c0b2..133be77fb6dc6e1f203d67947d24e28d213f3bb8 100644 (file)
@@ -66,7 +66,7 @@ import com.gitblit.utils.JGitUtils;
                ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,\r
                BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class,\r
                SshKeysDispatcherTest.class, UITicketTest.class, PathUtilsTest.class, SshKerberosAuthenticationTest.class,\r
-               GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class })\r
+               GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class, TicketReferenceTest.class })\r
 public class GitBlitSuite {\r
 \r
        public static final File BASEFOLDER = new File("data");\r
diff --git a/src/test/java/com/gitblit/tests/TicketReferenceTest.java b/src/test/java/com/gitblit/tests/TicketReferenceTest.java
new file mode 100644 (file)
index 0000000..934659c
--- /dev/null
@@ -0,0 +1,939 @@
+/*
+ * Copyright 2016 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.GitBlitException;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.models.TicketModel.Change;
+import com.gitblit.models.TicketModel.Field;
+import com.gitblit.models.TicketModel.Reference;
+import com.gitblit.tickets.ITicketService;
+
+/**
+ * Creates and deletes a range of ticket references via ticket comments and commits 
+ */
+public class TicketReferenceTest extends GitblitUnitTest {
+
+       static File workingCopy = new File(GitBlitSuite.REPOSITORIES, "working/TicketReferenceTest.git-wc");
+       
+       static ITicketService ticketService;
+       
+       static final String account = "TicketRefTest";
+       static final String password = GitBlitSuite.password;
+
+       static final String url = GitBlitSuite.gitServletUrl;
+       
+       static UserModel user = null;
+       static RepositoryModel repo = null;
+       static CredentialsProvider cp = null;
+       static Git git = null;
+       
+       @BeforeClass
+       public static void configure() throws Exception {
+               File repositoryName = new File("TicketReferenceTest.git");;
+               
+               GitBlitSuite.close(repositoryName);
+               if (repositoryName.exists()) {
+                       FileUtils.delete(repositoryName, FileUtils.RECURSIVE | FileUtils.RETRY);
+               }
+               repo = new RepositoryModel("TicketReferenceTest.git", null, null, null);
+               
+               if (gitblit().hasRepository(repo.name)) {
+                       gitblit().deleteRepositoryModel(repo);
+               }
+               
+               gitblit().updateRepositoryModel(repo.name, repo, true);
+               
+               user = new UserModel(account);
+               user.displayName = account;
+               user.emailAddress = account + "@example.com";
+               user.password = password;
+
+               cp = new UsernamePasswordCredentialsProvider(user.username, user.password);
+               
+               if (gitblit().getUserModel(user.username) != null) {
+                       gitblit().deleteUser(user.username);
+               }
+
+               repo.authorizationControl = AuthorizationControl.NAMED;
+               repo.accessRestriction = AccessRestrictionType.PUSH;
+               gitblit().updateRepositoryModel(repo.name, repo, false);
+               
+               // grant user push permission
+               user.setRepositoryPermission(repo.name, AccessPermission.REWIND);
+               gitblit().updateUserModel(user);
+
+               ticketService = gitblit().getTicketService();
+               assertTrue(ticketService.deleteAll(repo));
+               
+               GitBlitSuite.close(workingCopy);
+               if (workingCopy.exists()) {
+                       FileUtils.delete(workingCopy, FileUtils.RECURSIVE | FileUtils.RETRY);
+               }
+               
+               CloneCommand clone = Git.cloneRepository();
+               clone.setURI(MessageFormat.format("{0}/{1}", url, repo.name));
+               clone.setDirectory(workingCopy);
+               clone.setBare(false);
+               clone.setBranch("master");
+               clone.setCredentialsProvider(cp);
+               GitBlitSuite.close(clone.call());
+               
+               git = Git.open(workingCopy);
+               git.getRepository().getConfig().setString("user", null, "name", user.displayName);
+               git.getRepository().getConfig().setString("user", null, "email", user.emailAddress);
+               git.getRepository().getConfig().save();
+               
+               final RevCommit revCommit1 = makeCommit("initial commit");
+               final String initialSha = revCommit1.name();
+               Iterable<PushResult> results = git.push().setPushAll().setCredentialsProvider(cp).call();
+               GitBlitSuite.close(git);
+               for (PushResult result : results) {
+                       for (RemoteRefUpdate update : result.getRemoteUpdates()) {
+                               assertEquals(Status.OK, update.getStatus());
+                               assertEquals(initialSha, update.getNewObjectId().name());
+                       }
+               }
+       }
+       
+       @AfterClass
+       public static void cleanup() throws Exception {
+               GitBlitSuite.close(git);
+       }
+       
+       @Test
+       public void noReferencesOnTicketCreation() throws Exception {
+
+               TicketModel a = ticketService.createTicket(repo, newTicket("noReferencesOnCreation"));
+               assertNotNull(a);
+               assertFalse(a.hasReferences());
+               
+               //Ensure retrieval process doesn't affect anything
+               a = ticketService.getTicket(repo,  a.number);
+               assertNotNull(a);
+               assertFalse(a.hasReferences());
+       }
+       
+       
+       @Test
+       public void commentNoUnexpectedReference() throws Exception {
+               
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentNoUnexpectedReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commentNoUnexpectedReference-B"));
+               
+               assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for 1 - no reference")));
+               assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for # - no reference")));
+               assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for #42 - ignores invalid reference")));
+
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+
+               assertFalse(a.hasReferences());
+               assertFalse(b.hasReferences());
+       }
+       
+       @Test
+       public void commentNoSelfReference() throws Exception {
+                       
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentNoSelfReference-A"));
+               
+               final Change comment = newComment(String.format("comment for #%d - no self reference", a.number));
+               assertNotNull(ticketService.updateTicket(repo, a.number, comment)); 
+               
+               a = ticketService.getTicket(repo, a.number);
+               
+               assertFalse(a.hasReferences());
+       }
+       
+       @Test
+       public void commentSingleReference() throws Exception {
+               
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentSingleReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commentSingleReference-B"));
+               
+               final Change comment = newComment(String.format("comment for #%d - single reference", b.number));
+               assertNotNull(ticketService.updateTicket(repo, a.number, comment));
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertEquals(a.number, cRefB.get(0).ticketId.longValue());
+               assertEquals(comment.comment.id, cRefB.get(0).hash);
+       }
+               
+       @Test
+       public void commentSelfAndOtherReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentSelfAndOtherReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commentSelfAndOtherReference-B"));
+               
+               final Change comment = newComment(String.format("comment for #%d and #%d - self and other reference", a.number, b.number));
+               assertNotNull(ticketService.updateTicket(repo, a.number, comment));
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertEquals(a.number, cRefB.get(0).ticketId.longValue());
+               assertEquals(comment.comment.id, cRefB.get(0).hash);
+       }
+       
+       @Test
+       public void commentMultiReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentMultiReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commentMultiReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commentMultiReference-C"));
+               
+               final Change comment = newComment(String.format("comment for #%d and #%d - multi reference", b.number, c.number));
+               assertNotNull(ticketService.updateTicket(repo, a.number, comment));
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertTrue(c.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertEquals(a.number, cRefB.get(0).ticketId.longValue());
+               assertEquals(comment.comment.id, cRefB.get(0).hash);
+               
+               List<Reference> cRefC = c.getReferences();
+               assertNotNull(cRefC);
+               assertEquals(1, cRefC.size());
+               assertEquals(a.number, cRefC.get(0).ticketId.longValue());
+               assertEquals(comment.comment.id, cRefC.get(0).hash);
+       }
+       
+       
+
+       @Test
+       public void commitMasterNoUnexpectedReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commentMultiReference-A"));
+               
+               final String branchName = "master";
+               git.checkout().setCreateBranch(false).setName(branchName).call();
+               
+               makeCommit("commit for 1 - no reference");
+               makeCommit("comment for # - no reference");
+               final RevCommit revCommit1 = makeCommit("comment for #42 - ignores invalid reference");
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertFalse(a.hasReferences());
+       }
+       
+       @Test
+       public void commitMasterSingleReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterSingleReference-A"));
+               
+               final String branchName = "master";
+               git.checkout().setCreateBranch(false).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d - single reference", a.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertTrue(a.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+       }
+       
+       @Test
+       public void commitMasterMultiReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterMultiReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitMasterMultiReference-B"));
+               
+               final String branchName = "master";
+               git.checkout().setCreateBranch(false).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d and #%d - multi reference", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = a.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+       }
+       
+       @Test
+       public void commitMasterAmendReference() throws Exception {
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterAmendReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitMasterAmendReference-B"));
+               
+               final String branchName = "master";
+               git.checkout().setCreateBranch(false).setName(branchName).call();
+               
+               String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               //Confirm that old invalid references removed for both tickets
+               //and new reference added for one referenced ticket
+               message = String.format("commit after amend for #%d", a.number);
+               final String commit2Sha = amendCommit(message);
+               
+               assertForcePushSuccess(commit2Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               assertTrue(a.hasReferences());
+               assertFalse(b.hasReferences());
+               
+               cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit2Sha, cRefA.get(0).hash);
+       }
+       
+       
+       @Test
+       public void commitPatchsetNoUnexpectedReference() throws Exception {
+               setPatchsetAvailable(true);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetNoUnexpectedReference-A"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               makeCommit("commit for 1 - no reference");
+               makeCommit("commit for # - no reference");
+               final String message = "commit for #42 - ignores invalid reference";
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertFalse(a.hasReferences());
+       }
+
+       @Test
+       public void commitPatchsetNoSelfReference() throws Exception {
+               setPatchsetAvailable(true);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetNoSelfReference-A"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d - patchset self reference", a.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertFalse(a.hasReferences());
+       }
+       
+       @Test
+       public void commitPatchsetSingleReference() throws Exception {
+               setPatchsetAvailable(true);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetSingleReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetSingleReference-B"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d - patchset single reference", b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+       }
+       
+       @Test
+       public void commitPatchsetMultiReference() throws Exception {
+               setPatchsetAvailable(true);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-C"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d and #%d- patchset multi reference", b.number, c.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertTrue(c.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               List<Reference> cRefC = c.getReferences();
+               assertNotNull(cRefC);
+               assertEquals(1, cRefC.size());
+               assertNull(cRefC.get(0).ticketId);
+               assertEquals(commit1Sha, cRefC.get(0).hash);
+       }
+       
+       @Test
+       public void commitPatchsetAmendReference() throws Exception {
+               setPatchsetAvailable(true);
+
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-C"));
+               assertFalse(c.hasPatchsets());
+               
+               String branchName = String.format("ticket/%d", c.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               assertTrue(c.hasPatchsets());
+               assertNotNull(c.getPatchset(1, 1));
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               //As a new patchset is created the references will remain until deleted
+               message = String.format("commit after amend for #%d", a.number);
+               final String commit2Sha = amendCommit(message);
+
+               assertForcePushSuccess(commit2Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               assertNotNull(c.getPatchset(1, 1));
+               assertNotNull(c.getPatchset(2, 1));
+               
+               cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(2, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertNull(cRefA.get(1).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               assertEquals(commit2Sha, cRefA.get(1).hash);
+               
+               cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               //Delete the original patchset and confirm old references are removed
+               ticketService.deletePatchset(c, c.getPatchset(1, 1), user.username);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertFalse(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               assertNull(c.getPatchset(1, 1));
+               assertNotNull(c.getPatchset(2, 1));
+               
+               cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit2Sha, cRefA.get(0).hash);
+       }
+               
+       
+       @Test
+       public void commitTicketBranchNoUnexpectedReference() throws Exception {
+               setPatchsetAvailable(false);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchNoUnexpectedReference-A"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               makeCommit("commit for 1 - no reference");
+               makeCommit("commit for # - no reference");
+               final String message = "commit for #42 - ignores invalid reference";
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertFalse(a.hasReferences());
+       }
+
+       @Test
+       public void commitTicketBranchSelfReference() throws Exception {
+               setPatchsetAvailable(false);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchSelfReference-A"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d - patchset self reference", a.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               assertTrue(a.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+       }
+       
+       @Test
+       public void commitTicketBranchSingleReference() throws Exception {
+               setPatchsetAvailable(false);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchSingleReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchSingleReference-B"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d - patchset single reference", b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+       }
+       
+       @Test
+       public void commitTicketBranchMultiReference() throws Exception {
+               setPatchsetAvailable(false);
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-C"));
+               
+               String branchName = String.format("ticket/%d", a.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               final String message = String.format("commit for #%d and #%d- patchset multi reference", b.number, c.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertFalse(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertTrue(c.hasReferences());
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               List<Reference> cRefC = c.getReferences();
+               assertNotNull(cRefC);
+               assertEquals(1, cRefC.size());
+               assertNull(cRefC.get(0).ticketId);
+               assertEquals(commit1Sha, cRefC.get(0).hash);
+       }
+       
+       @Test
+       public void commitTicketBranchAmendReference() throws Exception {
+               setPatchsetAvailable(false);
+
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-C"));
+               assertFalse(c.hasPatchsets());
+               
+               String branchName = String.format("ticket/%d", c.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               assertFalse(c.hasPatchsets());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               //Confirm that old invalid references removed for both tickets
+               //and new reference added for one referenced ticket
+               message = String.format("commit after amend for #%d", a.number);
+               final String commit2Sha = amendCommit(message);
+               
+               assertForcePushSuccess(commit2Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertFalse(b.hasReferences());
+               assertFalse(c.hasReferences());
+               assertFalse(c.hasPatchsets());
+               
+               cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit2Sha, cRefA.get(0).hash);
+       }
+       
+       @Test
+       public void commitTicketBranchDeleteNoMergeReference() throws Exception {
+               setPatchsetAvailable(false);
+
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-C"));
+               assertFalse(c.hasPatchsets());
+               
+               String branchName = String.format("ticket/%d", c.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               //Confirm that old invalid references removed for both tickets
+               assertDeleteBranch(branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertFalse(a.hasReferences());
+               assertFalse(b.hasReferences());
+               assertFalse(c.hasReferences());
+       }
+       
+       @Test
+       public void commitTicketBranchDeletePostMergeReference() throws Exception {
+               setPatchsetAvailable(false);
+
+               TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-A"));
+               TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-B"));
+               TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-C"));
+               assertFalse(c.hasPatchsets());
+               
+               String branchName = String.format("ticket/%d", c.number);
+               git.checkout().setCreateBranch(true).setName(branchName).call();
+               
+               String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
+               final RevCommit revCommit1 = makeCommit(message);
+               final String commit1Sha = revCommit1.name();
+               assertPushSuccess(commit1Sha, branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               List<Reference> cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               List<Reference> cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+               
+               git.checkout().setCreateBranch(false).setName("refs/heads/master").call();
+               
+               // merge the tip of the branch into master
+               MergeResult mergeResult = git.merge().setFastForward(FastForwardMode.NO_FF).include(revCommit1.getId()).call();
+               assertEquals(MergeResult.MergeStatus.MERGED, mergeResult.getMergeStatus());
+
+               // push the merged master to the origin
+               Iterable<PushResult> results = git.push().setCredentialsProvider(cp).setRemote("origin").call();
+               for (PushResult result : results) {
+                       RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");
+                       assertEquals(Status.OK, ref.getStatus());
+               }
+               
+               //As everything has been merged no references should be changed
+               assertDeleteBranch(branchName);
+               
+               a = ticketService.getTicket(repo, a.number);
+               b = ticketService.getTicket(repo, b.number);
+               c = ticketService.getTicket(repo, c.number);
+               assertTrue(a.hasReferences());
+               assertTrue(b.hasReferences());
+               assertFalse(c.hasReferences());
+               
+               cRefA = a.getReferences();
+               assertNotNull(cRefA);
+               assertEquals(1, cRefA.size());
+               assertNull(cRefA.get(0).ticketId);
+               assertEquals(commit1Sha, cRefA.get(0).hash);
+               
+               cRefB = b.getReferences();
+               assertNotNull(cRefB);
+               assertEquals(1, cRefB.size());
+               assertNull(cRefB.get(0).ticketId);
+               assertEquals(commit1Sha, cRefB.get(0).hash);
+       }
+       
+       private static Change newComment(String text) {
+               Change change = new Change("JUnit");
+               change.comment(text);
+               return change;
+       }
+       
+       private static Change newTicket(String title) {
+               Change change = new Change("JUnit");
+               change.setField(Field.title, title);
+               change.setField(Field.type, TicketModel.Type.Bug );
+               return change;
+       }
+       
+       private static RevCommit makeCommit(String message) throws Exception {
+               File file = new File(workingCopy, "testFile.txt");
+               OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
+               BufferedWriter w = new BufferedWriter(os);
+               w.write("// " + new Date().toString() + "\n");
+               w.close();
+               git.add().addFilepattern(file.getName()).call();
+               RevCommit rev = git.commit().setMessage(message).call();
+               return rev;
+       }
+       
+       private static String amendCommit(String message) throws Exception {
+               File file = new File(workingCopy, "testFile.txt");
+               OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
+               BufferedWriter w = new BufferedWriter(os);
+               w.write("// " + new Date().toString() + "\n");
+               w.close();
+               git.add().addFilepattern(file.getName()).call();
+               RevCommit rev = git.commit().setAmend(true).setMessage(message).call();
+               return rev.getId().name();
+       }
+       
+       
+       private void setPatchsetAvailable(boolean state) throws GitBlitException {
+               repo.acceptNewPatchsets = state;
+               gitblit().updateRepositoryModel(repo.name, repo, false);
+       }
+       
+       
+       private void assertPushSuccess(String commitSha, String branchName) throws Exception {
+               Iterable<PushResult> results = git.push().setRemote("origin").setCredentialsProvider(cp).call();
+               
+               for (PushResult result : results) {
+                       RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
+                       assertEquals(Status.OK, ref.getStatus());
+                       assertEquals(commitSha, ref.getNewObjectId().name());
+               }
+       }
+       
+       private void assertForcePushSuccess(String commitSha, String branchName) throws Exception {
+               Iterable<PushResult> results = git.push().setForce(true).setRemote("origin").setCredentialsProvider(cp).call();
+               
+               for (PushResult result : results) {
+                       RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
+                       assertEquals(Status.OK, ref.getStatus());
+                       assertEquals(commitSha, ref.getNewObjectId().name());
+               }
+       }
+       
+       private void assertDeleteBranch(String branchName) throws Exception {
+               
+               RefSpec refSpec = new RefSpec()
+        .setSource(null)
+        .setDestination("refs/heads/" + branchName);
+               
+               Iterable<PushResult> results = git.push().setRefSpecs(refSpec).setRemote("origin").setCredentialsProvider(cp).call();
+               
+               for (PushResult result : results) {
+                       RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
+                       assertEquals(Status.OK, ref.getStatus());
+               }
+       }
+}
\ No newline at end of file