From cd7e4f9186f2ace4416780a7dd6341e01e23a45f Mon Sep 17 00:00:00 2001
From: Paul Martin <paul@paulsputer.com>
Date: Wed, 06 Apr 2016 14:46:58 -0400
Subject: [PATCH] Fix for #962 - Delete patchset ability

---
 src/main/java/com/gitblit/tickets/ITicketService.java |  185 +++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 164 insertions(+), 21 deletions(-)

diff --git a/src/main/java/com/gitblit/tickets/ITicketService.java b/src/main/java/com/gitblit/tickets/ITicketService.java
index 8d922b5..e831003 100644
--- a/src/main/java/com/gitblit/tickets/ITicketService.java
+++ b/src/main/java/com/gitblit/tickets/ITicketService.java
@@ -36,6 +36,7 @@
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.extensions.TicketHook;
+import com.gitblit.manager.IManager;
 import com.gitblit.manager.INotificationManager;
 import com.gitblit.manager.IPluginManager;
 import com.gitblit.manager.IRepositoryManager;
@@ -47,8 +48,10 @@
 import com.gitblit.models.TicketModel.Change;
 import com.gitblit.models.TicketModel.Field;
 import com.gitblit.models.TicketModel.Patchset;
+import com.gitblit.models.TicketModel.PatchsetType;
 import com.gitblit.models.TicketModel.Status;
 import com.gitblit.tickets.TicketIndexer.Lucene;
+import com.gitblit.utils.DeepCopier;
 import com.gitblit.utils.DiffUtils;
 import com.gitblit.utils.DiffUtils.DiffStat;
 import com.gitblit.utils.StringUtils;
@@ -62,7 +65,9 @@
  * @author James Moger
  *
  */
