From aae58435191c1b4e73ef7c5447e7a0832c7f0e53 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 06 Mar 2014 13:49:02 -0500
Subject: [PATCH] Merged #22 "Tie mirroring, pushing, and the BranchTicketService together"

---
 src/main/java/com/gitblit/utils/RpcUtils.java              |   31 ++++
 src/main/java/com/gitblit/git/PatchsetReceivePack.java     |   33 +++-
 src/main/java/com/gitblit/servlet/RpcServlet.java          |   15 ++
 src/main/java/com/gitblit/Constants.java                   |    2 
 src/main/java/com/gitblit/git/GitblitReceivePack.java      |    9 +
 src/site/tickets_replication.mkd                           |  135 +++++++++++++++++++
 src/main/java/com/gitblit/service/MirrorService.java       |   32 ++++
 src/main/java/com/gitblit/git/ReceiveCommandEvent.java     |   38 +++++
 src/site/rpc.mkd                                           |    2 
 src/main/distrib/win/reindex-tickets.cmd                   |   17 +
 src/main/java/com/gitblit/tickets/ITicketService.java      |    4 
 build.xml                                                  |    4 
 src/main/java/com/gitblit/tickets/BranchTicketService.java |   71 ++++++++++
 src/main/distrib/linux/reindex-tickets.sh                  |    9 +
 14 files changed, 387 insertions(+), 15 deletions(-)

diff --git a/build.xml b/build.xml
index 71e7430..beefc54 100644
--- a/build.xml
+++ b/build.xml
@@ -570,7 +570,8 @@
 					  <page name="overview" src="tickets_overview.mkd" />
 					  <page name="using" src="tickets_using.mkd" />
 					  <page name="barnum" src="tickets_barnum.mkd" />
-					  <page name="setup" src="tickets_setup.mkd" />
+					  <page name="setup" src="tickets_setup.mkd" />
+					  <page name="replication &amp; advanced administration" src="tickets_replication.mkd" />
 					</menu>
 					<divider />
 					<page name="federation" src="federation.mkd" />
@@ -909,6 +910,7 @@
 							<page name="using" src="tickets_using.mkd" />
 							<page name="barnum" src="tickets_barnum.mkd" />
 							<page name="setup" src="tickets_setup.mkd" />
+							<page name="replication &amp; advanced administration" src="tickets_replication.mkd" />
 						</menu>
 						<divider />
 						<page name="federation" src="federation.mkd" />
diff --git a/src/main/distrib/linux/reindex-tickets.sh b/src/main/distrib/linux/reindex-tickets.sh
index 1593929..5a4fc34 100644
--- a/src/main/distrib/linux/reindex-tickets.sh
+++ b/src/main/distrib/linux/reindex-tickets.sh
@@ -11,5 +11,14 @@
 #
 # --------------------------------------------------------------------------
 
