From c2188a840bc4153ae92112b04b2e06a90d3944aa Mon Sep 17 00:00:00 2001
From: Paul Martin <paul@paulsputer.com>
Date: Wed, 27 Apr 2016 18:58:06 -0400
Subject: [PATCH] Ticket Reference handling #1048

---
 src/main/java/com/gitblit/git/PatchsetReceivePack.java    |  331 ++++----
 src/main/java/com/gitblit/git/GitblitReceivePack.java     |  243 ++++++
 src/main/java/com/gitblit/models/TicketModel.java         |  218 ++++++
 src/main/java/com/gitblit/wicket/pages/TicketPage.java    |   43 +
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties |    4 
 src/main/java/com/gitblit/wicket/pages/TicketPage.html    |    2 
 src/test/java/com/gitblit/tests/GitBlitSuite.java         |    2 
 src/main/java/com/gitblit/utils/JGitUtils.java            |  171 ++++
 src/main/java/com/gitblit/tickets/ITicketService.java     |  107 ++
 src/main/resources/gitblit.css                            |   15 
 src/main/distrib/data/defaults.properties                 |    7 
 src/test/java/com/gitblit/tests/TicketReferenceTest.java  |  939 ++++++++++++++++++++++++++
 src/test/config/test-gitblit.properties                   |    2 
 src/main/java/com/gitblit/tickets/TicketNotifier.java     |   13 
 14 files changed, 1,895 insertions(+), 202 deletions(-)

diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties
index 403b741..0c7d6cd 100644
--- a/src/main/distrib/data/defaults.properties
+++ b/src/main/distrib/data/defaults.properties
@@ -574,6 +574,13 @@
 # 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
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePack.java b/src/main/java/com/gitblit/git/GitblitReceivePack.java
index 34bbea2..f271f6f 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePack.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePack.java
@@ -22,18 +22,28 @@
 import java.io.File;
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -50,14 +60,24 @@
 import com.gitblit.extensions.ReceiveHook;
 import com.gitblit.manager.IGitblit;
 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.Patchset;
+import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.TicketModel.TicketAction;
+import com.gitblit.models.TicketModel.TicketLink;
 import com.gitblit.tickets.BranchTicketService;
+import com.gitblit.tickets.ITicketService;
+import com.gitblit.tickets.TicketNotifier;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.ClientLogger;
 import com.gitblit.utils.CommitCache;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.RefLogUtils;
 import com.gitblit.utils.StringUtils;
