From a502d96a860456ec5e8c96761db70f7cabb74751 Mon Sep 17 00:00:00 2001
From: Paul Martin <paul@paulsputer.com>
Date: Sat, 30 Apr 2016 04:19:14 -0400
Subject: [PATCH] Merge pull request #1073 from gitblit/1062-DocEditorUpdates

---
 src/main/java/com/gitblit/git/PatchsetReceivePack.java |  231 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 205 insertions(+), 26 deletions(-)

diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index d3d0b1d..ef0b409 100644
--- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java
+++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -51,6 +51,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;
@@ -72,6 +73,7 @@
 import com.gitblit.utils.JGitUtils.MergeStatus;
 import com.gitblit.utils.RefLogUtils;
 import com.gitblit.utils.StringUtils;
+import com.google.common.collect.Lists;
 
 
 /**
@@ -149,6 +151,11 @@
 			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;
 		try {
 			ticketId = Long.parseLong(branch);
@@ -163,11 +170,11 @@
 
 	/** Extracts the ticket id from the ref name */
 	private long getTicketId(String refName) {
+		if (refName.indexOf('%') > -1) {
+			refName = refName.substring(0, refName.indexOf('%'));
+		}
 		if (refName.startsWith(Constants.R_FOR)) {
 			String ref = refName.substring(Constants.R_FOR.length());
-			if (ref.indexOf('%') > -1) {
-				ref = ref.substring(0, ref.indexOf('%'));
-			}
 			try {
 				return Long.parseLong(ref);
 			} catch (Exception e) {
@@ -308,6 +315,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;
@@ -345,10 +353,27 @@
 					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;
+				}
+
 				if (patchsetRefCmd != null) {
 					sendRejection(cmd, "You may only push one patchset at a time.");
 					continue;
 				}
+
+				LOGGER.info(MessageFormat.format("Verifying {0} push ref \"{1}\" received from {2}",
+						repository.name, cmd.getRefName(), user.username));
 
 				// responsible verification
 				String responsible = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.RESPONSIBLE);
@@ -380,13 +405,18 @@
 				// watcher verification
 				List<String> watchers = PatchsetCommand.getOptions(cmd, PatchsetCommand.WATCH);
 				if (!ArrayUtils.isEmpty(watchers)) {
+					boolean verified = true;
 					for (String watcher : watchers) {
 						UserModel user = gitblit.getUserModel(watcher);
 						if (user == null) {
 							// watcher does not exist
 							sendRejection(cmd, "Sorry, \"{0}\" is not a valid username for the watch list!", watcher);
-							continue;
+							verified = false;
+							break;
 						}
+					}
+					if (!verified) {
+						continue;
 					}
 				}
 
@@ -429,7 +459,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);
@@ -484,8 +517,6 @@
 	 * @return the patchset command
 	 */
 	private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
-		LOGGER.info(MessageFormat.format("Preparing {0} patchset command for \"{1}\" received from {2}",
-				repository.name, cmd.getRefName(), user.username));
 		String branch = getIntegrationBranch(cmd.getRefName());
 		long number = getTicketId(cmd.getRefName());
 
@@ -511,8 +542,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)) {
@@ -594,14 +627,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;
 			}
 
@@ -707,6 +828,15 @@
 				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 {
 				sendError("FAILED to create ticket");
@@ -733,6 +863,20 @@
 				// log the new patchset ref
 				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;
@@ -769,6 +913,9 @@
 				}
 
 				TicketModel ticket = ticketService.getTicket(repository, ticketNumber);
+				if (ticket == null) {
+					continue;
+				}
 				String integrationBranch;
 				if (StringUtils.isEmpty(ticket.mergeTo)) {
 					// unspecified integration branch
@@ -826,8 +973,8 @@
 					psCmd.updateTicket(c, mergeTo, ticket, null);
 
 					// create a ticket patchset ref
-					updateRef(psCmd.getPatchsetBranch(), c.getId());
-					RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId());
+					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
@@ -888,11 +1035,20 @@
 
 		if (parseMessage) {
 			// parse commit message looking for fixes/closes #n
-			Pattern p = Pattern.compile("(?:fixes|closes)[\\s-]+#?(\\d+)", Pattern.CASE_INSENSITIVE);
-			Matcher m = p.matcher(commit.getFullMessage());
-			while (m.find()) {
-				String val = m.group();
-				return Long.parseLong(val);
+			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;
@@ -922,7 +1078,7 @@
 			LOGGER.error("failed to get commit count", e);
 			return 0;
 		} finally {
-			walk.release();
+			walk.close();
 		}
 		return count;
 	}
@@ -1034,7 +1190,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);
@@ -1045,7 +1201,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());
@@ -1130,11 +1296,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