mrbytes
2014-03-08 6be15a1cac2f98585174f501879c3685ef2a54b6
Merge branch 'master' of https://github.com/gitblit/gitblit
14 files modified
359 ■■■■■ changed files
releases.moxie 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/UserModel.java 12 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/service/LuceneService.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/BranchTicketService.java 28 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/QueryBuilder.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/RefLogUtils.java 118 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditTicketPage.java 120 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewTicketPage.html 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewTicketPage.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TicketPage.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RefsPanel.java 6 ●●●● patch | view | raw | blame | history
src/site/tickets_replication.mkd 12 ●●●● patch | view | raw | blame | history
src/site/tickets_setup.mkd 2 ●●● patch | view | raw | blame | history
releases.moxie
@@ -54,6 +54,7 @@
    - Serve repositories on both /r and /git, displaying /r because it is shorter
    - Eliminate HEAD from the blob, blame, and tree pages. That assumed a resource was available in HEAD and it may not be.
    - Eliminate Gravatar profile linking.
    - Moved Gitblit reflog from refs/gitblit/reflog to refs/meta/gitblit/reflog
    - Updated Spanish translation
    - Updated Simplified Chinese translation
    - Updated Dutch translation
src/main/java/com/gitblit/Constants.java
@@ -100,7 +100,7 @@
    public static final String HEAD = "HEAD";
    public static final String R_GITBLIT = "refs/gitblit/";
    public static final String R_META = "refs/meta/";
    public static final String R_HEADS = "refs/heads/";
src/main/java/com/gitblit/models/UserModel.java
@@ -449,9 +449,15 @@
    public boolean canEdit(TicketModel ticket, RepositoryModel repository) {
         return isAuthenticated() &&
                 (username.equals(ticket.createdBy)
                 || username.equals(ticket.responsible)
                 || canPush(repository));
                 (canPush(repository)
                 || (ticket != null && username.equals(ticket.responsible))
                 || (ticket != null && username.equals(ticket.createdBy)));
    }
    public boolean canAdmin(TicketModel ticket, RepositoryModel repository) {
         return isAuthenticated() &&
                 (canPush(repository)
                 || ticket != null && username.equals(ticket.responsible));
    }
    public boolean canReviewPatchset(RepositoryModel model) {
src/main/java/com/gitblit/service/LuceneService.java
@@ -476,8 +476,8 @@
                        && branch.equals(defaultBranch)) {
                    // indexing "default" branch
                    indexBranch = true;
                } else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) {
                    // skip Gitblit internal branches
                } else if (branch.getName().startsWith(com.gitblit.Constants.R_META)) {
                    // skip internal meta branches
                    indexBranch = false;
                } else {
                    // normal explicit branch check
@@ -807,8 +807,8 @@
                        && branch.equals(defaultBranch)) {
                    // indexing "default" branch
                    indexBranch = true;
                } else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) {
                    // ignore internal Gitblit branches
                } else if (branch.getName().startsWith(com.gitblit.Constants.R_META)) {
                    // ignore internal meta branches
                    indexBranch = false;
                } else {
                    // normal explicit branch check
src/main/java/com/gitblit/tickets/BranchTicketService.java
@@ -43,6 +43,8 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
@@ -80,7 +82,7 @@
 */
public class BranchTicketService extends ITicketService implements RefsChangedListener {
    public static final String BRANCH = "refs/gitblit/tickets";
    public static final String BRANCH = "refs/meta/gitblit/tickets";
    private static final String JOURNAL = "journal.json";
@@ -193,10 +195,32 @@
     * @return a refmodel for the gitblit tickets branch or null
     */
    private RefModel getTicketsBranch(Repository db) {
        List<RefModel> refs = JGitUtils.getRefs(db, Constants.R_GITBLIT);
        List<RefModel> refs = JGitUtils.getRefs(db, "refs/");
        Ref oldRef = null;
        for (RefModel ref : refs) {
            if (ref.reference.getName().equals(BRANCH)) {
                return ref;
            } else if (ref.reference.getName().equals("refs/gitblit/tickets")) {
                oldRef = ref.reference;
            }
        }
        if (oldRef != null) {
            // rename old ref to refs/meta/gitblit/tickets
            RefRename cmd;
            try {
                cmd = db.renameRef(oldRef.getName(), BRANCH);
                cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
                cmd.setRefLogMessage("renamed " + oldRef.getName() + " => " + BRANCH);
                Result res = cmd.rename();
                switch (res) {
                case RENAMED:
                    log.info(db.getDirectory() + " " + cmd.getRefLogMessage());
                    return getTicketsBranch(db);
                default:
                    log.error("failed to rename " + oldRef.getName() + " => " + BRANCH + " (" + res.name() + ")");
                }
            } catch (IOException e) {
                log.error("failed to rename tickets branch", e);
            }
        }
        return null;
src/main/java/com/gitblit/tickets/QueryBuilder.java
@@ -201,6 +201,12 @@
                q = q.substring(1, q.length() - 1);
            }
        }
        if (q.startsWith("AND ")) {
            q = q.substring(3).trim();
        }
        if (q.startsWith("OR ")) {
            q = q.substring(2).trim();
        }
        return q;
    }