+import com.google.common.collect.Lists;
 
 
 /**
@@ -92,6 +112,11 @@
 	protected final IStoredSettings settings;
 
 	protected final IGitblit gitblit;
+	
+	protected final ITicketService ticketService;
+
+	protected final TicketNotifier ticketNotifier;
+	
 
 	public GitblitReceivePack(
 			IGitblit gitblit,
@@ -114,6 +139,14 @@
 		} catch (IOException e) {
 		}
 
+		if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) {
+			this.ticketService = gitblit.getTicketService();
+			this.ticketNotifier = this.ticketService.createNotifier();
+		} else {
+			this.ticketService = null;
+			this.ticketNotifier = null;
+		}
+		
 		// set advanced ref permissions
 		setAllowCreates(user.canCreateRef(repository));
 		setAllowDeletes(user.canDeleteRef(repository));
@@ -500,6 +533,104 @@
 				}
 			}
 		}
+		
+		//
+		// if there are ref update receive commands that were
+		// successfully processed and there is an active ticket service for the repository
+		// then process any referenced tickets
+		//
+		if (ticketService != null) {
+			List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
+			if (!allUpdates.isEmpty()) {
+				int ticketsProcessed = 0;
+				for (ReceiveCommand cmd : allUpdates) {
+					switch (cmd.getType()) {
+					case CREATE:
+					case UPDATE:
+						if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
+							Collection<TicketModel> tickets = processReferencedTickets(cmd);
+							ticketsProcessed += tickets.size();
+							for (TicketModel ticket : tickets) {
+								ticketNotifier.queueMailing(ticket);
+							}
+						}
+						break;
+						
+					case UPDATE_NONFASTFORWARD:
+						if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
+							String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
+							List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
+							for (TicketLink link : deletedRefs) {
+								link.isDelete = true;
+							}
+							Change deletion = new Change(user.username);
+							deletion.pendingLinks = deletedRefs;
+							ticketService.updateTicket(repository, 0, deletion);
+							
+							Collection<TicketModel> tickets = processReferencedTickets(cmd);
+							ticketsProcessed += tickets.size();
+							for (TicketModel ticket : tickets) {
+								ticketNotifier.queueMailing(ticket);
+							}
+						}
+						break;
+					case DELETE:
+						//Identify if the branch has been merged 
+						SortedMap<Integer, String> bases =  new TreeMap<Integer, String>();
+						try {
+							ObjectId dObj = cmd.getOldId();
+							Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values();
+							for (Ref ref : tips) {
+								ObjectId iObj = ref.getObjectId();
+								String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj);
+								if (mergeBase != null) {
+									int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name());
+									bases.put(d, mergeBase);
+									//All commits have been merged into some other branch
+									if (d == 0) {
+										break;
+									}
+								}
+							}
+							
+							if (bases.isEmpty()) {
+								//TODO: Handle orphan branch case
+							} else {
+								if (bases.firstKey() > 0) {
+									//Delete references from the remaining commits that haven't been merged
+									String mergeBase = bases.get(bases.firstKey());
+									List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(),
+											settings, mergeBase, dObj.name());
+									
+									for (TicketLink link : deletedRefs) {
+										link.isDelete = true;
+									}
+									Change deletion = new Change(user.username);
+									deletion.pendingLinks = deletedRefs;
+									ticketService.updateTicket(repository, 0, deletion);
+								}
+							}
+							
+						} catch (IOException e) {
+							LOGGER.error(null, e);
+						}
+						break;
+						
+					default:
+						break;
+					}
+				}
+	
+				if (ticketsProcessed == 1) {
+					sendInfo("1 ticket updated");
+				} else if (ticketsProcessed > 1) {
+					sendInfo("{0} tickets updated", ticketsProcessed);
+				}
+			}
+	
+			// reset the ticket caches for the repository
+			ticketService.resetCaches(repository);
+		}
 	}
 
 	protected void setGitblitUrl(String url) {
@@ -616,4 +747,116 @@
 	public UserModel getUserModel() {
 		return user;
 	}
+	
+	/**
+	 * Automatically closes open tickets and adds references to tickets if made in the commit message.
+	 *
+	 * @param cmd
+	 */
+	private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
+		Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();
+
+		final RevWalk rw = getRevWalk();
+		try {
+			rw.reset();
+			rw.markStart(rw.parseCommit(cmd.getNewId()));
+			if (!ObjectId.zeroId().equals(cmd.getOldId())) {
+				rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
+			}
+
+			RevCommit c;
+			while ((c = rw.next()) != null) {
+				rw.parseBody(c);
+				List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
+				if (ticketLinks == null) {
+					continue;
+				}
+
+				for (TicketLink link : ticketLinks) {
+					
+					TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
+					if (ticket == null) {
+						continue;
+					}
+					
+					Change change = null;
+					String commitSha = c.getName();
+					String branchName = Repository.shortenRefName(cmd.getRefName());
+					
+					switch (link.action) {
+						case Commit: {
+							//A commit can reference a ticket in any branch even if the ticket is closed.
+							//This allows developers to identify and communicate related issues
+							change = new Change(user.username);
+							change.referenceCommit(commitSha);
+						} break;
+						
+						case Close: {
+							// As this isn't a patchset theres no merging taking place when closing a ticket
+							if (ticket.isClosed()) {
+								continue;
+							}
+							
+							change = new Change(user.username);
+							change.setField(Field.status, Status.Fixed);
+							
+							if (StringUtils.isEmpty(ticket.responsible)) {
+								// unassigned tickets are assigned to the closer
+								change.setField(Field.responsible, user.username);
+							}
+						}
+						
+						default: {
+							//No action
+						} break;
+					}
+					
+					if (change != null) {
+						ticket = ticketService.updateTicket(repository, ticket.number, change);
+					}
+	
+					if (ticket != null) {
+						sendInfo("");
+						sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
+
+						switch (link.action) {
+							case Commit: {
+								sendInfo("referenced by push of {0} to {1}", commitSha, branchName);
+								changedTickets.put(ticket.number, ticket);
+							} break;
+
+							case Close: {
+								sendInfo("closed by push of {0} to {1}", commitSha, branchName);
+								changedTickets.put(ticket.number, ticket);
+							} break;
+
+							default: { }
+						}
+
+						sendInfo(ticketService.getTicketUrl(ticket));
+						sendInfo("");
+					} else {
+						switch (link.action) {
+							case Commit: {
+								sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);
+							} break;
+							
+							case Close: {
+								sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha);	
+							} break;
+							
+							default: { }
+						}
+					}
+				}
+			}
+				
+		} catch (IOException e) {
+			LOGGER.error("Can't scan for changes to reference or close", e);
+		} finally {
+			rw.reset();
+		}
+
+		return changedTickets.values();
+	}
 }
diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index ef0b409..33fa470 100644
--- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java
+++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -30,7 +30,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -60,6 +59,8 @@
 import com.gitblit.models.TicketModel.Patchset;
 import com.gitblit.models.TicketModel.PatchsetType;
 import com.gitblit.models.TicketModel.Status;
+import com.gitblit.models.TicketModel.TicketAction;
+import com.gitblit.models.TicketModel.TicketLink;
 import com.gitblit.models.UserModel;
 import com.gitblit.tickets.BranchTicketService;
 import com.gitblit.tickets.ITicketService;
@@ -485,9 +486,27 @@
 				switch (cmd.getType()) {
 				case CREATE:
 				case UPDATE:
+					if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
+						Collection<TicketModel> tickets = processReferencedTickets(cmd);
+						ticketsProcessed += tickets.size();
+						for (TicketModel ticket : tickets) {
+							ticketNotifier.queueMailing(ticket);
+						}
+					}
+					break;
+					
 				case UPDATE_NONFASTFORWARD:
 					if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
-						Collection<TicketModel> tickets = processMergedTickets(cmd);
+						String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
+						List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
+						for (TicketLink link : deletedRefs) {
+							link.isDelete = true;
+						}
+						Change deletion = new Change(user.username);
+						deletion.pendingLinks = deletedRefs;
+						ticketService.updateTicket(repository, 0, deletion);
+
+						Collection<TicketModel> tickets = processReferencedTickets(cmd);
 						ticketsProcessed += tickets.size();
 						for (TicketModel ticket : tickets) {
 							ticketNotifier.queueMailing(ticket);
@@ -604,15 +623,17 @@
 				return null;
 			}
 		}
-
+		
 		// check to see if this commit is already linked to a ticket
-		long id = identifyTicket(tipCommit, false);
-		if (id > 0) {
-			sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, id);
+		if (ticket != null && 
+				JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {
+			sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);
 			sendRejection(cmd, "everything up-to-date");
 			return null;
 		}
