Example usage for org.eclipse.jgit.lib PersonIdent getEmailAddress

List of usage examples for org.eclipse.jgit.lib PersonIdent getEmailAddress

Introduction

In this page you can find the example usage for org.eclipse.jgit.lib PersonIdent getEmailAddress.

Prototype

public String getEmailAddress() 

Source Link

Document

Get email address of person

Usage

From source file:at.ac.tuwien.inso.subcat.miner.GitMiner.java

License:Open Source License

private Identity resolveIdentity(PersonIdent author) throws SQLException {
    String mail = author.getEmailAddress();
    String name = author.getName();

    assert (mail != null || name != null);

    String mapKey = (mail != null) ? mail : name;
    name = (name != null) ? name : mail;

    Identity identity = identities.get(mapKey);
    if (identity == null) {
        User user = model.addUser(project, name);
        identity = model.addIdentity(null, Model.CONTEXT_SRC, mail, name, user);
        identities.put(mapKey, identity);
    }/*  www.j a  v  a  2 s .  c o m*/

    return identity;
}

From source file:com.binarybirchtree.contributionart.RepositoryTest.java

License:Open Source License

@Test
public void validate_commits()
        throws IOException, Matrix.FileFormatException, Repository.GitException, GitAPIException {
    final int factor = 20;
    final String name = "name";
    final String email = "email";

    /////from  w  w w  .j  a  va  2  s .  c o m
    /// Encapsulates commit-validation logic.
    ///
    class CommitValidator {
        final ZonedDateTime timestamp;

        ///
        /// @param[in] commit Commit to validate.
        /// @param[in] timestamp Expected timestamp.
        /// @param[in] message Expected message.
        ///
        CommitValidator(RevCommit commit, ZonedDateTime timestamp, String message) {
            this.timestamp = timestamp;

            Assert.assertEquals(timestamp.toInstant(), Instant.ofEpochSecond(commit.getCommitTime()));
            Assert.assertEquals(message, commit.getFullMessage());

            new IdentityValidator(commit.getAuthorIdent());
            new IdentityValidator(commit.getCommitterIdent());
        }

        ///
        /// Contains shared validation logic used for both Author and Committer identities.
        ///
        class IdentityValidator {
            private PersonIdent identity;

            ///
            /// @param[in] identity Identity to validate.
            ///
            public IdentityValidator(PersonIdent identity) {
                this.identity = identity;

                validate_name();
                validate_email();
                validate_timestamp();
            }

            private void validate_name() {
                Assert.assertEquals(name, identity.getName());
            }

            private void validate_email() {
                Assert.assertEquals(email, identity.getEmailAddress());
            }

            private void validate_timestamp() {
                Assert.assertEquals(timestamp.toInstant(), identity.getWhen().toInstant());
            }
        }
    }

    Path repo = folder.newFolder().toPath();
    Matrix matrix = new Matrix(file);
    ZonedDateTime today = ZonedDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS);

    // Generate commits in a temporary repository.
    try (Repository repository = new Repository(repo, name, email)) {
        repository.illustrate(matrix, factor);
    }

    // Verify that the state of the repository is as expected.
    try (Git git = Git.open(repo.toFile())) {
        // Start from the earliest date for which commits were generated.
        ZonedDateTime current = today.with(WeekFields.SUNDAY_START.dayOfWeek(), DayOfWeek.values().length)
                .minusDays(Matrix.AREA);

        // Prepare to iterate through the definition matrix alongside the commit log,
        // as the values in the definition matrix affect how many commits should have been generated.
        Iterator<Matrix.Value> values = matrix.iterator();
        Matrix.Value value;
        int cell_iterations = 0;
        int commit_count = 0;

        // Retrieve the list of commits, sorted from earliest to latest.
        List<RevCommit> commits = Lists.reverse(Lists.newArrayList(git.log().call()));
        Assert.assertFalse(commits.isEmpty());

        // Validate the README commit.
        String readme = "README.md";
        new CommitValidator(Iterables.getLast(commits), today, String.format("Added %s.", readme));
        commits.remove(commits.size() - 1);
        Assert.assertEquals(Repository.README, new String(Files.readAllBytes(repo.resolve(readme))));

        // Iterate through the commit log, starting from the earliest commit.
        for (RevCommit commit : commits) {
            if (cell_iterations == 0) {
                Assert.assertTrue(values.hasNext());
                value = values.next();
                cell_iterations = value.weight() * factor;
                current = current.plusDays(1);
            }

            new CommitValidator(commit, current, "");

            ++commit_count;
            --cell_iterations;
        }

        // Determine the expected commit count and compare it with the actual commit count.
        int expected_commit_count = StreamSupport.stream(matrix.spliterator(), false).map(Matrix.Value::weight)
                .reduce(0, (accumulator, element) -> accumulator + element * factor);

        while (values.hasNext()) {
            expected_commit_count -= values.next().weight() * factor;
        }

        Assert.assertEquals(expected_commit_count, commit_count);
    }
}

From source file:com.centurylink.mdw.dataaccess.file.VersionControlGit.java

License:Apache License

/**
 * Does not fetch.//from w  w  w .ja v a 2s .  c  om
 */
public CommitInfo getCommitInfo(String path) throws Exception {
    Iterator<RevCommit> revCommits = git.log().addPath(path).setMaxCount(1).call().iterator();
    if (revCommits.hasNext()) {
        RevCommit revCommit = revCommits.next();
        CommitInfo commitInfo = new CommitInfo(revCommit.getId().name());
        PersonIdent committerIdent = revCommit.getCommitterIdent();
        commitInfo.setCommitter(committerIdent.getName());
        commitInfo.setEmail(committerIdent.getEmailAddress());
        if ((commitInfo.getCommitter() == null || commitInfo.getCommitter().isEmpty())
                && commitInfo.getEmail() != null)
            commitInfo.setCommitter(commitInfo.getEmail());
        commitInfo.setDate(committerIdent.getWhen());
        commitInfo.setMessage(revCommit.getShortMessage());
        return commitInfo;
    }
    return null;
}

From source file:com.gitblit.git.GitblitReceivePack.java

License:Apache License

/**
 * Instrumentation point where the incoming push event has been parsed,
 * validated, objects created BUT refs have not been updated. You might
 * use this to enforce a branch-write permissions model.
 *//*from   www  . j a v  a 2 s .c om*/