src/main/java/com/gitblit/utils/RefLogUtils.java
@@ -72,7 +72,7 @@
 */
public class RefLogUtils {
    private static final String GB_REFLOG = "refs/gitblit/reflog";
    private static final String GB_REFLOG = "refs/meta/gitblit/reflog";
    private static final Logger LOGGER = LoggerFactory.getLogger(RefLogUtils.class);
@@ -122,32 +122,34 @@
     * @return a refmodel for the reflog branch or null
     */
    public static RefModel getRefLogBranch(Repository repository) {
        List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
        RefModel pushLog = null;
        final String GB_PUSHES = "refs/gitblit/pushes";
        List<RefModel> refs = JGitUtils.getRefs(repository, "refs/");
        Ref oldRef = null;
        for (RefModel ref : refs) {
            if (ref.reference.getName().equals(GB_REFLOG)) {
                return ref;
            } else if (ref.reference.getName().equals(GB_PUSHES)) {
                pushLog = ref;
            } else if (ref.reference.getName().equals("refs/gitblit/reflog")) {
                oldRef = ref.reference;
            } else if (ref.reference.getName().equals("refs/gitblit/pushes")) {
                oldRef = ref.reference;
            }
        }
        if (pushLog != null) {
            // rename refs/gitblit/pushes to refs/gitblit/reflog
        if (oldRef != null) {
            // rename old ref to refs/meta/gitblit/reflog
            RefRename cmd;
            try {
                cmd = repository.renameRef(GB_PUSHES, GB_REFLOG);
                cmd = repository.renameRef(oldRef.getName(), GB_REFLOG);
                cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
                cmd.setRefLogMessage("renamed " + GB_PUSHES + " => " + GB_REFLOG);
                cmd.setRefLogMessage("renamed " + oldRef.getName() + " => " + GB_REFLOG);
                Result res = cmd.rename();
                switch (res) {
                case RENAMED:
                    LOGGER.info(repository.getDirectory() + " " + cmd.getRefLogMessage());
                    return getRefLogBranch(repository);
                default:
                    LOGGER.error("failed to rename " + GB_PUSHES + " => " + GB_REFLOG + " (" + res.name() + ")");
                    LOGGER.error("failed to rename " + oldRef.getName() + " => " + GB_REFLOG + " (" + res.name() + ")");
                }
            } catch (IOException e) {
                LOGGER.error("failed to rename pushlog", e);
                LOGGER.error("failed to rename reflog", e);
            }
        }
        return null;
@@ -241,7 +243,7 @@
            ObjectId headId = repository.resolve(GB_REFLOG + "^{commit}");
            ObjectInserter odi = repository.newObjectInserter();
            try {
                // Create the in-memory index of the push log entry
                // Create the in-memory index of the reflog log entry
                DirCache index = createIndex(repository, headId, commands);
                ObjectId indexTreeId = index.writeTree(odi);
@@ -304,7 +306,7 @@
    }
    /**
     * Creates an in-memory index of the push log entry.
     * Creates an in-memory index of the reflog entry.
     *
     * @param repo
     * @param headId
@@ -428,8 +430,8 @@
     * @param minimumDate
     * @param offset
     * @param maxCount
     *             if < 0, all pushes are returned.
     * @return a list of push log entries
     *             if < 0, all entries are returned.
     * @return a list of reflog entries
     */
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository,
            Date minimumDate, int offset, int maxCount) {
@@ -507,31 +509,31 @@
    }
    /**
     * Returns the list of pushes separated by ref (e.g. each ref has it's own
     * PushLogEntry object).
     * Returns the list of entries organized by ref (e.g. each ref has it's own
     * RefLogEntry object).
     *
     * @param repositoryName
     * @param repository
     * @param maxCount
     * @return a list of push log entries separated by ref
     * @return a list of reflog entries separated by ref
     */
    public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository, int maxCount) {
        return getLogByRef(repositoryName, repository, 0, maxCount);
    }
    /**
     * Returns the list of pushes separated by ref (e.g. each ref has it's own
     * PushLogEntry object).
     * Returns the list of entries organized by ref (e.g. each ref has it's own
     * RefLogEntry object).
     *
     * @param repositoryName
     * @param repository
     * @param offset
     * @param maxCount
     * @return a list of push log entries separated by ref
     * @return a list of reflog entries separated by ref
     */
    public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository,  int offset,
            int maxCount) {
        // break the push log into ref push logs and then merge them back into a list
        // break the reflog into ref entries and then merge them back into a list
        Map<String, List<RefLogEntry>> refMap = new HashMap<String, List<RefLogEntry>>();
        List<RefLogEntry> refLog = getRefLog(repositoryName, repository, offset, maxCount);
        for (RefLogEntry entry : refLog) {
@@ -543,10 +545,10 @@
                // construct new ref-specific ref change entry
                RefLogEntry refChange;
                if (entry instanceof DailyLogEntry) {
                    // simulated push log from commits grouped by date
                    // simulated reflog from commits grouped by date
                    refChange = new DailyLogEntry(entry.repository, entry.date);
                } else {
                    // real push log entry
                    // real reflog entry
                    refChange = new RefLogEntry(entry.repository, entry.date, entry.user);
                }
                refChange.updateRef(ref, entry.getChangeType(ref), entry.getOldId(ref), entry.getNewId(ref));
@@ -577,33 +579,32 @@
     * @return a list of ref log entries separated by ref
     */
    public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository,  Date minimumDate) {
        // break the push log into ref push logs and then merge them back into a list
        // break the reflog into refs and then merge them back into a list
        Map<String, List<RefLogEntry>> refMap = new HashMap<String, List<RefLogEntry>>();
        List<RefLogEntry> pushes = getRefLog(repositoryName, repository, minimumDate);
        for (RefLogEntry push : pushes) {
            for (String ref : push.getChangedRefs()) {
        List<RefLogEntry> entries = getRefLog(repositoryName, repository, minimumDate);
        for (RefLogEntry entry : entries) {
            for (String ref : entry.getChangedRefs()) {
                if (!refMap.containsKey(ref)) {
                    refMap.put(ref, new ArrayList<RefLogEntry>());
                }
                // construct new ref-specific push log entry
                RefLogEntry refPush = new RefLogEntry(push.repository, push.date, push.user);
                refPush.updateRef(ref, push.getChangeType(ref), push.getOldId(ref), push.getNewId(ref));
                refPush.addCommits(push.getCommits(ref));
                // construct new ref-specific log entry
                RefLogEntry refPush = new RefLogEntry(entry.repository, entry.date, entry.user);
                refPush.updateRef(ref, entry.getChangeType(ref), entry.getOldId(ref), entry.getNewId(ref));
                refPush.addCommits(entry.getCommits(ref));
                refMap.get(ref).add(refPush);
            }
        }
        // merge individual ref pushes into master list
        List<RefLogEntry> refPushLog = new ArrayList<RefLogEntry>();
        for (List<RefLogEntry> refPush : refMap.values()) {
            refPushLog.addAll(refPush);
        // merge individual ref entries into master list
        List<RefLogEntry> refLog = new ArrayList<RefLogEntry>();
        for (List<RefLogEntry> entry : refMap.values()) {
            refLog.addAll(entry);
        }
        // sort ref push log
        Collections.sort(refPushLog);
        Collections.sort(refLog);
        return refPushLog;
        return refLog;
    }
    /**
@@ -614,7 +615,7 @@
     * @param minimumDate
     * @param offset
     * @param maxCount
     *             if < 0, all pushes are returned.
     *             if < 0, all entries are returned.
     * @param the timezone to use when aggregating commits by date
     * @return a list of grouped commit log entries
     */
@@ -703,7 +704,7 @@
    /**
     * Returns the list of commits separated by ref (e.g. each ref has it's own
     * PushLogEntry object for each day).
     * RefLogEntry object for each day).
     *
     * @param repositoryName
     * @param repository
@@ -713,36 +714,35 @@
     */
    public static List<DailyLogEntry> getDailyLogByRef(String repositoryName, Repository repository,
            Date minimumDate, TimeZone timezone) {
        // break the push log into ref push logs and then merge them back into a list
        // break the reflog into ref entries and then merge them back into a list
        Map<String, List<DailyLogEntry>> refMap = new HashMap<String, List<DailyLogEntry>>();
        List<DailyLogEntry> pushes = getDailyLog(repositoryName, repository, minimumDate, 0, -1, timezone);
        for (DailyLogEntry push : pushes) {
            for (String ref : push.getChangedRefs()) {
        List<DailyLogEntry> entries = getDailyLog(repositoryName, repository, minimumDate, 0, -1, timezone);
        for (DailyLogEntry entry : entries) {
            for (String ref : entry.getChangedRefs()) {
                if (!refMap.containsKey(ref)) {
                    refMap.put(ref, new ArrayList<DailyLogEntry>());
                }
                // construct new ref-specific push log entry
                DailyLogEntry refPush = new DailyLogEntry(push.repository, push.date, push.user);
                refPush.updateRef(ref, push.getChangeType(ref), push.getOldId(ref), push.getNewId(ref));
                refPush.addCommits(push.getCommits(ref));
                refMap.get(ref).add(refPush);
                // construct new ref-specific log entry
                DailyLogEntry refEntry = new DailyLogEntry(entry.repository, entry.date, entry.user);
                refEntry.updateRef(ref, entry.getChangeType(ref), entry.getOldId(ref), entry.getNewId(ref));
                refEntry.addCommits(entry.getCommits(ref));
                refMap.get(ref).add(refEntry);
            }
        }
        // merge individual ref pushes into master list
        List<DailyLogEntry> refPushLog = new ArrayList<DailyLogEntry>();
        for (List<DailyLogEntry> refPush : refMap.values()) {
            for (DailyLogEntry entry : refPush) {
        // merge individual ref entries into master list
        List<DailyLogEntry> refLog = new ArrayList<DailyLogEntry>();
        for (List<DailyLogEntry> refEntry : refMap.values()) {
            for (DailyLogEntry entry : refEntry) {
                if (entry.getCommitCount() > 0) {
                    refPushLog.add(entry);
                    refLog.add(entry);
                }
            }
        }
        // sort ref push log
        Collections.sort(refPushLog);
        Collections.sort(refLog);
        return refPushLog;
        return refLog;
    }
}
src/main/java/com/gitblit/wicket/pages/EditTicketPage.java
@@ -250,71 +250,77 @@
        status.add(new DropDownChoice<TicketModel.Status>("status", statusModel, statusChoices));
        form.add(status);
        // responsible
        Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
        if (currentUser.canAdmin(ticket, getRepositoryModel())) {
            // responsible
            Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
        for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
            if (rp.permission.atLeast(AccessPermission.PUSH) && !rp.isTeam()) {
                userlist.add(rp.registrant);
            }
        }
        List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
        for (String username : userlist) {
            UserModel user = app().users().getUserModel(username);
            if (user != null) {
                TicketResponsible responsible = new TicketResponsible(user);
                responsibles.add(responsible);
                if (user.username.equals(ticket.responsible)) {
                    responsibleModel.setObject(responsible);
            for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
                if (rp.permission.atLeast(AccessPermission.PUSH) && !rp.isTeam()) {
                    userlist.add(rp.registrant);
                }
            }
        }
        Collections.sort(responsibles);
        responsibles.add(new TicketResponsible(NIL, "", ""));
        Fragment responsible = new Fragment("responsible", "responsibleFragment", this);
        responsible.add(new DropDownChoice<TicketResponsible>("responsible", responsibleModel, responsibles));
        form.add(responsible.setVisible(!responsibles.isEmpty()));
        // milestone
        List<TicketMilestone> milestones = app().tickets().getMilestones(getRepositoryModel(), Status.Open);
        for (TicketMilestone milestone : milestones) {
            if (milestone.name.equals(ticket.milestone)) {
                milestoneModel.setObject(milestone);
                break;
            List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
            for (String username : userlist) {
                UserModel user = app().users().getUserModel(username);
                if (user != null) {
                    TicketResponsible responsible = new TicketResponsible(user);
                    responsibles.add(responsible);
                    if (user.username.equals(ticket.responsible)) {
                        responsibleModel.setObject(responsible);
                    }
                }
            }
        }
        if (milestoneModel.getObject() == null && !StringUtils.isEmpty(ticket.milestone)) {
            // ensure that this unrecognized milestone is listed
            // so that we get the <nil> selection.
            TicketMilestone tms = new TicketMilestone(ticket.milestone);
            milestones.add(tms);
            milestoneModel.setObject(tms);
        }
        if (!milestones.isEmpty()) {
            milestones.add(new TicketMilestone(NIL));
        }
            Collections.sort(responsibles);
            responsibles.add(new TicketResponsible(NIL, "", ""));
            Fragment responsible = new Fragment("responsible", "responsibleFragment", this);
            responsible.add(new DropDownChoice<TicketResponsible>("responsible", responsibleModel, responsibles));
            form.add(responsible.setVisible(!responsibles.isEmpty()));
        Fragment milestone = new Fragment("milestone", "milestoneFragment", this);
        milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
        form.add(milestone.setVisible(!milestones.isEmpty()));
        // mergeTo (integration branch)
        List<String> branches = new ArrayList<String>();
        for (String branch : getRepositoryModel().getLocalBranches()) {
            // exclude ticket branches
            if (!branch.startsWith(Constants.R_TICKET)) {
                branches.add(Repository.shortenRefName(branch));
            // milestone
            List<TicketMilestone> milestones = app().tickets().getMilestones(getRepositoryModel(), Status.Open);
            for (TicketMilestone milestone : milestones) {
                if (milestone.name.equals(ticket.milestone)) {
                    milestoneModel.setObject(milestone);
                    break;
                }
            }
            if (milestoneModel.getObject() == null && !StringUtils.isEmpty(ticket.milestone)) {
                // ensure that this unrecognized milestone is listed
                // so that we get the <nil> selection.
                TicketMilestone tms = new TicketMilestone(ticket.milestone);
                milestones.add(tms);
                milestoneModel.setObject(tms);
            }
            if (!milestones.isEmpty()) {
                milestones.add(new TicketMilestone(NIL));
            }
            Fragment milestone = new Fragment("milestone", "milestoneFragment", this);
            milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
            form.add(milestone.setVisible(!milestones.isEmpty()));
            // mergeTo (integration branch)
            List<String> branches = new ArrayList<String>();
            for (String branch : getRepositoryModel().getLocalBranches()) {
                // exclude ticket branches
                if (!branch.startsWith(Constants.R_TICKET)) {
                    branches.add(Repository.shortenRefName(branch));
                }
            }
            branches.remove(Repository.shortenRefName(getRepositoryModel().HEAD));
            branches.add(0, Repository.shortenRefName(getRepositoryModel().HEAD));
            Fragment mergeto = new Fragment("mergeto", "mergeToFragment", this);
            mergeto.add(new DropDownChoice<String>("mergeto", mergeToModel, branches));
            form.add(mergeto.setVisible(!branches.isEmpty()));
        } else {
            // user can not admin this ticket
            form.add(new Label("responsible").setVisible(false));
            form.add(new Label("milestone").setVisible(false));
            form.add(new Label("mergeto").setVisible(false));
        }
        branches.remove(Repository.shortenRefName(getRepositoryModel().HEAD));
        branches.add(0, Repository.shortenRefName(getRepositoryModel().HEAD));
        Fragment mergeto = new Fragment("mergeto", "mergeToFragment", this);
        mergeto.add(new DropDownChoice<String>("mergeto", mergeToModel, branches));
        form.add(mergeto.setVisible(!branches.isEmpty()));
        form.add(new Button("update"));
        Button cancel = new Button("cancel") {
            private static final long serialVersionUID = 1L;
src/main/java/com/gitblit/wicket/pages/NewTicketPage.html
@@ -41,6 +41,7 @@
            <tr><th><wicket:message key="gb.type"></wicket:message><span style="color:red;">*</span></th><td class="edit"><select class="input-large" wicket:id="type"></select></td></tr>
            <tr wicket:id="responsible"></tr>
            <tr wicket:id="milestone"></tr>
            <tr wicket:id="mergeto"></tr>
        </table>
    </div>
    </div>    
@@ -62,5 +63,9 @@
    <th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="milestone"></select></td>
</wicket:fragment>
<wicket:fragment wicket:id="mergeToFragment">
    <th><wicket:message key="gb.mergeTo"></wicket:message></th><td class="edit"><select class="input-large" wicket:id="mergeto"></select></td>
</wicket:fragment>
</wicket:extend>
</html>
src/main/java/com/gitblit/wicket/pages/NewTicketPage.java
@@ -29,7 +29,9 @@
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.eclipse.jgit.lib.Repository;
import com.gitblit.Constants;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.TicketModel;
@@ -61,6 +63,8 @@
    private IModel<String> topicModel;
    private IModel<String> mergeToModel;
    private IModel<TicketResponsible> responsibleModel;
    private IModel<TicketMilestone> milestoneModel;
@@ -83,6 +87,7 @@
        typeModel = Model.of(TicketModel.Type.defaultType);
        titleModel = Model.of();
        topicModel = Model.of();
        mergeToModel = Model.of(Repository.shortenRefName(getRepositoryModel().HEAD));
        responsibleModel = Model.of();
        milestoneModel = Model.of();
@@ -123,6 +128,12 @@
                    change.setField(Field.milestone, milestone.name);
                }
                // integration branch
                String mergeTo = mergeToModel.getObject();
                if (!StringUtils.isEmpty(mergeTo)) {
                    change.setField(Field.mergeTo, mergeTo);
                }
                TicketModel ticket = app().tickets().createTicket(getRepositoryModel(), 0L, change);
                if (ticket != null) {
                    TicketNotifier notifier = app().tickets().createNotifier();
@@ -149,7 +160,7 @@
        descriptionEditor.setRepository(repositoryName);
        form.add(descriptionEditor);
        if (currentUser != null && currentUser.isAuthenticated && currentUser.canPush(getRepositoryModel())) {
        if (currentUser.canAdmin(null, getRepositoryModel())) {
            // responsible
            List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
            for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
@@ -170,10 +181,26 @@
            Fragment milestone = new Fragment("milestone", "milestoneFragment", this);
            milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
            form.add(milestone.setVisible(!milestones.isEmpty()));
            // integration branch
            List<String> branches = new ArrayList<String>();
            for (String branch : getRepositoryModel().getLocalBranches()) {
                // exclude ticket branches
                if (!branch.startsWith(Constants.R_TICKET)) {
                    branches.add(Repository.shortenRefName(branch));
                }
            }
            branches.remove(Repository.shortenRefName(getRepositoryModel().HEAD));
            branches.add(0, Repository.shortenRefName(getRepositoryModel().HEAD));
            Fragment mergeto = new Fragment("mergeto", "mergeToFragment", this);
            mergeto.add(new DropDownChoice<String>("mergeto", mergeToModel, branches));
            form.add(mergeto.setVisible(!branches.isEmpty()));
        } else {
            // user does not have permission to assign milestone or responsible
            // user does not have permission to assign milestone, responsible, or mergeto
            form.add(new Label("responsible").setVisible(false));
            form.add(new Label("milestone").setVisible(false));
            form.add(new Label("mergeto").setVisible(false));
        }
        form.add(new Button("create"));
src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -327,7 +327,7 @@
         * UPDATE FORM (DISCUSSION TAB)
         */
        if (user.canEdit(ticket, repository) && app().tickets().isAcceptingTicketUpdates(repository)) {
            if (ticket.isOpen()) {
            if (user.canAdmin(ticket, repository) && ticket.isOpen()) {
                /*
                 * OPEN TICKET
                 */
@@ -988,7 +988,11 @@
        md = md.replace("${ticketId}", "" + ticketId);
        md = md.replace("${patchset}", "" + 1);
        md = md.replace("${reviewBranch}", Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticketId)));
        md = md.replace("${integrationBranch}", Repository.shortenRefName(getRepositoryModel().HEAD));
        String integrationBranch = Repository.shortenRefName(getRepositoryModel().HEAD);
        if (!StringUtils.isEmpty(ticket.mergeTo)) {
            integrationBranch = ticket.mergeTo;
        }
        md = md.replace("${integrationBranch}", integrationBranch);
        return MarkdownUtils.transformMarkdown(md);
    }
src/main/java/com/gitblit/wicket/panels/RefsPanel.java
@@ -173,11 +173,11 @@
                    // codereview refs
                    linkClass = CommitPage.class;
                    cssClass = "otherRef";
                } else if (name.startsWith(com.gitblit.Constants.R_GITBLIT)) {
                    // gitblit refs
                } else if (name.startsWith(com.gitblit.Constants.R_META)) {
                    // internal meta refs
                    linkClass = LogPage.class;
                    cssClass = "otherRef";
                    name = name.substring(com.gitblit.Constants.R_GITBLIT.length());
                    name = name.substring(com.gitblit.Constants.R_META.length());
                }
                Component c = new LinkPanel("refName", null, name, linkClass,
src/site/tickets_replication.mkd
@@ -23,7 +23,7 @@
#### 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
##### Manually Pushing refs/meta/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*.
@@ -41,7 +41,7 @@
If your push was successful you should have a new repository with the entire official Gitblit tickets data.
##### Mirroring refs/gitblit/tickets
##### Mirroring refs/meta/gitblit/tickets
Gitblit 1.4.0 introduces a mirroring service.  This is not the same as the federation feature - although there are similarities.
@@ -55,16 +55,16 @@
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.
Repository owners or Gitblit administrators have the option of manually editing ticket data.  To do this you must fetch and checkout the `refs/meta/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 fetch origin refs/meta/gitblit/tickets
    git checkout -B tix FETCH_HEAD
    ...fix data...
    git add .
    git commit
    git push origin HEAD:refs/gitblit/tickets
    git push origin HEAD:refs/meta/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.
Gitblit will identify the incoming `refs/meta/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
src/site/tickets_setup.mkd
@@ -20,7 +20,7 @@
    tickets.service = com.gitblit.tickets.BranchTicketService
Your ticket journals are persisted to `id/{shard}/{id}/journal.json`.  These journals are stored on an orphan branch, `refs/gitblit/tickets`, within your repository.  This allows you to easily clone your entire ticket history to client working copies or to mirrors.
Your ticket journals are persisted to `id/{shard}/{id}/journal.json`.  These journals are stored on an orphan branch, `refs/meta/gitblit/tickets`, within your repository.  This allows you to easily clone your entire ticket history to client working copies or to mirrors.
#### Redis Ticket Service