-
+		
+		List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
+		
 		PatchsetCommand psCmd;
 		if (ticket == null) {
 			/*
@@ -802,6 +823,10 @@
 			}
 			break;
 		}
+
+		Change change = psCmd.getChange();
+		change.pendingLinks = ticketLinks;
+
 		return psCmd;
 	}
 
@@ -890,11 +915,11 @@
 
 	/**
 	 * Automatically closes open tickets that have been merged to their integration
-	 * branch by a client.
+	 * branch by a client and adds references to tickets if made in the commit message.
 	 *
 	 * @param cmd
 	 */
-	private Collection<TicketModel> processMergedTickets(ReceiveCommand cmd) {
+	private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
 		Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
 		final RevWalk rw = getRevWalk();
 		try {
@@ -907,105 +932,151 @@
 			RevCommit c;
 			while ((c = rw.next()) != null) {
 				rw.parseBody(c);
-				long ticketNumber = identifyTicket(c, true);
-				if (ticketNumber == 0L || mergedTickets.containsKey(ticketNumber)) {
+				List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
+				if (ticketLinks == null) {
 					continue;
 				}
 
-				TicketModel ticket = ticketService.getTicket(repository, ticketNumber);
-				if (ticket == null) {
-					continue;
-				}
-				String integrationBranch;
-				if (StringUtils.isEmpty(ticket.mergeTo)) {
-					// unspecified integration branch
-					integrationBranch = null;
-				} else {
-					// specified integration branch
-					integrationBranch = Constants.R_HEADS + ticket.mergeTo;
-				}
-
-				// ticket must be open and, if specified, the ref must match the integration branch
-				if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
-					continue;
-				}
-
-				String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
-				boolean knownPatchset = false;
-				Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
-				if (refs != null) {
-					for (Ref ref : refs) {
-						if (ref.getName().startsWith(baseRef)) {
-							knownPatchset = true;
-							break;
-						}
-					}
-				}
-
-				String mergeSha = c.getName();
-				String mergeTo = Repository.shortenRefName(cmd.getRefName());
-				Change change;
-				Patchset patchset;
-				if (knownPatchset) {
-					// identify merged patchset by the patchset tip
-					patchset = null;
-					for (Patchset ps : ticket.getPatchsets()) {
-						if (ps.tip.equals(mergeSha)) {
-							patchset = ps;
-							break;
-						}
-					}
-
-					if (patchset == null) {
-						// should not happen - unless ticket has been hacked
-						sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
-								mergeSha, ticket.number);
+				for (TicketLink link : ticketLinks) {
+					
+					if (mergedTickets.containsKey(link.targetTicketId)) {
 						continue;
 					}
+	
+					TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
+					if (ticket == null) {
+						continue;
+					}
+					String integrationBranch;
+					if (StringUtils.isEmpty(ticket.mergeTo)) {
+						// unspecified integration branch
+						integrationBranch = null;
+					} else {
+						// specified integration branch
+						integrationBranch = Constants.R_HEADS + ticket.mergeTo;
+					}
+	
+					Change change;
+					Patchset patchset = null;
+					String mergeSha = c.getName();
+					String mergeTo = Repository.shortenRefName(cmd.getRefName());
 
-					// create a new change
-					change = new Change(user.username);
-				} else {
-					// new patchset pushed by user
-					String base = cmd.getOldId().getName();
-					patchset = newPatchset(ticket, base, mergeSha);
-					PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
-					psCmd.updateTicket(c, mergeTo, ticket, null);
+					if (link.action == TicketAction.Commit) {
+						//A commit can reference a ticket in any branch even if the ticket is closed.
+						//This allows developers to identify and communicate related issues
+						change = new Change(user.username);
+						change.referenceCommit(mergeSha);
+					} else {
+						// ticket must be open and, if specified, the ref must match the integration branch
+						if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
+							continue;
+						}
+	
+						String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
+						boolean knownPatchset = false;
+						Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
+						if (refs != null) {
+							for (Ref ref : refs) {
+								if (ref.getName().startsWith(baseRef)) {
+									knownPatchset = true;
+									break;
+								}
+							}
+						}
+	
+						if (knownPatchset) {
+							// identify merged patchset by the patchset tip
+							for (Patchset ps : ticket.getPatchsets()) {
+								if (ps.tip.equals(mergeSha)) {
+									patchset = ps;
+									break;
+								}
+							}
+	
+							if (patchset == null) {
+								// should not happen - unless ticket has been hacked
+								sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
+										mergeSha, ticket.number);
+								continue;
+							}
+	
+							// create a new change
+							change = new Change(user.username);
+						} else {
+							// new patchset pushed by user
+							String base = cmd.getOldId().getName();
+							patchset = newPatchset(ticket, base, mergeSha);
+							PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
+							psCmd.updateTicket(c, mergeTo, ticket, null);
+	
+							// create a ticket patchset ref
+							updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
+							RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
+							updateReflog(ru);
+	
+							// create a change from the patchset command
+							change = psCmd.getChange();
+						}
+	
+						// set the common change data about the merge
+						change.setField(Field.status, Status.Merged);
+						change.setField(Field.mergeSha, mergeSha);
+						change.setField(Field.mergeTo, mergeTo);
+	
+						if (StringUtils.isEmpty(ticket.responsible)) {
+							// unassigned tickets are assigned to the closer
+							change.setField(Field.responsible, user.username);
+						}
+					}
+	
+					ticket = ticketService.updateTicket(repository, ticket.number, change);
+	
+					if (ticket != null) {
+						sendInfo("");
+						sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
 
-					// create a ticket patchset ref
-					updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
-					RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
-					updateReflog(ru);
+						switch (link.action) {
+							case Commit: {
+								sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
+							}
+							break;
 
-					// create a change from the patchset command
-					change = psCmd.getChange();
-				}
+							case Close: {
+								sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
+								mergedTickets.put(ticket.number, ticket);	
+							}
+							break;
 
-				// set the common change data about the merge
-				change.setField(Field.status, Status.Merged);
-				change.setField(Field.mergeSha, mergeSha);
-				change.setField(Field.mergeTo, mergeTo);
+							default: {
+								
+							}
+						}
 
-				if (StringUtils.isEmpty(ticket.responsible)) {
-					// unassigned tickets are assigned to the closer
-					change.setField(Field.responsible, user.username);
-				}
+						sendInfo(ticketService.getTicketUrl(ticket));
+						sendInfo("");
 
-				ticket = ticketService.updateTicket(repository, ticket.number, change);
-				if (ticket != null) {
-					sendInfo("");
-					sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
-					sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
-					sendInfo(ticketService.getTicketUrl(ticket));
-					sendInfo("");
-					mergedTickets.put(ticket.number, ticket);
-				} else {
-					String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
-					sendError("FAILED to close ticket {0,number,0} by push of {1}", ticketNumber, shortid);
+					} else {
+						String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
+						
+						switch (link.action) {
+							case Commit: {
+								sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
+							}
+							break;
+							case Close: {
+								sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);	
+							} break;
+							
+							default: {
+								
+							}
+						}
+					}
 				}
 			}
+				
 		} catch (IOException e) {
-			LOGGER.error("Can't scan for changes to close", e);
+			LOGGER.error("Can't scan for changes to reference or close", e);
 		} finally {
 			rw.reset();
 		}