-public abstract class ITicketService {
+public abstract class ITicketService implements IManager {
+
+	public static final String SETTING_UPDATE_DIFFSTATS = "migration.updateDiffstats";
 
 	private static final String LABEL = "label";
 
@@ -105,6 +110,8 @@
 	private final Map<String, List<TicketLabel>> labelsCache;
 
 	private final Map<String, List<TicketMilestone>> milestonesCache;
+
+	private final boolean updateDiffstats;
 
 	private static class TicketKey {
 		final String repository;
@@ -163,18 +170,22 @@
 
 		this.labelsCache = new ConcurrentHashMap<String, List<TicketLabel>>();
 		this.milestonesCache = new ConcurrentHashMap<String, List<TicketMilestone>>();
+
+		this.updateDiffstats = settings.getBoolean(SETTING_UPDATE_DIFFSTATS, true);
 	}
 
 	/**
 	 * Start the service.
 	 * @since 1.4.0
 	 */
+	@Override
 	public abstract ITicketService start();
 
 	/**
 	 * Stop the service.
 	 * @since 1.4.0
 	 */
+	@Override
 	public final ITicketService stop() {
 		indexer.close();
 		ticketsCache.invalidateAll();
@@ -244,6 +255,7 @@
 	 */
 	public boolean isAcceptingTicketUpdates(RepositoryModel repository) {
 		return isReady()
+				&& repository.hasCommits
 				&& repository.isBare
 				&& !repository.isFrozen
 				&& !repository.isMirror;
@@ -379,7 +391,9 @@
 		} catch (IOException e) {
 			log.error("failed to create label " + label + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return lb;
 	}
@@ -405,7 +419,9 @@
 		} catch (IOException e) {
 			log.error("failed to update label " + label + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
@@ -444,7 +460,9 @@
 		} catch (IOException e) {
 			log.error("failed to rename label " + oldName + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
@@ -473,7 +491,9 @@
 		} catch (IOException e) {
 			log.error("failed to delete label " + label + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
@@ -548,9 +568,10 @@
 	public TicketMilestone getMilestone(RepositoryModel repository, String milestone) {
 		for (TicketMilestone ms : getMilestones(repository)) {
 			if (ms.name.equalsIgnoreCase(milestone)) {
+				TicketMilestone tm = DeepCopier.copy(ms);
 				String q = QueryBuilder.q(Lucene.rid.matches(repository.getRID())).and(Lucene.milestone.matches(milestone)).build();
-				ms.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true);
-				return ms;
+				tm.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true);
+				return tm;
 			}
 		}
 		return null;
@@ -579,7 +600,9 @@
 		} catch (IOException e) {
 			log.error("failed to create milestone " + milestone + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return ms;
 	}
@@ -611,7 +634,9 @@
 		} catch (IOException e) {
 			log.error("failed to update milestone " + milestone + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
@@ -627,39 +652,64 @@
 	 * @since 1.4.0
 	 */
 	public synchronized boolean renameMilestone(RepositoryModel repository, String oldName, String newName, String createdBy) {
+		return renameMilestone(repository, oldName, newName, createdBy, true);
+	}
+
+	/**
+	 * Renames a milestone.
+	 *
+	 * @param repository
+	 * @param oldName
+	 * @param newName
+	 * @param createdBy
+	 * @param notifyOpenTickets
+	 * @return true if successful
+	 * @since 1.6.0
+	 */
+	public synchronized boolean renameMilestone(RepositoryModel repository, String oldName,
+			String newName, String createdBy, boolean notifyOpenTickets) {
 		if (StringUtils.isEmpty(newName)) {
 			throw new IllegalArgumentException("new milestone can not be empty!");
 		}
 		Repository db = null;
 		try {
 			db = repositoryManager.getRepository(repository.name);
-			TicketMilestone milestone = getMilestone(repository, oldName);
+			TicketMilestone tm = getMilestone(repository, oldName);
+			if (tm == null) {
+				return false;
+			}
 			StoredConfig config = db.getConfig();
 			config.unsetSection(MILESTONE, oldName);
-			config.setString(MILESTONE, newName, STATUS, milestone.status.name());
-			config.setString(MILESTONE, newName, COLOR, milestone.color);
-			if (milestone.due != null) {
-				config.setString(MILESTONE, milestone.name, DUE,
-						new SimpleDateFormat(DUE_DATE_PATTERN).format(milestone.due));
+			config.setString(MILESTONE, newName, STATUS, tm.status.name());
+			config.setString(MILESTONE, newName, COLOR, tm.color);
+			if (tm.due != null) {
+				config.setString(MILESTONE, newName, DUE,
+						new SimpleDateFormat(DUE_DATE_PATTERN).format(tm.due));
 			}
 			config.save();
 
 			milestonesCache.remove(repository.name);
 
 			TicketNotifier notifier = createNotifier();
-			for (QueryResult qr : milestone.tickets) {
+			for (QueryResult qr : tm.tickets) {
 				Change change = new Change(createdBy);
 				change.setField(Field.milestone, newName);
 				TicketModel ticket = updateTicket(repository, qr.number, change);
-				notifier.queueMailing(ticket);
+				if (notifyOpenTickets && ticket.isOpen()) {
+					notifier.queueMailing(ticket);
+				}
 			}
-			notifier.sendAll();
+			if (notifyOpenTickets) {
+				notifier.sendAll();
+			}
 
 			return true;
 		} catch (IOException e) {
 			log.error("failed to rename milestone " + oldName + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
@@ -674,11 +724,30 @@
 	 * @since 1.4.0
 	 */
 	public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone, String createdBy) {
+		return deleteMilestone(repository, milestone, createdBy, true);
+	}
+
+	/**
+	 * Deletes a milestone.
+	 *
+	 * @param repository
+	 * @param milestone
+	 * @param createdBy
+	 * @param notifyOpenTickets
+	 * @return true if successful
+	 * @since 1.6.0
+	 */
+	public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone,
+			String createdBy, boolean notifyOpenTickets) {
 		if (StringUtils.isEmpty(milestone)) {
 			throw new IllegalArgumentException("milestone can not be empty!");
 		}
 		Repository db = null;
 		try {
+			TicketMilestone tm = getMilestone(repository, milestone);
+			if (tm == null) {
+				return false;
+			}
 			db = repositoryManager.getRepository(repository.name);
 			StoredConfig config = db.getConfig();
 			config.unsetSection(MILESTONE, milestone);
@@ -686,14 +755,37 @@
 
 			milestonesCache.remove(repository.name);
 
+			TicketNotifier notifier = createNotifier();
+			for (QueryResult qr : tm.tickets) {
+				Change change = new Change(createdBy);
+				change.setField(Field.milestone, "");
+				TicketModel ticket = updateTicket(repository, qr.number, change);
+				if (notifyOpenTickets && ticket.isOpen()) {
+					notifier.queueMailing(ticket);
+				}
+			}
+			if (notifyOpenTickets) {
+				notifier.sendAll();
+			}
 			return true;
 		} catch (IOException e) {
 			log.error("failed to delete milestone " + milestone + " in " + repository, e);
 		} finally {
-			db.close();
+			if (db != null) {
+				db.close();
+			}
 		}
 		return false;
 	}
+
+	/**
+	 * Returns the set of assigned ticket ids in the repository.
+	 *
+	 * @param repository
+	 * @return a set of assigned ticket ids in the repository
+	 * @since 1.6.0
+	 */
+	public abstract Set<Long> getIds(RepositoryModel repository);
 
 	/**
 	 * Assigns a new ticket id.
@@ -757,7 +849,7 @@
 			ticket = getTicketImpl(repository, ticketId);
 			// if ticket exists
 			if (ticket != null) {
-				if (ticket.hasPatchsets()) {
+				if (ticket.hasPatchsets() && updateDiffstats) {
 					Repository r = repositoryManager.getRepository(repository.name);
 					try {
 						Patchset patchset = ticket.getCurrentPatchset();
@@ -789,6 +881,33 @@
 	 * @since 1.4.0
 	 */
 	protected abstract TicketModel getTicketImpl(RepositoryModel repository, long ticketId);
+
+
+	/**
+	 * Returns the journal used to build a ticket.
+	 *
+	 * @param repository
+	 * @param ticketId
+	 * @return the journal for the ticket, if it exists, otherwise null
+	 * @since 1.6.0
+	 */
+	public final List<Change> getJournal(RepositoryModel repository, long ticketId) {
+		if (hasTicket(repository, ticketId)) {
+			List<Change> journal = getJournalImpl(repository, ticketId);
+			return journal;
+		}
+		return null;
+	}
+
+	/**
+	 * Retrieves the ticket journal.
+	 *
+	 * @param repository
+	 * @param ticketId
+	 * @return a ticket, if it exists, otherwise null
+	 * @since 1.6.0
+	 */
+	protected abstract List<Change> getJournalImpl(RepositoryModel repository, long ticketId);
 
 	/**
 	 * Get the ticket url
@@ -1095,6 +1214,30 @@
 		TicketModel revisedTicket = updateTicket(repository, ticket.number, deletion);
 		return revisedTicket;
 	}
+	
+	/**
+	 * Deletes a patchset from a ticket.
+	 *
+	 * @param ticket
+	 * @param patchset
+	 *            the patchset to delete (should be the highest revision)
+	 * @param userName
+	 * 			the user deleting the commit
+	 * @return the revised ticket if the deletion was successful
+	 * @since 1.8.0
+	 */
+	public final TicketModel deletePatchset(TicketModel ticket, Patchset patchset, String userName) {
+		Change deletion = new Change(userName);
+		deletion.patchset = new Patchset();
+		deletion.patchset.number = patchset.number;
+		deletion.patchset.rev = patchset.rev;
+		deletion.patchset.type = PatchsetType.Delete;
+		
+		RepositoryModel repository = repositoryManager.getRepositoryModel(ticket.repository);
+		TicketModel revisedTicket = updateTicket(repository, ticket.number, deletion);
+		
+		return revisedTicket;
+	} 
 
 	/**
 	 * Commit a ticket change to the repository.

--
Gitblit v1.9.1