@Override
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {

    if (commands.size() == 0) {
        // no receive commands to process
        // this can happen if receive pack subclasses intercept and filter
        // the commands
        LOGGER.debug("skipping pre-receive processing, no refs created, updated, or removed");
        return;
    }

    if (repository.isMirror) {
        // repository is a mirror
        for (ReceiveCommand cmd : commands) {
            sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!",
                    repository.name);
        }
        return;
    }

    if (repository.isFrozen) {
        // repository is frozen/readonly
        for (ReceiveCommand cmd : commands) {
            sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is frozen!",
                    repository.name);
        }
        return;
    }

    if (!repository.isBare) {
        // repository has a working copy
        for (ReceiveCommand cmd : commands) {
            sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it has a working copy!",
                    repository.name);
        }
        return;
    }

    if (!canPush(commands)) {
        // user does not have push permissions
        for (ReceiveCommand cmd : commands) {
            sendRejection(cmd, "User \"{0}\" does not have push permissions for \"{1}\"!", user.username,
                    repository.name);
        }
        return;
    }

    if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
        // enforce committer verification
        if (StringUtils.isEmpty(user.emailAddress)) {
            // reject the push because the pushing account does not have an email address
            for (ReceiveCommand cmd : commands) {
                sendRejection(cmd,
                        "Sorry, the account \"{0}\" does not have an email address set for committer verification!",
                        user.username);
            }
            return;
        }

        // Optionally enforce that the committer of first parent chain
        // match the account being used to push the commits.
        //
        // This requires all merge commits are executed with the "--no-ff"
        // option to force a merge commit even if fast-forward is possible.
        // This ensures that the chain first parents has the commit
        // identity of the merging user.
        boolean allRejected = false;
        for (ReceiveCommand cmd : commands) {
            String firstParent = null;
            try {
                List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(),
                        cmd.getNewId().name());
                for (RevCommit commit : commits) {

                    if (firstParent != null) {
                        if (!commit.getName().equals(firstParent)) {
                            // ignore: commit is right-descendant of a merge
                            continue;
                        }
                    }

                    // update expected next commit id
                    if (commit.getParentCount() == 0) {
                        firstParent = null;
                    } else {
                        firstParent = commit.getParents()[0].getId().getName();
                    }

                    PersonIdent committer = commit.getCommitterIdent();
                    if (!user.is(committer.getName(), committer.getEmailAddress())) {
                        // verification failed
                        String reason = MessageFormat.format(
                                "{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>", commit.getId().name(),
                                committer.getName(),
                                StringUtils.isEmpty(committer.getEmailAddress()) ? "?"
                                        : committer.getEmailAddress(),
                                user.getDisplayName(), user.username, user.emailAddress);
                        LOGGER.warn(reason);
                        cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
                        allRejected &= true;
                        break;
                    } else {
                        allRejected = false;
                    }
                }
            } catch (Exception e) {
                LOGGER.error("Failed to verify commits were made by pushing user", e);
            }
        }

        if (allRejected) {
            // all ref updates rejected, abort
            return;
        }
    }

    for (ReceiveCommand cmd : commands) {
        String ref = cmd.getRefName();
        if (ref.startsWith(Constants.R_HEADS)) {
            switch (cmd.getType()) {
            case UPDATE_NONFASTFORWARD:
            case DELETE:
                // reset branch commit cache on REWIND and DELETE
                CommitCache.instance().clear(repository.name, ref);
                break;
            default:
                break;
            }
        } else if (ref.equals(BranchTicketService.BRANCH)) {
            // ensure pushing user is an administrator OR an owner
            // i.e. prevent ticket tampering
            boolean permitted = user.canAdmin() || repository.isOwner(user.username);
            if (!permitted) {
                sendRejection(cmd, "{0} is not permitted to push to {1}", user.username, ref);
            }
        } else if (ref.startsWith(Constants.R_FOR)) {
            // prevent accidental push to refs/for
            sendRejection(cmd, "{0} is not configured to receive patchsets", repository.name);
        }
    }

    // call pre-receive plugins
    for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
        try {
            hook.onPreReceive(this, commands);
        } catch (Exception e) {
            LOGGER.error("Failed to execute extension", e);
        }
    }

    Set<String> scripts = new LinkedHashSet<String>();
    scripts.addAll(gitblit.getPreReceiveScriptsInherited(repository));
    if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
        scripts.addAll(repository.preReceiveScripts);
    }
    runGroovy(commands, scripts);
    for (ReceiveCommand cmd : commands) {
        if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
            LOGGER.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId().getName(),
                    cmd.getResult(), cmd.getMessage()));
        }
    }
}

From source file:com.gitblit.git.ReceiveHook.java

License:Apache License

/**
 * Instrumentation point where the incoming push event has been parsed,
 * validated, objects created BUT refs have not been updated. You might
 * use this to enforce a branch-write permissions model.
 *///from   w  w w .  ja  v a2 s . c o m
@Override
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
    if (repository.isFrozen) {
        // repository is frozen/readonly
        String reason = MessageFormat.format("Gitblit does not allow pushes to \"{0}\" because it is frozen!",
                repository.name);
        logger.warn(reason);
        for (ReceiveCommand cmd : commands) {
            cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
        }
        return;
    }

    if (!repository.isBare) {
        // repository has a working copy
        String reason = MessageFormat.format(
                "Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name);
        logger.warn(reason);
        for (ReceiveCommand cmd : commands) {
            cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
        }
        return;
    }

    if (!user.canPush(repository)) {
        // user does not have push permissions
        String reason = MessageFormat.format("User \"{0}\" does not have push permissions for \"{1}\"!",
                user.username, repository.name);
        logger.warn(reason);
        for (ReceiveCommand cmd : commands) {
            cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
        }
        return;
    }

    if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
        // enforce committer verification
        if (StringUtils.isEmpty(user.emailAddress)) {
            // emit warning if user does not have an email address 
            logger.warn(MessageFormat.format(
                    "Consider setting an email address for {0} ({1}) to improve committer verification.",
                    user.getDisplayName(), user.username));
        }

        // Optionally enforce that the committer of the left parent chain
        // match the account being used to push the commits.
        // 
        // This requires all merge commits are executed with the "--no-ff"
        // option to force a merge commit even if fast-forward is possible.
        // This ensures that the chain of left parents has the commit
        // identity of the merging user.
        boolean allRejected = false;
        for (ReceiveCommand cmd : commands) {
            String linearParent = null;
            try {
                List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(),
                        cmd.getNewId().name());
                for (RevCommit commit : commits) {

                    if (linearParent != null) {
                        if (!commit.getName().equals(linearParent)) {
                            // ignore: commit is right-descendant of a merge
                            continue;
                        }
                    }

                    // update expected next commit id
                    if (commit.getParentCount() == 0) {
                        linearParent = null;
                    } else {
                        linearParent = commit.getParents()[0].getId().getName();
                    }

                    PersonIdent committer = commit.getCommitterIdent();
                    if (!user.is(committer.getName(), committer.getEmailAddress())) {
                        String reason;
                        if (StringUtils.isEmpty(user.emailAddress)) {
                            // account does not have an email address
                            reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4})",
                                    commit.getId().name(), committer.getName(),
                                    StringUtils.isEmpty(committer.getEmailAddress()) ? "?"
                                            : committer.getEmailAddress(),
                                    user.getDisplayName(), user.username);
                        } else {
                            // account has an email address
                            reason = MessageFormat.format(
                                    "{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>",
                                    commit.getId().name(), committer.getName(),
                                    StringUtils.isEmpty(committer.getEmailAddress()) ? "?"
                                            : committer.getEmailAddress(),
                                    user.getDisplayName(), user.username, user.emailAddress);
                        }
                        logger.warn(reason);
                        cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
                        allRejected &= true;
                        break;
                    } else {
                        allRejected = false;
                    }
                }
            } catch (Exception e) {
                logger.error("Failed to verify commits were made by pushing user", e);
            }
        }

        if (allRejected) {
            // all ref updates rejected, abort
            return;
        }
    }

    // reset branch commit cache on REWIND and DELETE
    for (ReceiveCommand cmd : commands) {
        String ref = cmd.getRefName();
        if (ref.startsWith(Constants.R_HEADS)) {
            switch (cmd.getType()) {
            case UPDATE_NONFASTFORWARD:
            case DELETE:
                CommitCache.instance().clear(repository.name, ref);
                break;
            default:
                break;
            }
        }
    }

    Set<String> scripts = new LinkedHashSet<String>();
    scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
    if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
        scripts.addAll(repository.preReceiveScripts);
    }
    runGroovy(repository, user, commands, rp, scripts);
    for (ReceiveCommand cmd : commands) {
        if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
            logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId().getName(),
                    cmd.getResult(), cmd.getMessage()));
        }
    }
}