@@ -1013,75 +1084,9 @@
 		return mergedTickets.values();
 	}
 
-	/**
-	 * Try to identify a ticket id from the commit.
-	 *
-	 * @param commit
-	 * @param parseMessage
-	 * @return a ticket id or 0
-	 */
-	private long identifyTicket(RevCommit commit, boolean parseMessage) {
-		// try lookup by change ref
-		Map<AnyObjectId, Set<Ref>> map = getRepository().getAllRefsByPeeledObjectId();
-		Set<Ref> refs = map.get(commit.getId());
-		if (!ArrayUtils.isEmpty(refs)) {
-			for (Ref ref : refs) {
-				long number = PatchsetCommand.getTicketNumber(ref.getName());
-				if (number > 0) {
-					return number;
-				}
-			}
-		}
+	
 
-		if (parseMessage) {
-			// parse commit message looking for fixes/closes #n
-			String dx = "(?:fixes|closes)[\\s-]+#?(\\d+)";
-			String x = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, dx);
-			if (StringUtils.isEmpty(x)) {
-				x = dx;
-			}
-			try {
-				Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
-				Matcher m = p.matcher(commit.getFullMessage());
-				while (m.find()) {
-					String val = m.group(1);
-					return Long.parseLong(val);
-				}
-			} catch (Exception e) {
-				LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", x, commit.getName()), e);
-			}
-		}
-		return 0L;
-	}
-
-	private int countCommits(String baseId, String tipId) {
-		int count = 0;
-		RevWalk walk = getRevWalk();
-		walk.reset();
-		walk.sort(RevSort.TOPO);
-		walk.sort(RevSort.REVERSE, true);
-		try {
-			RevCommit tip = walk.parseCommit(getRepository().resolve(tipId));
-			RevCommit base = walk.parseCommit(getRepository().resolve(baseId));
-			walk.markStart(tip);
-			walk.markUninteresting(base);
-			for (;;) {
-				RevCommit c = walk.next();
-				if (c == null) {
-					break;
-				}
-				count++;
-			}
-		} catch (IOException e) {
-			// Should never happen, the core receive process would have
-			// identified the missing object earlier before we got control.
-			LOGGER.error("failed to get commit count", e);
-			return 0;
-		} finally {
-			walk.close();
-		}
-		return count;
-	}
+	
 
 	/**
 	 * Creates a new patchset with metadata.
@@ -1091,7 +1096,7 @@
 	 * @param tip
 	 */
 	private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
-		int totalCommits = countCommits(mergeBase, tip);
+		int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);
 
 		Patchset newPatchset = new Patchset();
 		newPatchset.tip = tip;
diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java
index 7495448..d534589 100644
--- a/src/main/java/com/gitblit/models/TicketModel.java
+++ b/src/main/java/com/gitblit/models/TicketModel.java
@@ -107,6 +107,7 @@
 		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 @@
 					
 					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,9 +180,15 @@
 		// 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);
 		}
@@ -354,6 +373,15 @@
 		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 @@
 		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 @@
 			}
 		}
 
-		// 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 Comment comment;
 
+		public Reference reference;
+
 		public Map<Field, String> fields;
 
 		public Set<Attachment> attachments;
