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 |  516 +++++++++++++++++++++++++++++++++++++-------------------
 1 files changed, 341 insertions(+), 175 deletions(-)

diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index 64a739e..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;
@@ -51,6 +50,7 @@
 
 import com.gitblit.Constants;
 import com.gitblit.Keys;
+import com.gitblit.extensions.PatchsetHook;
 import com.gitblit.manager.IGitblit;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.TicketModel;
@@ -59,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;
@@ -72,6 +74,7 @@
 import com.gitblit.utils.JGitUtils.MergeStatus;
 import com.gitblit.utils.RefLogUtils;
 import com.gitblit.utils.StringUtils;
+import com.google.common.collect.Lists;
 
 
 /**
@@ -147,6 +150,11 @@
 			defaultBranch = getRepository().getBranch();
 		} catch (Exception e) {
 			LOGGER.error("failed to determine default branch for " + repository.name, e);
+		}
+
+		if (!StringUtils.isEmpty(getRepositoryModel().mergeTo)) {
+			// repository settings specifies a default integration branch
+			defaultBranch = Repository.shortenRefName(getRepositoryModel().mergeTo);
 		}
 
 		long ticketId = 0L;
@@ -308,6 +316,7 @@
 			}
 
 			if (isPatchsetRef(cmd.getRefName()) && processPatchsets) {
+
 				if (ticketService == null) {
 					sendRejection(cmd, "Sorry, the ticket service is unavailable and can not accept patchsets at this time.");
 					continue;
@@ -342,6 +351,20 @@
 					LOGGER.error("{} already has refs in the {} namespace",
 							repository.name, Constants.R_FOR);
 					sendRejection(cmd, "Sorry, a repository administrator will have to remove the {} namespace", Constants.R_FOR);
+					continue;
+				}
+
+				if (cmd.getNewId().equals(ObjectId.zeroId())) {
+					// ref deletion request
+					if (cmd.getRefName().startsWith(Constants.R_TICKET)) {
+						if (user.canDeleteRef(repository)) {
+							batch.addCommand(cmd);
+						} else {
+							sendRejection(cmd, "Sorry, you do not have permission to delete {}", cmd.getRefName());
+						}
+					} else {
+						sendRejection(cmd, "Sorry, you can not delete {}", cmd.getRefName());
+					}
 					continue;
 				}
 
@@ -437,7 +460,10 @@
 				patchsetRefCmd.setResult(Result.OK);
 
 				// update the ticket branch ref
-				RefUpdate ru = updateRef(patchsetCmd.getTicketBranch(), patchsetCmd.getNewId());
+				RefUpdate ru = updateRef(
+						patchsetCmd.getTicketBranch(),
+						patchsetCmd.getNewId(),
+						patchsetCmd.getPatchsetType());
 				updateReflog(ru);
 
 				TicketModel ticket = processPatchset(patchsetCmd);
@@ -460,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);
@@ -517,8 +561,10 @@
 						break;
 					}
 				}
-				sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
+				if (mergeChange != null) {
+					sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
 						mergeChange.author, mergeChange.patchset, number, ticket.mergeTo);
+				}
 				sendRejection(cmd, "Ticket {0,number,0} already resolved", number);
 				return null;
 			} else if (!StringUtils.isEmpty(ticket.mergeTo)) {
@@ -577,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) {
 			/*
@@ -600,14 +648,102 @@
 
 			if (patchset.commits > 1) {
 				sendError("");
-				sendError("To create a proposal ticket, please squash your commits and");
-				sendError("provide a meaningful commit message with a short title &");
-				sendError("an optional description/body.");
+				sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!",
+						forBranch, patchset.commits);
 				sendError("");
-				sendError(minTitle);
-				sendError(maxTitle);
+				// display an ellipsized log of the commits being pushed
+				RevWalk walk = getRevWalk();
+				walk.reset();
+				walk.sort(RevSort.TOPO);
+				int boundary = 3;
+				int count = 0;
+				try {
+					walk.markStart(tipCommit);
+					walk.markUninteresting(mergeBase);
+
+					for (;;) {
+
+						RevCommit c = walk.next();
+						if (c == null) {
+							break;
+						}
+
+						if (count < boundary || count >= (patchset.commits - boundary)) {
+
+							walk.parseBody(c);
+							sendError("   {0}  {1}", c.getName().substring(0, shortCommitIdLen),
+								StringUtils.trimString(c.getShortMessage(), 60));
+
+						} else if (count == boundary) {
+
+							sendError("   ... more commits ...");
+
+						}
+
+						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);
+				} finally {
+					walk.close();
+				}
+
 				sendError("");
-				sendRejection(cmd, "please squash to one commit");
+				sendError("Possible Solutions:");
+				sendError("");
+				int solution = 1;
+				String forSpec = cmd.getRefName().substring(Constants.R_FOR.length());
+				if (forSpec.equals("default") || forSpec.equals("new")) {
+					try {
+						// determine other possible integration targets
+						List<String> bases = Lists.newArrayList();
+						for (Ref ref : getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+							if (!ref.getName().startsWith(Constants.R_TICKET)
+									&& !ref.getName().equals(forBranchRef.getName())) {
+								if (JGitUtils.isMergedInto(getRepository(), ref.getObjectId(), tipCommit)) {
+									bases.add(Repository.shortenRefName(ref.getName()));
+								}
+							}
+						}
+
+						if (!bases.isEmpty()) {
+
+							if (bases.size() == 1) {
+								// suggest possible integration targets
+								String base = bases.get(0);
+								sendError("{0}. Propose this change for the ''{1}'' branch.", solution++, base);
+								sendError("");
+								sendError("   git push origin HEAD:refs/for/{0}", base);
+								sendError("   pt propose {0}", base);
+								sendError("");
+							} else {
+								// suggest possible integration targets
+								sendError("{0}. Propose this change for a different branch.", solution++);
+								sendError("");
+								for (String base : bases) {
+									sendError("   git push origin HEAD:refs/for/{0}", base);
+									sendError("   pt propose {0}", base);
+									sendError("");
+								}
+							}
+
+						}
+					} catch (IOException e) {
+						LOGGER.error(null, e);
+					}
+				}
+				sendError("{0}. Squash your changes into a single commit with a meaningful message.", solution++);
+				sendError("");
+				sendError("{0}. Open a ticket for your changes and then push your {1} commits to the ticket.",
+						solution++, patchset.commits);
+				sendError("");
+				sendError("   git push origin HEAD:refs/for/{id}");
+				sendError("   pt propose {id}");
+				sendError("");
+				sendRejection(cmd, "too many commits");
 				return null;
 			}
 
@@ -687,6 +823,10 @@
 			}
 			break;
 		}
+
+		Change change = psCmd.getChange();
+		change.pendingLinks = ticketLinks;
+
 		return psCmd;
 	}
 
@@ -712,6 +852,15 @@
 				// log the new patch ref
 				RefLogUtils.updateRefLog(user, getRepository(),
 						Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
+
+				// call any patchset hooks
+				for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
+					try {
+						hook.onNewPatchset(ticket);
+					} catch (Exception e) {
+						LOGGER.error("Failed to execute extension", e);
+					}
+				}
 
 				return ticket;
 			} else {
@@ -740,6 +889,20 @@
 				RefLogUtils.updateRefLog(user, getRepository(),
 					Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
 
+				// call any patchset hooks
+				final boolean isNewPatchset = change.patchset.rev == 1;
+				for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
+					try {
+						if (isNewPatchset) {
+							hook.onNewPatchset(ticket);
+						} else {
+							hook.onUpdatePatchset(ticket);
+						}
+					} catch (Exception e) {
+						LOGGER.error("Failed to execute extension", e);
+					}
+				}
+
 				// return the updated ticket
 				return ticket;
 			} else {
@@ -752,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 {
@@ -769,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());
-					RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId());
-					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();
 		}
@@ -875,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.release();
-		}
-		return count;
-	}
+	
 
 	/**
 	 * Creates a new patchset with metadata.
@@ -953,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;
@@ -1052,7 +1195,7 @@
 		return newPatchset;
 	}
 
-	private RefUpdate updateRef(String ref, ObjectId newId) {
+	private RefUpdate updateRef(String ref, ObjectId newId, PatchsetType type) {
 		ObjectId ticketRefId = ObjectId.zeroId();
 		try {
 			ticketRefId = getRepository().resolve(ref);
@@ -1063,7 +1206,17 @@
 		try {
 			RefUpdate ru = getRepository().updateRef(ref,  false);
 			ru.setRefLogIdent(getRefLogIdent());
-			ru.setForceUpdate(true);
+			switch (type) {
+			case Amend:
+			case Rebase:
+			case Rebase_Squash:
+			case Squash:
+				ru.setForceUpdate(true);
+				break;
+			default:
+				break;
+			}
+
 			ru.setExpectedOldObjectId(ticketRefId);
 			ru.setNewObjectId(newId);
 			RefUpdate.Result result = ru.update(getRevWalk());
@@ -1148,11 +1301,24 @@
 		if (ticket != null) {
 			ticketNotifier.queueMailing(ticket);
 
-			// update the reflog with the merge
 			if (oldRef != null) {
 				ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
 						ObjectId.fromString(mergeResult.sha), oldRef.getName());
-				RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
+				cmd.setResult(Result.OK);
+				List<ReceiveCommand> commands = Arrays.asList(cmd);
+
+				logRefChange(commands);
+				updateIncrementalPushTags(commands);
+				updateGitblitRefLog(commands);
+			}
+
+			// call patchset hooks
+			for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
+				try {
+					hook.onMergePatchset(ticket);
+				} catch (Exception e) {
+					LOGGER.error("Failed to execute extension", e);
+				}
 			}
 			return mergeResult.status;
 		} else {

--
Gitblit v1.9.1