From source file:com.gitblit.plugin.flowdock.FlowDockReceiveHook.java

License:Apache License

@Override
public void onPostReceive(GitblitReceivePack receivePack, Collection<ReceiveCommand> commands) {
    if (!shallPost(receivePack, commands)) {
        return;// w  ww .  j av  a  2s . c o  m
    }

    IRuntimeManager runtimeManager = GitblitContext.getManager(IRuntimeManager.class);
    try {
        for (ReceiveCommand cmd : commands) {
            if (cmd.getRefName().startsWith(Constants.R_TAGS)) {
                boolean shallPostTag = runtimeManager.getSettings().getBoolean(Plugin.SETTING_POST_TAGS, true);
                if (!shallPostTag) {
                    continue;
                }
            } else if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
                boolean shallPostBranch = runtimeManager.getSettings().getBoolean(Plugin.SETTING_POST_BRANCHES,
                        true);
                if (!shallPostBranch) {
                    continue;
                }
            } else {
                // ignore other refs
                continue;
            }

            RepositoryModel repo = receivePack.getRepositoryModel();

            String repoUrl = getUrl(repo.name, null, null);
            String diffUrl = getUrl(repo.name, cmd.getOldId().getName(), cmd.getNewId().getName());

            GitPayload payload = new GitPayload().pusher(receivePack.getUserModel()).repository(repo.name)
                    .repoUrl(repoUrl).tags(getTags(repo)).ref(cmd.getRefName())
                    .refName(Repository.shortenRefName(cmd.getRefName())).diffUrl(diffUrl)
                    .before(cmd.getOldId().getName()).after(cmd.getNewId().getName());

            List<RevCommit> commits = getCommits(receivePack, cmd.getOldId().name(), cmd.getNewId().name());
            for (RevCommit commit : commits) {
                Commit c = new Commit();
                c.id = commit.getName();
                c.url = getUrl(repo.name, null, commit.getName());
                c.message = commit.getFullMessage().trim();

                PersonIdent author = commit.getAuthorIdent();
                c.author = new Ident(author.getName(), author.getEmailAddress());
                c.timestamp = author.getWhen();
                if (c.timestamp == null) {
                    c.timestamp = commit.getCommitterIdent().getWhen();
                }

                List<PathChangeModel> paths = JGitUtils.getFilesInCommit(receivePack.getRepository(), commit);
                c.added = filter(paths, ChangeType.ADD);
                c.modified = filter(paths, ChangeType.MODIFY);
                c.removed = filter(paths, ChangeType.DELETE);

                payload.add(c);
            }

            flowdock.setFlow(repo, payload);
            flowdock.sendAsync(payload);
        }
    } catch (Exception e) {
        log.error("Failed to notify FlowDock!", e);
    }
}

From source file:com.gitblit.utils.JGitUtils.java

License:Apache License

/**
 * Returns the displayable name of the person in the form "Real Name <email
 * address>".  If the email address is empty, just "Real Name" is returned.
 *
 * @param person//  www.  j  a  va  2 s.c  o m
 * @return "Real Name <email address>" or "Real Name"
 */
public static String getDisplayName(PersonIdent person) {
    if (StringUtils.isEmpty(person.getEmailAddress())) {
        return person.getName();
    }
    final StringBuilder r = new StringBuilder();
    r.append(person.getName());
    r.append(" <");
    r.append(person.getEmailAddress());
    r.append('>');
    return r.toString().trim();
}

From source file:com.gitblit.utils.RefLogUtils.java

License:Apache License

private static UserModel newUserModelFrom(PersonIdent ident) {
    String name = ident.getName();
    String username;//from www  .  j av  a  2  s . c o m
    String displayname;
    if (name.indexOf('/') > -1) {
        int slash = name.indexOf('/');
        displayname = name.substring(0, slash);
        username = name.substring(slash + 1);
    } else {
        displayname = name;
        username = ident.getEmailAddress();
    }

    UserModel user = new UserModel(username);
    user.displayName = displayname;
    user.emailAddress = ident.getEmailAddress();
    return user;
}

From source file:com.gitblit.wicket.pages.RepositoryPage.java

License:Apache License

protected Component createPersonPanel(String wicketId, PersonIdent identity, Constants.SearchType searchType) {
    String name = identity == null ? "" : identity.getName();
    String address = identity == null ? "" : identity.getEmailAddress();
    name = StringUtils.removeNewlines(name);
    address = StringUtils.removeNewlines(address);
    boolean showEmail = app().settings().getBoolean(Keys.web.showEmailAddresses, false);
    if (!showEmail || StringUtils.isEmpty(name) || StringUtils.isEmpty(address)) {
        String value = name;//  ww  w  . j  a  v a  2  s  .c  om
        if (StringUtils.isEmpty(value)) {
            if (showEmail) {
                value = address;
            } else {
                value = getString("gb.missingUsername");
            }
        }
        Fragment partial = new Fragment(wicketId, "partialPersonIdent", RepositoryPage.this);
        LinkPanel link = new LinkPanel("personName", "list", value, GitSearchPage.class,
                WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType));
        setPersonSearchTooltip(link, value, searchType);
        partial.add(link);
        return partial;
    } else {
        Fragment fullPerson = new Fragment(wicketId, "fullPersonIdent", RepositoryPage.this);
        LinkPanel nameLink = new LinkPanel("personName", "list", name, GitSearchPage.class,
                WicketUtils.newSearchParameter(repositoryName, objectId, name, searchType));
        setPersonSearchTooltip(nameLink, name, searchType);
        fullPerson.add(nameLink);

        LinkPanel addressLink = new LinkPanel("personAddress", "hidden-phone list", "<" + address + ">",
                GitSearchPage.class,
                WicketUtils.newSearchParameter(repositoryName, objectId, address, searchType));
        setPersonSearchTooltip(addressLink, address, searchType);
        fullPerson.add(addressLink);
        return fullPerson;
    }
}