@@ -654,6 +698,10 @@
 		public Review review;
 
 		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 boolean hasPatchset() {
-			return patchset != null;
+			return patchset != null && !patchset.isDeleted();
 		}
 
 		public boolean hasReview() {
@@ -688,11 +736,42 @@
 		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 @@
 			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 @@
 			}
 			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 @@
 				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 @@
 			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 {
 
diff --git a/src/main/java/com/gitblit/tickets/ITicketService.java b/src/main/java/com/gitblit/tickets/ITicketService.java
index e831003..20b6505 100644
--- a/src/main/java/com/gitblit/tickets/ITicketService.java
+++ b/src/main/java/com/gitblit/tickets/ITicketService.java
@@ -50,9 +50,11 @@
 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 @@
 	}
 
 	/**
-	 * 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 @@
 			throw new RuntimeException("must specify a change author!");
 		}
 
-		TicketKey key = new TicketKey(repository, ticketId);
-		ticketsCache.invalidate(key);
-
-		boolean success = commitChangeImpl(repository, ticketId, change);
-		if (success) {
-			TicketModel 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);
+		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);
+						}
 					}
 				}
 			}
-			return ticket;
 		}
-		return null;
+		
+		if (success) {
+			//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;
+						}
+
+						if (updateTicket(repository, link.targetTicketId, dstChange) != null) {
+							link.success = true;
+						}
+					}
+				}
+			}
+		}
+		
+		return ticket;
 	}
 
 	/**
@@ -1232,9 +1284,18 @@
 		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;
 	} 
diff --git a/src/main/java/com/gitblit/tickets/TicketNotifier.java b/src/main/java/com/gitblit/tickets/TicketNotifier.java
index 5979cf2..8c7fe6d 100644
--- a/src/main/java/com/gitblit/tickets/TicketNotifier.java
+++ b/src/main/java/com/gitblit/tickets/TicketNotifier.java
@@ -317,6 +317,19 @@
 			// 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.";
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index adcbb4d..a02fc3f 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -28,6 +28,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -46,12 +47,15 @@
 import org.eclipse.jgit.diff.RawTextComparator;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
 import org.eclipse.jgit.errors.StopWalkException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
@@ -91,19 +95,22 @@
 import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.FS;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.gitblit.GitBlit;
 import com.gitblit.GitBlitException;
-import com.gitblit.manager.GitblitManager;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.git.PatchsetCommand;
 import com.gitblit.models.FilestoreModel;
 import com.gitblit.models.GitNote;
 import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.TicketModel.TicketAction;
+import com.gitblit.models.TicketModel.TicketLink;
 import com.gitblit.models.RefModel;
 import com.gitblit.models.SubmoduleModel;
-import com.gitblit.servlet.FilestoreServlet;
 import com.google.common.base.Strings;
 
 /**
@@ -2740,5 +2747,163 @@
 		}
 		return false;
 	}
+	
+	/*
+	 * Identify ticket by considering the branch the commit is on
+	 * 
+	 * @param repository
+	 * @param commit 
+	 * @return ticket number, or 0 if no ticket
+	 */
+	public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) {
+		// try lookup by change ref
+		Map<AnyObjectId, Set<Ref>> map = repository.getAllRefsByPeeledObjectId();
+		Set<Ref> refs = map.get(commit.getId());
+		if (!ArrayUtils.isEmpty(refs)) {
+			for (Ref ref : refs) {
+				long number = PatchsetCommand.getTicketNumber(ref.getName());
+				
+				if (number > 0) {
+					return number;
+				}
+			}
+		}
+		
+		return 0;
+	}
+	
+	
+	/**
+	 * Try to identify all referenced tickets from the commit.
+	 *
+	 * @param commit
+	 * @return a collection of TicketLinks
+	 */
+	@NotNull
+	public static List<TicketLink> identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings,
+			RevCommit commit) {
+		List<TicketLink> ticketLinks = new ArrayList<TicketLink>();
+		List<Long> linkedTickets = new ArrayList<Long>();
 
+		// parse commit message looking for fixes/closes #n
+		final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)";
+		String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault);
+		if (StringUtils.isEmpty(xFix)) {
+			xFix = xFixDefault;
+		}
+		try {
+			Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE);
+			Matcher m = p.matcher(commit.getFullMessage());
+			while (m.find()) {
+				String val = m.group(1);
+				long number = Long.parseLong(val); 
+				
+				if (number > 0) {
+					ticketLinks.add(new TicketLink(number, TicketAction.Close));
+					linkedTickets.add(number);
+				}
+			}
+		} catch (Exception e) {
+			LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e);
+		}
+		
+		// parse commit message looking for ref #n
+		final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";
+		String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault);
+		if (StringUtils.isEmpty(xRef)) {
+			xRef = xRefDefault;
+		}
+		try {
+			Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE);
+			Matcher m = p.matcher(commit.getFullMessage());
+			while (m.find()) {
+				String val = m.group(1);
+				long number = Long.parseLong(val); 
+				//Most generic case so don't included tickets more precisely linked
+				if ((number > 0) && (!linkedTickets.contains(number))) {
+					ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName()));
+					linkedTickets.add(number);
+				}
+			}
+		} catch (Exception e) {
+			LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e);
+		}
+
+		return ticketLinks;
+	}
+	
+	/**
+	 * Try to identify all referenced tickets between two commits
+	 *
+	 * @param commit
+	 * @param parseMessage
+	 * @param currentTicketId, or 0 if not on a ticket branch
+	 * @return a collection of TicketLink, or null if commit is already linked
+	 */
+	public static List<TicketLink> identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings,
+			String baseSha, String tipSha) {
+		List<TicketLink> links = new ArrayList<TicketLink>();
+		if (repository == null) { return links; }
+		
+		RevWalk walk = new RevWalk(repository);
+		walk.sort(RevSort.TOPO);
+		walk.sort(RevSort.REVERSE, true);
+		try {
+			RevCommit tip = walk.parseCommit(repository.resolve(tipSha));
+			RevCommit base = walk.parseCommit(repository.resolve(baseSha));
+			walk.markStart(tip);
+			walk.markUninteresting(base);
+			for (;;) {
+				RevCommit commit = walk.next();
+				if (commit == null) {
+					break;
+				}
+				links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit));
+			}
+		} catch (IOException e) {
+			LOGGER.error("failed to identify tickets between commits.", e);
+		} finally {
+			walk.dispose();
+		}
+		
+		return links;
+	}
+	
+	public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) {
+		int count = 0;
+		walk.reset();
+		walk.sort(RevSort.TOPO);
+		walk.sort(RevSort.REVERSE, true);
+		try {
+			RevCommit tip = walk.parseCommit(tipId);
+			RevCommit base = walk.parseCommit(baseId);
+			walk.markStart(tip);
+			walk.markUninteresting(base);
+			for (;;) {
+				RevCommit c = walk.next();
+				if (c == null) {
+					break;
+				}
+				count++;
+			}
+		} catch (IOException e) {
+			// Should never happen, the core receive process would have
+			// identified the missing object earlier before we got control.
+			LOGGER.error("failed to get commit count", e);
+			return 0;
+		} finally {
+			walk.close();
+		}
+		return count;
+	}
+
+	public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) {
+		int count = 0;
+		try {
+			count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId));
+		} catch (IOException e) {
+			LOGGER.error("failed to get commit count", e);
+		}
+		return count;
+	}
 }
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index cee7eab..a215b4d 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -778,4 +778,6 @@
 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.
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.html b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
index 974dcc0..46c0f7e 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
@@ -461,7 +461,7 @@
 				<td style="text-align:right;">
 					<span wicket:id="patchsetType">[revision type]</span>					
 				</td>
