James Moger
2014-03-06 aae58435191c1b4e73ef7c5447e7a0832c7f0e53
Merged #22 "Tie mirroring, pushing, and the BranchTicketService together"
2 files added
12 files modified
402 ■■■■■ changed files
build.xml 4 ●●● patch | view | raw | blame | history
src/main/distrib/linux/reindex-tickets.sh 9 ●●●●● patch | view | raw | blame | history
src/main/distrib/win/reindex-tickets.cmd 17 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/GitblitReceivePack.java 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/PatchsetReceivePack.java 33 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/ReceiveCommandEvent.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/service/MirrorService.java 32 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RpcServlet.java 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/BranchTicketService.java 71 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/ITicketService.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/RpcUtils.java 31 ●●●●● patch | view | raw | blame | history
src/site/rpc.mkd 2 ●●●●● patch | view | raw | blame | history
src/site/tickets_replication.mkd 135 ●●●●● patch | view | raw | blame | history
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" />
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
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
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,
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));
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.");
src/main/java/com/gitblit/git/ReceiveCommandEvent.java
New file
@@ -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;
    }
}
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) {
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
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.
     *
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);
    }
    /**
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
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>
src/site/tickets_replication.mkd
New file
@@ -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"