From source file:com.gitblit.wicket.pages.TicketPage.java

License:Apache License

public TicketPage(PageParameters params) {
    super(params);

    final UserModel user = GitBlitWebSession.get().getUser() == null ? UserModel.ANONYMOUS
            : GitBlitWebSession.get().getUser();
    final RepositoryModel repository = getRepositoryModel();
    final String id = WicketUtils.getObject(params);
    long ticketId = Long.parseLong(id);
    ticket = app().tickets().getTicket(repository, ticketId);

    if (ticket == null) {
        // ticket not found
        throw new RestartResponseException(TicketsPage.class,
                WicketUtils.newRepositoryParameter(repositoryName));
    }/*from www .  j  av  a2  s .  c om*/

    final List<Change> revisions = new ArrayList<Change>();
    List<Change> comments = new ArrayList<Change>();
    List<Change> statusChanges = new ArrayList<Change>();
    List<Change> discussion = new ArrayList<Change>();
    for (Change change : ticket.changes) {
        if (change.hasComment() || (change.isStatusChange() && (change.getStatus() != Status.New))) {
            discussion.add(change);
        }
        if (change.hasComment()) {
            comments.add(change);
        }
        if (change.hasPatchset()) {
            revisions.add(change);
        }
        if (change.isStatusChange() && !change.hasPatchset()) {
            statusChanges.add(change);
        }
    }

    final Change currentRevision = revisions.isEmpty() ? null : revisions.get(revisions.size() - 1);
    final Patchset currentPatchset = ticket.getCurrentPatchset();

    /*
     * TICKET HEADER
     */
    String href = urlFor(TicketsPage.class, params).toString();
    add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
    Label headerStatus = new Label("headerStatus", ticket.status.toString());
    WicketUtils.setCssClass(headerStatus, TicketsUI.getLozengeClass(ticket.status, false));
    add(headerStatus);
    add(new Label("ticketTitle", ticket.title));
    if (currentPatchset == null) {
        add(new Label("diffstat").setVisible(false));
    } else {
        // calculate the current diffstat of the patchset
        add(new DiffStatPanel("diffstat", ticket.insertions, ticket.deletions));
    }

    /*
     * TAB TITLES
     */
    add(new Label("commentCount", "" + comments.size()).setVisible(!comments.isEmpty()));
    add(new Label("commitCount", "" + (currentPatchset == null ? 0 : currentPatchset.commits))
            .setVisible(currentPatchset != null));

    /*
     * TICKET AUTHOR and DATE (DISCUSSION TAB)
     */
    UserModel createdBy = app().users().getUserModel(ticket.createdBy);
    if (createdBy == null) {
        add(new Label("whoCreated", ticket.createdBy));
    } else {
        add(new LinkPanel("whoCreated", null, createdBy.getDisplayName(), UserPage.class,
                WicketUtils.newUsernameParameter(createdBy.username)));
    }

    if (ticket.isProposal()) {
        // clearly indicate this is a change ticket
        add(new Label("creationMessage", getString("gb.proposedThisChange")));
    } else {
        // standard ticket
        add(new Label("creationMessage", getString("gb.createdThisTicket")));
    }

    String dateFormat = app().settings().getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
    String timestampFormat = app().settings().getString(Keys.web.datetimestampLongFormat, "EEEE, MMMM d, yyyy");
    final TimeZone timezone = getTimeZone();
    final DateFormat df = new SimpleDateFormat(dateFormat);
    df.setTimeZone(timezone);
    final DateFormat tsf = new SimpleDateFormat(timestampFormat);
    tsf.setTimeZone(timezone);
    final Calendar cal = Calendar.getInstance(timezone);

    String fuzzydate;
    TimeUtils tu = getTimeUtils();
    Date createdDate = ticket.created;
    if (TimeUtils.isToday(createdDate, timezone)) {
        fuzzydate = tu.today();
    } else if (TimeUtils.isYesterday(createdDate, timezone)) {
        fuzzydate = tu.yesterday();
    } else {
        // calculate a fuzzy time ago date
        cal.setTime(createdDate);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        createdDate = cal.getTime();
        fuzzydate = getTimeUtils().timeAgo(createdDate);
    }
    Label when = new Label("whenCreated", fuzzydate + ", " + df.format(createdDate));
    WicketUtils.setHtmlTooltip(when, tsf.format(ticket.created));
    add(when);

    String exportHref = urlFor(ExportTicketPage.class, params).toString();
    add(new ExternalLink("exportJson", exportHref, "json"));

    /*
     * RESPONSIBLE (DISCUSSION TAB)
     */
    if (StringUtils.isEmpty(ticket.responsible)) {
        add(new Label("responsible"));
    } else {
        UserModel responsible = app().users().getUserModel(ticket.responsible);
        if (responsible == null) {
            add(new Label("responsible", ticket.responsible));
        } else {
            add(new LinkPanel("responsible", null, responsible.getDisplayName(), UserPage.class,
                    WicketUtils.newUsernameParameter(responsible.username)));
        }
    }

    /*
     * MILESTONE PROGRESS (DISCUSSION TAB)
     */
    if (StringUtils.isEmpty(ticket.milestone)) {
        add(new Label("milestone"));
    } else {
        // link to milestone query
        TicketMilestone tm = app().tickets().getMilestone(repository, ticket.milestone);
        if (tm == null) {
            tm = new TicketMilestone(ticket.milestone);
        }
        PageParameters milestoneParameters;
        if (tm.isOpen()) {
            milestoneParameters = WicketUtils.newOpenTicketsParameter(repositoryName);
        } else {
            milestoneParameters = WicketUtils.newRepositoryParameter(repositoryName);
        }
        milestoneParameters.add(Lucene.milestone.name(), ticket.milestone);
        int progress = 0;
        int open = 0;
        int closed = 0;
        if (tm != null) {
            progress = tm.getProgress();
            open = tm.getOpenTickets();
            closed = tm.getClosedTickets();
        }

        Fragment milestoneProgress = new Fragment("milestone", "milestoneProgressFragment", TicketPage.this);
        milestoneProgress
                .add(new LinkPanel("link", null, ticket.milestone, TicketsPage.class, milestoneParameters));
        Label label = new Label("progress");
        WicketUtils.setCssStyle(label, "width:" + progress + "%;");
        milestoneProgress.add(label);
        WicketUtils.setHtmlTooltip(milestoneProgress,
                MessageFormat.format(getString("gb.milestoneProgress"), open, closed));
        add(milestoneProgress);
    }

    /*
     * TICKET DESCRIPTION (DISCUSSION TAB)
     */
    String desc;
    if (StringUtils.isEmpty(ticket.body)) {
        desc = getString("gb.noDescriptionGiven");
    } else {
        String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.body);
        String html = MarkdownUtils.transformGFM(app().settings(), bugtraq, ticket.repository);
        String safeHtml = app().xssFilter().relaxed(html);
        desc = safeHtml;
    }
    add(new Label("ticketDescription", desc).setEscapeModelStrings(false));

    /*
     * PARTICIPANTS (DISCUSSION TAB)
     */
    if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
        // gravatar allowed
        List<String> participants = ticket.getParticipants();
        add(new Label("participantsLabel",
                MessageFormat.format(
                        getString(participants.size() > 1 ? "gb.nParticipants" : "gb.oneParticipant"),
                        "<b>" + participants.size() + "</b>")).setEscapeModelStrings(false));
        ListDataProvider<String> participantsDp = new ListDataProvider<String>(participants);
        DataView<String> participantsView = new DataView<String>("participants", participantsDp) {
            private static final long serialVersionUID = 1L;

            @Override
            public void populateItem(final Item<String> item) {
                String username = item.getModelObject();
                UserModel user = app().users().getUserModel(username);
                if (user == null) {
                    user = new UserModel(username);
                }
                item.add(new AvatarImage("participant", user.getDisplayName(), user.emailAddress, null, 25,
                        true));
            }
        };
        add(participantsView);
    } else {
        // gravatar prohibited
        add(new Label("participantsLabel").setVisible(false));
        add(new Label("participants").setVisible(false));
    }

    /*
     * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
     */
    Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", TicketPage.this);
    Label ticketIcon = TicketsUI.getStateIcon("ticketIcon", ticket);
    ticketStatus.add(ticketIcon);
    ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
    WicketUtils.setCssClass(ticketStatus, TicketsUI.getLozengeClass(ticket.status, false));
    add(ticketStatus);

    /*
     * UPDATE FORM (DISCUSSION TAB)
     */
    if (user.canEdit(ticket, repository) && app().tickets().isAcceptingTicketUpdates(repository)) {
        if (user.canAdmin(ticket, repository) && ticket.isOpen()) {
            /*
             * OPEN TICKET
             */
            Fragment controls = new Fragment("controls", "openControlsFragment", TicketPage.this);

            /*
             * STATUS
             */
            List<Status> choices = new ArrayList<Status>();
            if (ticket.isProposal()) {
                choices.addAll(Arrays.asList(TicketModel.Status.proposalWorkflow));
            } else if (ticket.isBug()) {
                choices.addAll(Arrays.asList(TicketModel.Status.bugWorkflow));
            } else {
                choices.addAll(Arrays.asList(TicketModel.Status.requestWorkflow));
            }
            choices.remove(ticket.status);

            ListDataProvider<Status> workflowDp = new ListDataProvider<Status>(choices);
            DataView<Status> statusView = new DataView<Status>("newStatus", workflowDp) {
                private static final long serialVersionUID = 1L;

                @Override
                public void populateItem(final Item<Status> item) {
                    SimpleAjaxLink<Status> link = new SimpleAjaxLink<Status>("link", item.getModel()) {

                        private static final long serialVersionUID = 1L;

                        @Override
                        public void onClick(AjaxRequestTarget target) {
                            Status status = getModel().getObject();
                            Change change = new Change(user.username);
                            change.setField(Field.status, status);
                            if (!ticket.isWatching(user.username)) {
                                change.watch(user.username);
                            }
                            TicketModel update = app().tickets().updateTicket(repository, ticket.number,
                                    change);
                            app().tickets().createNotifier().sendMailing(update);
                            redirectTo(TicketsPage.class, getPageParameters());
                        }
                    };
                    String css = TicketsUI.getStatusClass(item.getModel().getObject());
                    WicketUtils.setCssClass(link, css);
                    item.add(link);
                }
            };
            controls.add(statusView);

            /*
             * RESPONSIBLE LIST
             */
            Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
            if (UserModel.ANONYMOUS.canPush(getRepositoryModel())
                    || AuthorizationControl.AUTHENTICATED == getRepositoryModel().authorizationControl) {
                //    authorization is ANONYMOUS or AUTHENTICATED (i.e. all users can be set responsible)
                userlist.addAll(app().users().getAllUsernames());
            } else {
                // authorization is by NAMED users (users with PUSH permission can be set responsible)
                for (RegistrantAccessPermission rp : app().repositories()
                        .getUserAccessPermissions(getRepositoryModel())) {
                    if (rp.permission.atLeast(AccessPermission.PUSH)) {
                        userlist.add(rp.registrant);
                    }
                }
            }
            List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
            if (!StringUtils.isEmpty(ticket.responsible)) {
                // exclude the current responsible
                userlist.remove(ticket.responsible);
            }
            for (String username : userlist) {
                UserModel u = app().users().getUserModel(username);
                if (u != null) {
                    responsibles.add(new TicketResponsible(u));
                }
            }
            Collections.sort(responsibles);
            responsibles.add(new TicketResponsible(ESC_NIL, "", ""));
            ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(
                    responsibles);
            DataView<TicketResponsible> responsibleView = new DataView<TicketResponsible>("newResponsible",
                    responsibleDp) {
                private static final long serialVersionUID = 1L;

                @Override
                public void populateItem(final Item<TicketResponsible> item) {
                    SimpleAjaxLink<TicketResponsible> link = new SimpleAjaxLink<TicketResponsible>("link",
                            item.getModel()) {

                        private static final long serialVersionUID = 1L;

                        @Override
                        public void onClick(AjaxRequestTarget target) {
                            TicketResponsible responsible = getModel().getObject();
                            Change change = new Change(user.username);
                            change.setField(Field.responsible, responsible.username);
                            if (!StringUtils.isEmpty(responsible.username)) {
                                if (!ticket.isWatching(responsible.username)) {
                                    change.watch(responsible.username);
                                }
                            }
                            if (!ticket.isWatching(user.username)) {
                                change.watch(user.username);
                            }
                            TicketModel update = app().tickets().updateTicket(repository, ticket.number,
                                    change);
                            app().tickets().createNotifier().sendMailing(update);
                            redirectTo(TicketsPage.class, getPageParameters());
                        }
                    };
                    item.add(link);
                }
            };
            controls.add(responsibleView);

            /*
             * MILESTONE LIST
             */
            List<TicketMilestone> milestones = app().tickets().getMilestones(repository, Status.Open);
            if (!StringUtils.isEmpty(ticket.milestone)) {
                for (TicketMilestone milestone : milestones) {
                    if (milestone.name.equals(ticket.milestone)) {
                        milestones.remove(milestone);
                        break;
                    }
                }
            }
            milestones.add(new TicketMilestone(ESC_NIL));
            ListDataProvider<TicketMilestone> milestoneDp = new ListDataProvider<TicketMilestone>(milestones);
            DataView<TicketMilestone> milestoneView = new DataView<TicketMilestone>("newMilestone",
                    milestoneDp) {
                private static final long serialVersionUID = 1L;

                @Override
                public void populateItem(final Item<TicketMilestone> item) {
                    SimpleAjaxLink<TicketMilestone> link = new SimpleAjaxLink<TicketMilestone>("link",
                            item.getModel()) {

                        private static final long serialVersionUID = 1L;

                        @Override
                        public void onClick(AjaxRequestTarget target) {
                            TicketMilestone milestone = getModel().getObject();
                            Change change = new Change(user.username);
                            if (NIL.equals(milestone.name) || ESC_NIL.equals(milestone.name)) {
                                change.setField(Field.milestone, "");
                            } else {
                                change.setField(Field.milestone, milestone.name);
                            }
                            if (!ticket.isWatching(user.username)) {
                                change.watch(user.username);
                            }
                            TicketModel update = app().tickets().updateTicket(repository, ticket.number,
                                    change);
                            app().tickets().createNotifier().sendMailing(update);
                            redirectTo(TicketsPage.class, getPageParameters());
                        }
                    };
                    item.add(link);
                }
            };
            controls.add(milestoneView);

            String editHref = urlFor(EditTicketPage.class, params).toString();
            controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));

            add(controls);
        } else {
            /*
             * CLOSED TICKET
             */
            Fragment controls = new Fragment("controls", "closedControlsFragment", TicketPage.this);

            String editHref = urlFor(EditTicketPage.class, params).toString();
            controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));

            add(controls);
        }
    } else {
        add(new Label("controls").setVisible(false));
    }

    /*
     * TICKET METADATA
     */
    add(new Label("ticketType", ticket.type.toString()));

    add(new Label("priority", ticket.priority.toString()));
    add(new Label("severity", ticket.severity.toString()));

    if (StringUtils.isEmpty(ticket.topic)) {
        add(new Label("ticketTopic").setVisible(false));
    } else {
        // process the topic using the bugtraq config to link things
        String topic = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.topic);
        String safeTopic = app().xssFilter().relaxed(topic);
        add(new Label("ticketTopic", safeTopic).setEscapeModelStrings(false));
    }

    /*
     * VOTERS
     */
    List<String> voters = ticket.getVoters();
    Label votersCount = new Label("votes", "" + voters.size());
    if (voters.size() == 0) {
        WicketUtils.setCssClass(votersCount, "badge");
    } else {
        WicketUtils.setCssClass(votersCount, "badge badge-info");
    }
    add(votersCount);
    if (user.isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
        Model<String> model;
        if (ticket.isVoter(user.username)) {
            model = Model.of(getString("gb.removeVote"));
        } else {
            model = Model.of(MessageFormat.format(getString("gb.vote"), ticket.type.toString()));
        }
        SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("voteLink", model) {

            private static final long serialVersionUID = 1L;

            @Override
            public void onClick(AjaxRequestTarget target) {
                Change change = new Change(user.username);
                if (ticket.isVoter(user.username)) {
                    change.unvote(user.username);
                } else {
                    change.vote(user.username);
                }
                app().tickets().updateTicket(repository, ticket.number, change);
                redirectTo(TicketsPage.class, getPageParameters());
            }
        };
        add(link);
    } else {
        add(new Label("voteLink").setVisible(false));
    }

    /*
     * WATCHERS
     */
    List<String> watchers = ticket.getWatchers();
    Label watchersCount = new Label("watchers", "" + watchers.size());
    if (watchers.size() == 0) {
        WicketUtils.setCssClass(watchersCount, "badge");
    } else {
        WicketUtils.setCssClass(watchersCount, "badge badge-info");
    }
    add(watchersCount);
    if (user.isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
        Model<String> model;
        if (ticket.isWatching(user.username)) {
            model = Model.of(getString("gb.stopWatching"));
        } else {
            model = Model.of(MessageFormat.format(getString("gb.watch"), ticket.type.toString()));
        }
        SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("watchLink", model) {

            private static final long serialVersionUID = 1L;

            @Override
            public void onClick(AjaxRequestTarget target) {
                Change change = new Change(user.username);
                if (ticket.isWatching(user.username)) {
                    change.unwatch(user.username);
                } else {
                    change.watch(user.username);
                }
                app().tickets().updateTicket(repository, ticket.number, change);
                redirectTo(TicketsPage.class, getPageParameters());
            }
        };
        add(link);
    } else {
        add(new Label("watchLink").setVisible(false));
    }

    /*
     * TOPIC & LABELS (DISCUSSION TAB->SIDE BAR)
     */
    ListDataProvider<String> labelsDp = new ListDataProvider<String>(ticket.getLabels());
    DataView<String> labelsView = new DataView<String>("labels", labelsDp) {
        private static final long serialVersionUID = 1L;

        @Override
        public void populateItem(final Item<String> item) {
            final String value = item.getModelObject();
            Label label = new Label("label", value);
            TicketLabel tLabel = app().tickets().getLabel(repository, value);
            String background = MessageFormat.format("background-color:{0};", tLabel.color);
            label.add(new AttributeModifier("style", background));
            item.add(label);
        }
    };

    add(labelsView);

    /*
     * COMMENTS & STATUS CHANGES (DISCUSSION TAB)
     */
    if (comments.size() == 0) {
        add(new Label("discussion").setVisible(false));
    } else {
        Fragment discussionFragment = new Fragment("discussion", "discussionFragment", TicketPage.this);
        ListDataProvider<Change> discussionDp = new ListDataProvider<Change>(discussion);
        DataView<Change> discussionView = new DataView<Change>("discussion", discussionDp) {
            private static final long serialVersionUID = 1L;

            @Override
            public void populateItem(final Item<Change> item) {
                final Change entry = item.getModelObject();
                if (entry.isMerge()) {
                    /*
                     * MERGE
                     */
                    String resolvedBy = entry.getString(Field.mergeSha);

                    // identify the merged patch, it is likely the last
                    Patchset mergedPatch = null;
                    for (Change c : revisions) {
                        if (c.patchset.tip.equals(resolvedBy)) {
                            mergedPatch = c.patchset;
                            break;
                        }
                    }

                    String commitLink;
                    if (mergedPatch == null) {
                        // shouldn't happen, but just-in-case
                        int len = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
                        commitLink = resolvedBy.substring(0, len);
                    } else {
                        // expected result
                        commitLink = mergedPatch.toString();
                    }

                    Fragment mergeFragment = new Fragment("entry", "mergeFragment", TicketPage.this);
                    mergeFragment.add(new LinkPanel("commitLink", null, commitLink, CommitPage.class,
                            WicketUtils.newObjectParameter(repositoryName, resolvedBy)));
                    mergeFragment.add(new Label("toBranch",
                            MessageFormat.format(getString("gb.toBranch"), "<b>" + ticket.mergeTo + "</b>"))
                                    .setEscapeModelStrings(false));
                    addUserAttributions(mergeFragment, entry, 0);
                    addDateAttributions(mergeFragment, entry);

                    item.add(mergeFragment);
                } else if (entry.isStatusChange()) {
                    /*
                     *  STATUS CHANGE
                     */
                    Fragment frag = new Fragment("entry", "statusFragment", TicketPage.this);
                    Label status = new Label("statusChange", entry.getStatus().toString());
                    String css = TicketsUI.getLozengeClass(entry.getStatus(), false);
                    WicketUtils.setCssClass(status, css);
                    frag.add(status);
                    addUserAttributions(frag, entry, avatarWidth);
                    addDateAttributions(frag, entry);
                    item.add(frag);
                } else {
                    /*
                     * COMMENT
                     */
                    String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName,
                            entry.comment.text);
                    String comment = MarkdownUtils.transformGFM(app().settings(), bugtraq, repositoryName);
                    String safeComment = app().xssFilter().relaxed(comment);
                    Fragment frag = new Fragment("entry", "commentFragment", TicketPage.this);
                    Label commentIcon = new Label("commentIcon");
                    if (entry.comment.src == CommentSource.Email) {
                        WicketUtils.setCssClass(commentIcon, "iconic-mail");
                    } else {
                        WicketUtils.setCssClass(commentIcon, "iconic-comment-alt2-stroke");
                    }
                    frag.add(commentIcon);
                    frag.add(new Label("comment", safeComment).setEscapeModelStrings(false));
                    addUserAttributions(frag, entry, avatarWidth);
                    addDateAttributions(frag, entry);
                    item.add(frag);
                }
            }
        };
        discussionFragment.add(discussionView);
        add(discussionFragment);
    }

    /*
     * ADD COMMENT PANEL
     */
    if (UserModel.ANONYMOUS.equals(user) || !repository.isBare || repository.isFrozen || repository.isMirror) {

        // prohibit comments for anonymous users, local working copy repos,
        // frozen repos, and mirrors
        add(new Label("newComment").setVisible(false));
    } else {
        // permit user to comment
        Fragment newComment = new Fragment("newComment", "newCommentFragment", TicketPage.this);
        AvatarImage img = new AvatarImage("newCommentAvatar", user.username, user.emailAddress,
                "gravatar-round", avatarWidth, true);
        newComment.add(img);
        CommentPanel commentPanel = new CommentPanel("commentPanel", user, ticket, null, TicketsPage.class);
        commentPanel.setRepository(repositoryName);
        newComment.add(commentPanel);
        add(newComment);
    }

    /*
     *  PATCHSET TAB
     */
    if (currentPatchset == null) {
        // no patchset available
        RepositoryUrl repoUrl = getRepositoryUrl(user, repository);
        boolean canPropose = repoUrl != null && repoUrl.hasPermission()
                && repoUrl.permission.atLeast(AccessPermission.CLONE) && !UserModel.ANONYMOUS.equals(user);
        if (ticket.isOpen() && app().tickets().isAcceptingNewPatchsets(repository) && canPropose) {
            // ticket & repo will accept a proposal patchset
            // show the instructions for proposing a patchset
            Fragment changeIdFrag = new Fragment("patchset", "proposeFragment", TicketPage.this);
            changeIdFrag.add(new Label("proposeInstructions",
                    MarkdownUtils.transformMarkdown(getString("gb.proposeInstructions")))
                            .setEscapeModelStrings(false));
            changeIdFrag
                    .add(new Label("ptWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Barnum")));
            changeIdFrag.add(new Label("ptWorkflowSteps",
                    getProposeWorkflow("propose_pt.md", repoUrl.url, ticket.number))
                            .setEscapeModelStrings(false));
            changeIdFrag
                    .add(new Label("gitWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Git")));
            changeIdFrag.add(new Label("gitWorkflowSteps",
                    getProposeWorkflow("propose_git.md", repoUrl.url, ticket.number))
                            .setEscapeModelStrings(false));
            add(changeIdFrag);
        } else {
            // explain why you can't propose a patchset
            Fragment fragment = new Fragment("patchset", "canNotProposeFragment", TicketPage.this);
            String reason = "";
            if (ticket.isClosed()) {
                reason = getString("gb.ticketIsClosed");
            } else if (repository.isMirror) {
                reason = getString("gb.repositoryIsMirror");
            } else if (repository.isFrozen) {
                reason = getString("gb.repositoryIsFrozen");
            } else if (!repository.acceptNewPatchsets) {
                reason = getString("gb.repositoryDoesNotAcceptPatchsets");
            } else if (!canPropose) {
                if (UserModel.ANONYMOUS.equals(user)) {
                    reason = getString("gb.anonymousCanNotPropose");
                } else {
                    reason = getString("gb.youDoNotHaveClonePermission");
                }
            } else {
                reason = getString("gb.serverDoesNotAcceptPatchsets");
            }
            fragment.add(new Label("reason", reason));
            add(fragment);
        }
    } else {
        // show current patchset
        Fragment patchsetFrag = new Fragment("patchset", "patchsetFragment", TicketPage.this);
        patchsetFrag.add(new Label("commitsInPatchset",
                MessageFormat.format(getString("gb.commitsInPatchsetN"), currentPatchset.number)));

        patchsetFrag.add(createMergePanel(user, repository));

        if (ticket.isOpen()) {
            // current revision
            MarkupContainer panel = createPatchsetPanel("panel", repository, user);
            patchsetFrag.add(panel);
            addUserAttributions(patchsetFrag, currentRevision, avatarWidth);
            addUserAttributions(panel, currentRevision, 0);
            addDateAttributions(panel, currentRevision);
        } else {
            // current revision
            patchsetFrag.add(new Label("panel").setVisible(false));
        }

        // commits
        List<RevCommit> commits = JGitUtils.getRevLog(getRepository(), currentPatchset.base,
                currentPatchset.tip);
        ListDataProvider<RevCommit> commitsDp = new ListDataProvider<RevCommit>(commits);
        DataView<RevCommit> commitsView = new DataView<RevCommit>("commit", commitsDp) {
            private static final long serialVersionUID = 1L;

            @Override
            public void populateItem(final Item<RevCommit> item) {
                RevCommit commit = item.getModelObject();
                PersonIdent author = commit.getAuthorIdent();
                item.add(new AvatarImage("authorAvatar", author.getName(), author.getEmailAddress(), null, 16,
                        false));
                item.add(new Label("author", commit.getAuthorIdent().getName()));
                item.add(new LinkPanel("commitId", null, getShortObjectId(commit.getName()), CommitPage.class,
                        WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
                item.add(new LinkPanel("diff", "link", getString("gb.diff"), CommitDiffPage.class,
                        WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
                item.add(new Label("title",
                        StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG_REFS)));
                item.add(WicketUtils.createDateLabel("commitDate", JGitUtils.getAuthorDate(commit),
                        GitBlitWebSession.get().getTimezone(), getTimeUtils(), false));
                item.add(new DiffStatPanel("commitDiffStat", 0, 0, true));
            }
        };
        patchsetFrag.add(commitsView);
        add(patchsetFrag);
    }

    /*
     * ACTIVITY TAB
     */
    Fragment revisionHistory = new Fragment("activity", "activityFragment", TicketPage.this);
    List<Change> events = new ArrayList<Change>(ticket.changes);
    Collections.sort(events);
    Collections.reverse(events);
    ListDataProvider<Change> eventsDp = new ListDataProvider<Change>(events);
    DataView<Change> eventsView = new DataView<Change>("event", eventsDp) {
        private static final long serialVersionUID = 1L;

        @Override
        public void populateItem(final Item<Change> item) {
            Change event = item.getModelObject();

            addUserAttributions(item, event, 16);

            if (event.hasPatchset()) {
                // patchset
                Patchset patchset = event.patchset;
                String what;
                if (event.isStatusChange() && (Status.New == event.getStatus())) {
                    what = getString("gb.proposedThisChange");
                } else if (patchset.rev == 1) {
                    what = MessageFormat.format(getString("gb.uploadedPatchsetN"), patchset.number);
                } else {
                    if (patchset.added == 1) {
                        what = getString("gb.addedOneCommit");
                    } else {
                        what = MessageFormat.format(getString("gb.addedNCommits"), patchset.added);
                    }
                }
                item.add(new Label("what", what));

                LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,
                        ComparePage.class, WicketUtils.newRangeParameter(repositoryName,
                                patchset.parent == null ? patchset.base : patchset.parent, patchset.tip),
                        true);
                WicketUtils.setHtmlTooltip(psr, patchset.toString());
                WicketUtils.setCssClass(psr, "aui-lozenge aui-lozenge-subtle");
                item.add(psr);
                String typeCss = getPatchsetTypeCss(patchset.type);
                Label typeLabel = new Label("patchsetType", patchset.type.toString());
                if (typeCss == null) {
                    typeLabel.setVisible(false);
                } else {
                    WicketUtils.setCssClass(typeLabel, typeCss);
                }
                item.add(typeLabel);

                Link<Void> deleteLink = createDeletePatchsetLink(repository, patchset);

                if (user.canDeleteRef(repository)) {
                    item.add(deleteLink.setVisible(patchset.canDelete));
                } else {
                    item.add(deleteLink.setVisible(false));
                }

                // show commit diffstat
                item.add(new DiffStatPanel("patchsetDiffStat", patchset.insertions, patchset.deletions,
                        patchset.rev > 1));
            } else if (event.hasComment()) {
                // comment
                item.add(new Label("what", getString("gb.commented")));
                item.add(new Label("patchsetRevision").setVisible(false));
                item.add(new Label("patchsetType").setVisible(false));
                item.add(new Label("deleteRevision").setVisible(false));
                item.add(new Label("patchsetDiffStat").setVisible(false));
            } else if (event.hasReference()) {
                // reference
                switch (event.reference.getSourceType()) {
                case Commit: {
                    final int shaLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);

                    item.add(new Label("what", getString("gb.referencedByCommit")));
                    LinkPanel psr = new LinkPanel("patchsetRevision", null,
                            event.reference.toString().substring(0, shaLen), CommitPage.class,
                            WicketUtils.newObjectParameter(repositoryName, event.reference.toString()), true);
                    WicketUtils.setHtmlTooltip(psr, event.reference.toString());
                    WicketUtils.setCssClass(psr, "ticketReference-commit shortsha1");
                    item.add(psr);

                }
                    break;

                case Ticket: {
                    final String text = MessageFormat.format("ticket/{0}", event.reference.ticketId);

                    item.add(new Label("what", getString("gb.referencedByTicket")));
                    //NOTE: Ideally reference the exact comment using reference.toString,
                    //      however anchor hash is used and is escaped resulting in broken link
                    LinkPanel psr = new LinkPanel("patchsetRevision", null, text, TicketsPage.class,
                            WicketUtils.newObjectParameter(repositoryName, event.reference.ticketId.toString()),
                            true);
                    WicketUtils.setCssClass(psr, "ticketReference-comment");
                    item.add(psr);
                }
                    break;

                default: {
                    item.add(new Label("what").setVisible(false));
                    item.add(new Label("patchsetRevision").setVisible(false));
                }
                }

                item.add(new Label("patchsetType").setVisible(false));
                item.add(new Label("deleteRevision").setVisible(false));
                item.add(new Label("patchsetDiffStat").setVisible(false));
            } else if (event.hasReview()) {
                // review
                String score;
                switch (event.review.score) {
                case approved:
                    score = "<span style='color:darkGreen'>" + getScoreDescription(event.review.score)
                            + "</span>";
                    break;
                case vetoed:
                    score = "<span style='color:darkRed'>" + getScoreDescription(event.review.score)
                            + "</span>";
                    break;
                default:
                    score = getScoreDescription(event.review.score);
                }
                item.add(new Label("what", MessageFormat.format(getString("gb.reviewedPatchsetRev"),
                        event.review.patchset, event.review.rev, score)).setEscapeModelStrings(false));
                item.add(new Label("patchsetRevision").setVisible(false));
                item.add(new Label("patchsetType").setVisible(false));
                item.add(new Label("deleteRevision").setVisible(false));
                item.add(new Label("patchsetDiffStat").setVisible(false));
            } else {
                // field change
                item.add(new Label("patchsetRevision").setVisible(false));
                item.add(new Label("patchsetType").setVisible(false));
                item.add(new Label("deleteRevision").setVisible(false));
                item.add(new Label("patchsetDiffStat").setVisible(false));

                String what = "";
                if (event.isStatusChange()) {
                    switch (event.getStatus()) {
                    case New:
                        if (ticket.isProposal()) {
                            what = getString("gb.proposedThisChange");
                        } else {
                            what = getString("gb.createdThisTicket");
                        }
                        break;
                    default:
                        break;
                    }
                }
                item.add(new Label("what", what).setVisible(what.length() > 0));
            }

            addDateAttributions(item, event);

            if (event.hasFieldChanges()) {
                StringBuilder sb = new StringBuilder();
                sb.append("<table class=\"summary\"><tbody>");
                for (Map.Entry<Field, String> entry : event.fields.entrySet()) {
                    String value;
                    switch (entry.getKey()) {
                    case body:
                        String body = entry.getValue();
                        if (event.isStatusChange() && Status.New == event.getStatus()
                                && StringUtils.isEmpty(body)) {
                            // ignore initial empty description
                            continue;
                        }
                        // trim body changes
                        if (StringUtils.isEmpty(body)) {
                            value = "<i>" + ESC_NIL + "</i>";
                        } else {
                            value = StringUtils.trimString(body, Constants.LEN_SHORTLOG_REFS);
                        }
                        break;
                    case status:
                        // special handling for status
                        Status status = event.getStatus();
                        String css = TicketsUI.getLozengeClass(status, true);
                        value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
                        break;
                    default:
                        value = StringUtils.isEmpty(entry.getValue()) ? ("<i>" + ESC_NIL + "</i>")
                                : StringUtils.escapeForHtml(entry.getValue(), false);
                        break;
                    }
                    sb.append("<tr><th style=\"width:70px;\">");
                    try {
                        sb.append(getString("gb." + entry.getKey().name()));
                    } catch (Exception e) {
                        sb.append(entry.getKey().name());
                    }
                    sb.append("</th><td>");
                    sb.append(value);
                    sb.append("</td></tr>");
                }
                sb.append("</tbody></table>");
                String safeHtml = app().xssFilter().relaxed(sb.toString());
                item.add(new Label("fields", safeHtml).setEscapeModelStrings(false));
            } else {
                item.add(new Label("fields").setVisible(false));
            }
        }
    };
    revisionHistory.add(eventsView);
    add(revisionHistory);
}