-				<td><span class="hidden-phone hidden-tablet aui-lozenge aui-lozenge-subtle" wicket:id="patchsetRevision">[R1]</span>
+				<td><span class="hidden-phone hidden-tablet" wicket:id="patchsetRevision">[R1]</span>
 					<span class="fa fa-fw" style="padding-left:15px;"><a wicket:id="deleteRevision" class="fa fa-fw fa-trash delete-patchset"></a></span>
 					<span class="hidden-tablet hidden-phone" style="padding-left:15px;"><span wicket:id="patchsetDiffStat"></span></span>
 				</td>			
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
index b2e63a6..cd049f4 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -36,7 +36,6 @@
 import org.apache.wicket.Component;
 import org.apache.wicket.MarkupContainer;
 import org.apache.wicket.PageParameters;
-import org.apache.wicket.RequestCycle;
 import org.apache.wicket.RestartResponseException;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.behavior.SimpleAttributeModifier;
@@ -45,7 +44,6 @@
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.link.ExternalLink;
 import org.apache.wicket.markup.html.link.Link;
-import org.apache.wicket.markup.html.link.StatelessLink;
 import org.apache.wicket.markup.html.pages.RedirectPage;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.repeater.Item;
@@ -54,7 +52,6 @@
 import org.apache.wicket.model.Model;
 import org.apache.wicket.protocol.http.RequestUtils;
 import org.apache.wicket.protocol.http.WebRequest;
