summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2014-03-06 12:49:02 -0600
committerJames Moger <james.moger@gitblit.com>2014-03-06 12:49:02 -0600
commitaae58435191c1b4e73ef7c5447e7a0832c7f0e53 (patch)
tree5ddf9d5be1cc9f0197148be99576214eb92a081b
parente6ea45327e387729e200ded58b91c4e21936d647 (diff)
parent148b40840657260d103eda1897d61f2d949656de (diff)
downloadgitblit-aae58435191c1b4e73ef7c5447e7a0832c7f0e53.tar.gz
gitblit-aae58435191c1b4e73ef7c5447e7a0832c7f0e53.zip
Merged #22 "Tie mirroring, pushing, and the BranchTicketService together"
-rw-r--r--build.xml4
-rw-r--r--src/main/distrib/linux/reindex-tickets.sh9
-rw-r--r--src/main/distrib/win/reindex-tickets.cmd17
-rw-r--r--src/main/java/com/gitblit/Constants.java2
-rw-r--r--src/main/java/com/gitblit/git/GitblitReceivePack.java9
-rw-r--r--src/main/java/com/gitblit/git/PatchsetReceivePack.java33
-rw-r--r--src/main/java/com/gitblit/git/ReceiveCommandEvent.java38
-rw-r--r--src/main/java/com/gitblit/service/MirrorService.java32
-rw-r--r--src/main/java/com/gitblit/servlet/RpcServlet.java15
-rw-r--r--src/main/java/com/gitblit/tickets/BranchTicketService.java71
-rw-r--r--src/main/java/com/gitblit/tickets/ITicketService.java4
-rw-r--r--src/main/java/com/gitblit/utils/RpcUtils.java31
-rw-r--r--src/site/rpc.mkd2
-rw-r--r--src/site/tickets_replication.mkd135
14 files changed, 387 insertions, 15 deletions
diff --git a/build.xml b/build.xml
index 71e7430b..beefc54b 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 15939291..5a4fc34f 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 e28f45f5..c9116ca2 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 5b71eeb9..e93f7b1d 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -350,7 +350,7 @@ public class Constants {
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 3a0eff22..e3e2faeb 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePack.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePack.java
@@ -344,6 +344,15 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
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 ae429d2e..c0ab8aeb 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.Patchset;
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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
/** 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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
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 @@ public class PatchsetReceivePack extends GitblitReceivePack {
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 00000000..84dabb35
--- /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 9833d939..cf9ccb55 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.RefUpdate.Result;
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 org.slf4j.LoggerFactory;
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 @@ public class MirrorService implements Runnable {
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 @@ public class MirrorService implements Runnable {
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 28f0d5bf..2d59ebd7 100644
--- a/src/main/java/com/gitblit/servlet/RpcServlet.java
+++ b/src/main/java/com/gitblit/servlet/RpcServlet.java
@@ -59,7 +59,7 @@ public class RpcServlet extends JsonServlet {
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 @@ public class RpcServlet extends JsonServlet {
} 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 14ed8094..fc0bd8f0 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.Map;
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.api.errors.JGitInternalException;
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.lib.Repository;
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 @@ import com.gitblit.utils.StringUtils;
* @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 @@ public class BranchTicketService extends ITicketService {
repositoryManager);
lastAssignedId = new ConcurrentHashMap<String, AtomicLong>();
+
+ // register the branch ticket service for repository ref changes
+ Repository.getGlobalListenerList().addRefsChangedListener(this);
}
@Override
@@ -121,6 +130,66 @@ public class BranchTicketService extends ITicketService {
}
/**
+ * 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 d04cd5e1..90f9c6dd 100644
--- a/src/main/java/com/gitblit/tickets/ITicketService.java
+++ b/src/main/java/com/gitblit/tickets/ITicketService.java
@@ -897,6 +897,7 @@ public abstract class ITicketService {
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 @@ public abstract class ITicketService {
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 @@ public abstract class ITicketService {
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 24e07dcd..5e577fb6 100644
--- a/src/main/java/com/gitblit/utils/RpcUtils.java
+++ b/src/main/java/com/gitblit/utils/RpcUtils.java
@@ -252,6 +252,37 @@ public class RpcUtils {
}
/**
+ * 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 58b2966c..b86fd9ad 100644
--- a/src/site/rpc.mkd
+++ b/src/site/rpc.mkd
@@ -59,6 +59,7 @@ The Gitblit API includes methods for retrieving and interpreting RSS feeds. The
<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 @@ Use *SET_REPOSITORY_TEAM_PERMISSIONS* instead.
<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 00000000..542fd5fc
--- /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"
+