+if [ -z $1 ]; then
+    echo "Please specify your baseFolder!";
+    echo "";
+    echo "usage:";
+    echo "    reindex-tickets <baseFolder>";
+    echo "";
+    exit 1;
+fi
+
 java -cp gitblit.jar:./ext/* com.gitblit.ReindexTickets --baseFolder $1
 
diff --git a/src/main/distrib/win/reindex-tickets.cmd b/src/main/distrib/win/reindex-tickets.cmd
index e28f45f..c9116ca 100644
--- a/src/main/distrib/win/reindex-tickets.cmd
+++ b/src/main/distrib/win/reindex-tickets.cmd
@@ -4,10 +4,19 @@
 @REM Since the Tickets feature is undergoing massive churn it may be necessary 
 @REM to reindex tickets due to model or index changes.
 @REM
-@REM Always use forward-slashes for the path separator in your parameters!!
+@REM usage:
+@REM     reindex-tickets <baseFolder>
 @REM
-@REM Set FOLDER to the baseFolder.
 @REM --------------------------------------------------------------------------
-@SET FOLDER=data
+@if [%1]==[] goto nobasefolder
 
-@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.ReindexTickets --baseFolder %FOLDER%
+@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.ReindexTickets --baseFolder %1
+@goto end
+
+:nobasefolder
+@echo "Please specify your baseFolder!"
+@echo
+@echo "    reindex-tickets c:/gitblit-data"
+@echo
+
+:end
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 5b71eeb..e93f7b1 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -350,7 +350,7 @@
 	public static enum RpcRequest {
 		// Order is important here.  anything above LIST_SETTINGS requires
 		// administrator privileges and web.allowRpcManagement.
-		CLEAR_REPOSITORY_CACHE, GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, GET_USER, LIST_SETTINGS,
+		CLEAR_REPOSITORY_CACHE, REINDEX_TICKETS, GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, GET_USER, LIST_SETTINGS,
 		CREATE_REPOSITORY, EDIT_REPOSITORY, DELETE_REPOSITORY,
 		LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER,
 		LIST_TEAMS, CREATE_TEAM, EDIT_TEAM, DELETE_TEAM,
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePack.java b/src/main/java/com/gitblit/git/GitblitReceivePack.java
index 3a0eff2..e3e2fae 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePack.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePack.java
@@ -344,6 +344,15 @@
 			LOGGER.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
 		}
 
+		// check for updates pushed to the BranchTicketService branch
+		// if the BranchTicketService is active it will reindex, as appropriate
+		for (ReceiveCommand cmd : commands) {
+			if (Result.OK.equals(cmd.getResult())
+					&& BranchTicketService.BRANCH.equals(cmd.getRefName())) {
+				rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
+			}
+		}
+
 		// run Groovy hook scripts
 		Set<String> scripts = new LinkedHashSet<String>();
 		scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index ae429d2..c0ab8ae 100644
--- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java
+++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -60,6 +60,7 @@
 import com.gitblit.models.TicketModel.PatchsetType;
 import com.gitblit.models.TicketModel.Status;
 import com.gitblit.models.UserModel;
+import com.gitblit.tickets.BranchTicketService;
 import com.gitblit.tickets.ITicketService;
 import com.gitblit.tickets.TicketMilestone;
 import com.gitblit.tickets.TicketNotifier;
@@ -105,7 +106,7 @@
 
 	protected final TicketNotifier ticketNotifier;
 
-	private boolean requireCleanMerge;
+	private boolean requireMergeablePatchset;
 
 	public PatchsetReceivePack(IGitblit gitblit, Repository db, RepositoryModel repository, UserModel user) {
 		super(gitblit, db, repository, user);
@@ -257,12 +258,26 @@
 	/** Execute commands to update references. */
 	@Override
 	protected void executeCommands() {
+		// we process patchsets unless the user is pushing something special
+		boolean processPatchsets = true;
+		for (ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
+			if (ticketService instanceof BranchTicketService
+					&& BranchTicketService.BRANCH.equals(cmd.getRefName())) {
+				// the user is pushing an update to the BranchTicketService data
+				processPatchsets = false;
+			}
+		}
+
 		// workaround for JGit's awful scoping choices
 		//
 		// reset the patchset refs to NOT_ATTEMPTED (see validateCommands)
 		for (ReceiveCommand cmd : filterCommands(Result.OK)) {
 			if (isPatchsetRef(cmd.getRefName())) {
 				cmd.setResult(Result.NOT_ATTEMPTED);
+			} else if (ticketService instanceof BranchTicketService
+					&& BranchTicketService.BRANCH.equals(cmd.getRefName())) {
+				// the user is pushing an update to the BranchTicketService data
+				processPatchsets = false;
 			}
 		}
 
@@ -292,7 +307,7 @@
 				continue;
 			}
 
-			if (isPatchsetRef(cmd.getRefName())) {
+			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;
@@ -393,6 +408,8 @@
 				for (ReceiveCommand cmd : toApply) {
 					if (cmd.getResult() == Result.NOT_ATTEMPTED) {
 						sendRejection(cmd, "lock error: {0}", err.getMessage());
+						LOGGER.error(MessageFormat.format("failed to lock {0}:{1}",
+								repository.name, cmd.getRefName()), err);
 					}
 				}
 			}
@@ -436,10 +453,12 @@
 				case CREATE:
 				case UPDATE:
 				case UPDATE_NONFASTFORWARD:
-					Collection<TicketModel> tickets = processMergedTickets(cmd);
-					ticketsProcessed += tickets.size();
-					for (TicketModel ticket : tickets) {
-						ticketNotifier.queueMailing(ticket);
+					if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
+						Collection<TicketModel> tickets = processMergedTickets(cmd);
+						ticketsProcessed += tickets.size();
+						for (TicketModel ticket : tickets) {
+							ticketNotifier.queueMailing(ticket);
+						}
 					}
 					break;
 				default:
@@ -537,7 +556,7 @@
 		case MERGEABLE:
 			break;
 		default:
-			if (ticket == null || requireCleanMerge) {
+			if (ticket == null || requireMergeablePatchset) {
 				sendError("");
 				sendError("Your patchset can not be cleanly merged into {0}.", forBranch);
 				sendError("Please rebase your patchset and push again.");
diff --git a/src/main/java/com/gitblit/git/ReceiveCommandEvent.java b/src/main/java/com/gitblit/git/ReceiveCommandEvent.java
new file mode 100644
index 0000000..84dabb3
--- /dev/null
+++ b/src/main/java/com/gitblit/git/ReceiveCommandEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2014 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.git;
+
+import org.eclipse.jgit.events.RefsChangedEvent;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import com.gitblit.models.RepositoryModel;
+
+/**
+ * The event fired by other classes to allow this service to index tickets.
+ *
+ * @author James Moger
+ */
+public class ReceiveCommandEvent extends RefsChangedEvent {
+
+	public final RepositoryModel model;
+
+	public final ReceiveCommand cmd;
+
+	public ReceiveCommandEvent(RepositoryModel model, ReceiveCommand cmd) {
+		this.model = model;
+		this.cmd = cmd;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/service/MirrorService.java b/src/main/java/com/gitblit/service/MirrorService.java
index 9833d93..cf9ccb5 100644
--- a/src/main/java/com/gitblit/service/MirrorService.java
+++ b/src/main/java/com/gitblit/service/MirrorService.java
@@ -28,6 +28,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Type;
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.TrackingRefUpdate;
 import org.slf4j.Logger;
@@ -35,9 +37,11 @@
 
 import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
+import com.gitblit.git.ReceiveCommandEvent;
 import com.gitblit.manager.IRepositoryManager;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.tickets.BranchTicketService;
 import com.gitblit.utils.JGitUtils;
 
 /**
@@ -145,6 +149,7 @@
 				FetchResult result = git.fetch().setRemote(mirror.getName()).setDryRun(testing).call();
 				Collection<TrackingRefUpdate> refUpdates = result.getTrackingRefUpdates();
 				if (refUpdates.size() > 0) {
+					ReceiveCommand ticketBranchCmd = null;
 					for (TrackingRefUpdate ru : refUpdates) {
 						StringBuilder sb = new StringBuilder();
 						sb.append("updated mirror ");
@@ -161,6 +166,33 @@
 						sb.append("..");
 						sb.append(ru.getNewObjectId() == null ? "" : ru.getNewObjectId().abbreviate(7).name());
 						logger.info(sb.toString());
+
+						if (BranchTicketService.BRANCH.equals(ru.getLocalName())) {
+							ReceiveCommand.Type type = null;
+							switch (ru.getResult()) {
+							case NEW:
+								type = Type.CREATE;
+								break;
+							case FAST_FORWARD:
+								type = Type.UPDATE;
+								break;
+							case FORCED:
+								type = Type.UPDATE_NONFASTFORWARD;
+								break;
+							default:
+								type = null;
+								break;
+							}
+
+							if (type != null) {
+								ticketBranchCmd = new ReceiveCommand(ru.getOldObjectId(),
+									ru.getNewObjectId(), ru.getLocalName(), type);
+							}
+						}
+					}
+
+					if (ticketBranchCmd != null) {
+						repository.fireEvent(new ReceiveCommandEvent(model, ticketBranchCmd));
 					}
 				}
 			} catch (Exception e) {
diff --git a/src/main/java/com/gitblit/servlet/RpcServlet.java b/src/main/java/com/gitblit/servlet/RpcServlet.java
index 28f0d5b..2d59ebd 100644
--- a/src/main/java/com/gitblit/servlet/RpcServlet.java
+++ b/src/main/java/com/gitblit/servlet/RpcServlet.java
@@ -59,7 +59,7 @@
 
 	private static final long serialVersionUID = 1L;
 
-	public static final int PROTOCOL_VERSION = 6;
+	public static final int PROTOCOL_VERSION = 7;
 
 	private IStoredSettings settings;
 
@@ -383,6 +383,19 @@
 			} else {
 				response.sendError(notAllowedCode);
 			}
+		} else if (RpcRequest.REINDEX_TICKETS.equals(reqType)) {
+			if (allowManagement) {
+				if (StringUtils.isEmpty(objectName)) {
+					// reindex all tickets
+					gitblit.getTicketService().reindex();
+				} else {
+					// reindex tickets in a specific repository
+					RepositoryModel model = gitblit.getRepositoryModel(objectName);
+					gitblit.getTicketService().reindex(model);
+				}
+			} else {
+				response.sendError(notAllowedCode);
+			}
 		}
 
 		// send the result of the request
diff --git a/src/main/java/com/gitblit/tickets/BranchTicketService.java b/src/main/java/com/gitblit/tickets/BranchTicketService.java
index 14ed809..fc0bd8f 100644
--- a/src/main/java/com/gitblit/tickets/BranchTicketService.java
+++ b/src/main/java/com/gitblit/tickets/BranchTicketService.java
@@ -27,6 +27,7 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
 import javax.inject.Inject;
@@ -36,6 +37,8 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.events.RefsChangedEvent;
+import org.eclipse.jgit.events.RefsChangedListener;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.FileMode;
@@ -48,15 +51,18 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.TreeWalk;
 
 import com.gitblit.Constants;
+import com.gitblit.git.ReceiveCommandEvent;
 import com.gitblit.manager.INotificationManager;
 import com.gitblit.manager.IRepositoryManager;
 import com.gitblit.manager.IRuntimeManager;
 import com.gitblit.manager.IUserManager;
 import com.gitblit.models.PathModel;
+import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.TicketModel;
@@ -74,7 +80,7 @@
  * @author James Moger
  *
  */
-public class BranchTicketService extends ITicketService {
+public class BranchTicketService extends ITicketService implements RefsChangedListener {
 
 	public static final String BRANCH = "refs/gitblit/tickets";
 
@@ -97,6 +103,9 @@
 				repositoryManager);
 
 		lastAssignedId = new ConcurrentHashMap<String, AtomicLong>();
+
+		// register the branch ticket service for repository ref changes
+		Repository.getGlobalListenerList().addRefsChangedListener(this);
 	}
 
 	@Override
@@ -121,6 +130,66 @@
 	}
 
 	/**
+	 * Listen for tickets branch changes and (re)index tickets, as appropriate
+	 */
+	@Override
+	public synchronized void onRefsChanged(RefsChangedEvent event) {
+		if (!(event instanceof ReceiveCommandEvent)) {
+			return;
+		}
+
+		ReceiveCommandEvent branchUpdate = (ReceiveCommandEvent) event;
+		RepositoryModel repository = branchUpdate.model;
+		ReceiveCommand cmd = branchUpdate.cmd;
+		try {
+			switch (cmd.getType()) {
+			case CREATE:
+			case UPDATE_NONFASTFORWARD:
+				// reindex everything
+				reindex(repository);
+				break;
+			case UPDATE:
+				// incrementally index ticket updates
+				resetCaches(repository);
+				long start = System.nanoTime();
+				log.info("incrementally indexing {} ticket branch due to received ref update", repository.name);
+				Repository db = repositoryManager.getRepository(repository.name);
+				try {
+					Set<Long> ids = new HashSet<Long>();
+					List<PathChangeModel> paths = JGitUtils.getFilesInRange(db,
+							cmd.getOldId().getName(), cmd.getNewId().getName());
+					for (PathChangeModel path : paths) {
+						String name = path.name.substring(path.name.lastIndexOf('/') + 1);
+						if (!JOURNAL.equals(name)) {
+							continue;
+						}
+						String tid = path.path.split("/")[2];
+						long ticketId = Long.parseLong(tid);
+						if (!ids.contains(ticketId)) {
+							ids.add(ticketId);
+							TicketModel ticket = getTicket(repository, ticketId);
+							log.info(MessageFormat.format("indexing ticket #{0,number,0}: {1}",
+									ticketId, ticket.title));
+							indexer.index(ticket);
+						}
+					}
+					long end = System.nanoTime();
+					log.info("incremental indexing of {0} ticket(s) completed in {1} msecs",
+							ids.size(), TimeUnit.NANOSECONDS.toMillis(end - start));
+				} finally {
+					db.close();
+				}
+				break;
+			default:
+				log.warn("Unexpected receive type {} in BranchTicketService.onRefsChanged" + cmd.getType());
+				break;
+			}
+		} catch (Exception e) {
+			log.error("failed to reindex " + repository.name, e);
+		}
+	}
+
+	/**
 	 * Returns a RefModel for the refs/gitblit/tickets branch in the repository.
 	 * If the branch can not be found, null is returned.
 	 *
diff --git a/src/main/java/com/gitblit/tickets/ITicketService.java b/src/main/java/com/gitblit/tickets/ITicketService.java
index d04cd5e..90f9c6d 100644
--- a/src/main/java/com/gitblit/tickets/ITicketService.java
+++ b/src/main/java/com/gitblit/tickets/ITicketService.java
@@ -897,6 +897,7 @@
 	public boolean deleteAll(RepositoryModel repository) {
 		boolean success = deleteAllImpl(repository);
 		if (success) {
+			log.info("Deleted all tickets for {}", repository.name);
 			resetCaches(repository);
 			indexer.deleteAll(repository);
 		}
@@ -936,6 +937,8 @@
 		TicketModel ticket = getTicket(repository, ticketId);
 		boolean success = deleteTicketImpl(repository, ticket, deletedBy);
 		if (success) {
+			log.info(MessageFormat.format("Deleted {0} ticket #{1,number,0}: {2}",
+					repository.name, ticketId, ticket.title));
 			ticketsCache.invalidate(new TicketKey(repository, ticketId));
 			indexer.delete(ticket);
 			return true;
@@ -1074,6 +1077,7 @@
 		long end = System.nanoTime();
 		long secs = TimeUnit.NANOSECONDS.toMillis(end - start);
 		log.info("reindexing completed in {} msecs.", secs);
+		resetCaches(repository);
 	}
 
 	/**
diff --git a/src/main/java/com/gitblit/utils/RpcUtils.java b/src/main/java/com/gitblit/utils/RpcUtils.java
index 24e07dc..5e577fb 100644
--- a/src/main/java/com/gitblit/utils/RpcUtils.java
+++ b/src/main/java/com/gitblit/utils/RpcUtils.java
@@ -252,6 +252,37 @@
 	}
 
 	/**
+	 * Reindex all tickets on the Gitblit server.
+	 *
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean reindexTickets(String serverUrl, String account,
+			char[] password) throws IOException {
+		return doAction(RpcRequest.REINDEX_TICKETS, null, null, serverUrl, account,
+				password);
+	}
+
+	/**
+	 * Reindex tickets for the specified repository on the Gitblit server.
+	 *
+	 * @param serverUrl
+	 * @param repositoryName
+	 * @param account
+	 * @param password
+	 * @return true if the action succeeded
+	 * @throws IOException
+	 */
+	public static boolean reindexTickets(String serverUrl, String repositoryName,
+			String account, char[] password) throws IOException {
+		return doAction(RpcRequest.REINDEX_TICKETS, repositoryName, null, serverUrl,
+				account, password);
+	}
+
+	/**
 	 * Create a user on the Gitblit server.
 	 *
 	 * @param user
diff --git a/src/site/rpc.mkd b/src/site/rpc.mkd
index 58b2966..b86fd9a 100644
--- a/src/site/rpc.mkd
+++ b/src/site/rpc.mkd
@@ -59,6 +59,7 @@
 <tr><td>Gitblit v1.1.0</td><td>4</td></tr>
 <tr><td>Gitblit v1.2.0+</td><td>5</td></tr>
 <tr><td>Gitblit v1.3.1+</td><td>6</td></tr>
+<tr><td>Gitblit v1.4.0+</td><td>7</td></tr>
 </tbody>
 </table>
 
@@ -102,6 +103,7 @@
 <tr><td>SET_REPOSITORY_TEAM_PERMISSIONS</td><td>repository name</td><td><em>admin</em></td><td>5</td><td>List&lt;String&gt;</td><td>-</td></tr>
 <tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>ServerSettings (management keys)</td></tr>
 <tr><td>CLEAR_REPOSITORY_CACHE</td><td>-</td><td><em>-</em></td><td>4</td><td>-</td><td>-</td></tr>
+<tr><td>REINDEX_TICKETS</td><td>repository name</td><td><em>-</em></td><td>7</td><td>-</td><td>-</td></tr>
 <tr><td colspan='6'><em>web.enableRpcAdministration=true</em></td></tr>
 <tr><td>LIST_FEDERATION_REGISTRATIONS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
 <tr><td>LIST_FEDERATION_RESULTS</td><td>-</td><td><em>admin</em></td><td>1</td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
diff --git a/src/site/tickets_replication.mkd b/src/site/tickets_replication.mkd
new file mode 100644
index 0000000..542fd5f
--- /dev/null
+++ b/src/site/tickets_replication.mkd
@@ -0,0 +1,135 @@
+## Ticket Replication & Advanced Administration
+
+*SINCE 1.4.0*
+
+**Ticket Replication**
+Gitblit does *not* provide a generic/universal replication mechanism that works across all persistence backends.
+
+**Advanced Administration**
+Gitblit does *not* provide a generic/universal for advanced administration (i.e. manually tweaking ticket data) however each service does have a strategy for that case.
+
+### FileTicketService
+
+#### Ticket Replication
+Replication is not supported.
+
+#### Advanced Administration
+Use your favorite text editor to **carefully** manipulate a ticket's journal file.  I recommend using a JSON validation service to ensure your changes are valid JSON.
+
+After you've done this, you will need to reset Gitblit's internal ticket cache and you may need to reindex the tickets, depending on your changes.
+
+### BranchTicketService
+
+#### Ticket Replication
+Gitblit supports ticket replication for a couple of scenarios with the *BranchTicketService*.  This requires that the Gitblit instance receiving the ticket data be configured for the *BranchTicketService*.  Likewise, the source of the ticket data must be a repository that has ticket data persisted using the *BranchTicketService*.
+
+##### Manually Pushing refs/gitblit/tickets
+
+Let's say you wanted to create a perfect clone of the Gitblit repository hosted at https://dev.gitblit.com in your own Gitblit instance.  We'll use this repository as an example because it is configured for the *BranchTicketService*.
+
+**Assumptions**
+
+1. We are pushing to our local Gitblit with the admin account, or some other privileged account
+2. Our local Gitblit is configured for create-on-push
+3. Our local Gitblit is configured for the *BranchTicketService*
+
+**Procedure**
+
+1. First we'll clone a mirror of the source repository:<pre>git clone --mirror https://dev.gitblit.com/r/gitblit.git </pre>
+2. Then we'll add a remote for our local Gitblit instance:<pre>cd gitblit.git<br/>git remote add local https://localhost:8443/gitblit.git </pre>
+3. Then we'll push *everything* to our local Gitblit:<pre>git push --mirror local</pre>
+
+If your push was successful you should have a new repository with the entire official Gitblit tickets data.
+
+##### Mirroring refs/gitblit/tickets
+
+Gitblit 1.4.0 introduces a mirroring service.  This is not the same as the federation feature - although there are similarities.
+
+If you setup a mirror of another Gitblit repository which uses the *BranchTicketService* **AND** your Gitblit instance is configured for *BranchTicketService*, then your Gitblit will automatically fetch and reindex all tickets without intervention or further configuration.
+
+**Things to note about mirrors...**
+
+1. You must set *git.enableMirroring=true* and optionally change *git.mirrorPeriod*
+2. Mirrors are read-only.  You can not push to a mirror.  You can not manipulate a mirror's ticket data.
+3. Mirrors are a Git feature - not a Gitblit invention.  To create one you must currently use Git within your *git.repositoriesFolder*, you must reset your cache, and you must trigger a ticket reindex.<pre>git clone --mirror &lt;url&gt;<br/>curl --insecure --user admin:admin "https://localhost:8443/rpc?req=clear_repository_cache"<br/>curl --insecure --user admin:admin "https://localhost:8443/rpc?req=reindex_tickets&name=&lt;repo&gt;"</pre>
+4. After you have indexed the repository, Gitblit will take over and incrementally update your tickets data on each fetch.
+
+#### Advanced Administration
+Repository owners or Gitblit administrators have the option of manually editing ticket data.  To do this you must fetch and checkout the `refs/gitblit/tickets` ref.  This orphan branch is where ticket data is stored.  You may then use a text editor to **carefully** manipulate journals and push your changes back upstream.  I recommend using a JSON validation tool to ensure your changes are valid JSON.
+
+    git fetch origin refs/gitblit/tickets
+    git checkout -B tix FETCH_HEAD
+    ...fix data...
+    git add .
+    git commit
+    git push origin HEAD:refs/gitblit/tickets
+
+Gitblit will identify the incoming `refs/gitblit/tickets` ref update and will incrementally index the changed tickets OR, if the update is non-fast-forward, all tickets on that branch will be reindexed.
+
+### RedisTicketService
+
+#### Ticket Replication
+Redis is capable of sophisticated replication and clustering.  I have not configured Redis replication myself.  If this topic interests you please document your procedure and open a pull request to improve this section for others who may also be interested in Redis replication.
+
+#### Advanced Administration
+You can directly manipulate the journals in Redis.  The most convenient way do manipulate data is using the simple, but very competent, [RedisDesktopManager](http://redisdesktop.com).  It even provides JSON pretty printing which faciliates editing.
+
+After you've done this, you will need to reset Gitblit's internal ticket cache and you may need to reindex the tickets, depending on your changes.
+
+The schema of the Redis backend looks like this *repository:object:id*.
+
+    redis 127.0.0.1:6379> keys *
+    1) "~james/mytickets.git:ticket:8"
+    2) "~james/mytickets.git:journal:8"
+    3) "~james/mytickets.git:ticket:4"
+    4) "~james/mytickets.git:counter"
+    5) "~james/mytickets.git:journal:2"
+    6) "~james/mytickets.git:journal:4"
+    7) "~james/mytickets.git:journal:7"
+    8) "~james/mytickets.git:ticket:3"
+    9) "~james/mytickets.git:ticket:6"
+    10) "~james/mytickets.git:journal:1"
+    11) "~james/mytickets.git:ticket:2"
+    12) "~james/mytickets.git:journal:6"
+    13) "~james/mytickets.git:ticket:7"
+    14) "~james/mytickets.git:ticket:1"
+    15) "~james/mytickets.git:journal:3"
+
+**Some notes about the Redis backend**
+The *ticket* object keys are provided as a convenience for integration with other systems.  Gitblit does not read those keys, but it does update them.
+
+The *journal* object keys are the important ones.  Gitblit maintains ticket change journals.  The *journal* object keys are Redis LISTs where each list entry is a JSON change document.
+
+The other important object key is the *counter* which is used to assign ticket ids.
+
+### Resetting the Tickets Cache and Reindexing Tickets
+
+Reindexing can be memory exhaustive.  It obviously depends on the number of tickets you have.  Normally, you won't need to manually reindex but if you do, offline reindexing is recommended.
+
+#### Offline Reindexing
+
+##### Gitblit GO
+
+Gitblit GO ships with a script that executes the *com.gitblit.ReindexTickets* tool included in the Gitblit jar file.  This tool will reindex *all* tickets in *all* repositories **AND** must be run when Gitblit is offline.
+
+    reindex-tickets <baseFolder>
+
+##### Gitblit WAR/Express
+
+Gitblit WAR/Express does not ship with anything other than the WAR, but you can still reindex tickets offline with a little extra effort.
+
+*Windows*
+
+    java -cp "C:/path/to/WEB-INF/lib/*" com.gitblit.ReindexTickets --baseFolder <baseFolder>
+
+*Linux/Unix/Mac OSX*
+
+    java -cp /path/to/WEB-INF/lib/* com.gitblit.ReindexTickets --baseFolder <baseFolder>
+
+#### Live Reindexing
+
+You can trigger a live reindex of tickets for any backend using Gitblit's RPC interface and curl or your browser.  This will also reset Gitblit's internal ticket cache.  Use of this RPC requires *web.enableRpcServlet=true* and *web.enableRpcManagement=true* along with administrator credentials.
+
+    curl --insecure --user admin:admin "https://localhost:8443/rpc?req=reindex_tickets"
+    curl --insecure --user admin:admin "https://localhost:8443/rpc?req=reindex_tickets&name=gitblit.git"
+

--
Gitblit v1.9.1