-import org.apache.wicket.request.target.basic.RedirectRequestTarget;
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -863,9 +860,6 @@
 				if (event.hasPatchset()) {
 					// patchset
 					Patchset patchset = event.patchset;
-					//In the case of using a cached change list
-					item.setVisible(!patchset.isDeleted());
-					
 					String what;
 					if (event.isStatusChange() && (Status.New == event.getStatus())) {
 						what = getString("gb.proposedThisChange");
@@ -883,6 +877,7 @@
 					LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,
 							ComparePage.class, WicketUtils.newRangeParameter(repositoryName, patchset.parent == null ? patchset.base : patchset.parent, patchset.tip), true);
 					WicketUtils.setHtmlTooltip(psr, patchset.toString());
+					WicketUtils.setCssClass(psr, "aui-lozenge aui-lozenge-subtle");
 					item.add(psr);
 					String typeCss = getPatchsetTypeCss(patchset.type);
 					Label typeLabel = new Label("patchsetType", patchset.type.toString());
@@ -910,6 +905,42 @@
 					item.add(new Label("patchsetType").setVisible(false));
 					item.add(new Label("deleteRevision").setVisible(false));
 					item.add(new Label("patchsetDiffStat").setVisible(false));
+				} else if (event.hasReference()) {
+					// reference
+					switch (event.reference.getSourceType()) {
+						case Commit: {
+							final int shaLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
+							
+							item.add(new Label("what", getString("gb.referencedByCommit")));
+							LinkPanel psr = new LinkPanel("patchsetRevision", null, event.reference.toString().substring(0, shaLen),
+									CommitPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.toString()), true);
+							WicketUtils.setHtmlTooltip(psr, event.reference.toString());
+							WicketUtils.setCssClass(psr, "ticketReference-commit shortsha1");
+							item.add(psr);
+							
+						} break;
+						
+						case Ticket: {
+							final String text = MessageFormat.format("ticket/{0}", event.reference.ticketId);
+
+							item.add(new Label("what", getString("gb.referencedByTicket")));
+							//NOTE: Ideally reference the exact comment using reference.toString,
+							//		however anchor hash is used and is escaped resulting in broken link
+							LinkPanel psr = new LinkPanel("patchsetRevision", null,  text,
+									TicketsPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.ticketId.toString()), true);
+							WicketUtils.setCssClass(psr, "ticketReference-comment");
+							item.add(psr);
+						} break;
+					
+						default: {
+							item.add(new Label("what").setVisible(false));
+							item.add(new Label("patchsetRevision").setVisible(false));
+						}
+					}
+					
+					item.add(new Label("patchsetType").setVisible(false));
+					item.add(new Label("deleteRevision").setVisible(false));
+					item.add(new Label("patchsetDiffStat").setVisible(false));
 				} else if (event.hasReview()) {
 					// review
 					String score;
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 3318441..10c9a0e 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -2391,4 +2391,19 @@
 .delete-patchset {
 	color:#D51900;
 	font-size: 1.2em;
+}
+
+.ticketReference-comment {
+    font-family: sans-serif;
+    font-weight: 200;
+    font-size: 1em;
+    font-variant: normal;
+    text-transform: none;
+}
+
+.ticketReference-commit {
+    font-family: monospace;
+    font-weight: 200;
+    font-size: 1em;
+    font-variant: normal;
 }
\ No newline at end of file
diff --git a/src/test/config/test-gitblit.properties b/src/test/config/test-gitblit.properties
index 78e9ab9..ef6a6c5 100644
--- a/src/test/config/test-gitblit.properties
+++ b/src/test/config/test-gitblit.properties
@@ -90,3 +90,5 @@
 server.httpsBindInterface = localhost
 server.storePassword = gitblit
 server.shutdownPort = 8081
+
+tickets.service = com.gitblit.tickets.BranchTicketService
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index b01c82c..133be77 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -66,7 +66,7 @@
 		ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class,
 		BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class,
 		SshKeysDispatcherTest.class, UITicketTest.class, PathUtilsTest.class, SshKerberosAuthenticationTest.class,
-		GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class })
+		GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class, TicketReferenceTest.class })
 public class GitBlitSuite {
 
 	public static final File BASEFOLDER = new File("data");
diff --git a/src/test/java/com/gitblit/tests/TicketReferenceTest.java b/src/test/java/com/gitblit/tests/TicketReferenceTest.java
new file mode 100644
index 0000000..934659c
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/TicketReferenceTest.java
@@ -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

--
Gitblit v1.9.1