James Moger
2013-06-18 ff7d3cffc7af1f24a1db8d42758943cc05bcbaa0
Reflogs, Digests, and Dashboards

Renamed pushlog to reflog to better match it's current and future purpose.
Split PushesPanel into ReflogPanel and DigestsPanel.
Overhauled project pages and gave them a coherent purpose from the dashboard.
4 files added
1 files copied
5 files renamed
19 files modified
2 files deleted
2495 ■■■■ changed files
src/main/distrib/data/gitblit.properties 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/ReceiveHook.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/DailyLogEntry.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/RefLogEntry.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/RefLogUtils.java 163 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DashboardPage.html 100 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DashboardPage.java 249 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html 158 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java 311 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/OverviewPage.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/OverviewPage.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.html 114 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.java 189 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectsPage.html 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java 109 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ReflogPage.html 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ReflogPage.java 22 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DigestsPanel.html 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DigestsPanel.java 273 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/PushesPanel.java 381 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ReflogPanel.html 10 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ReflogPanel.java 305 ●●●●● patch | view | raw | blame | history
src/main/resources/gitblit.css 22 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/GitServletTest.java 6 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/PushLogTest.java 6 ●●●● patch | view | raw | blame | history
src/main/distrib/data/gitblit.properties
@@ -853,17 +853,17 @@
# SINCE 0.5.0
web.itemsPerPage = 50
# The number of pushes to display on the overview page
# The number of reflog changes to display on the overview page
# Value must exceed 0 else default of 5 is used
#
# SINCE 1.3.0
web.overviewPushCount = 5
web.overviewReflogCount = 5
# The number of pushes to show on a push page before show the first, prev, next
# pagination links.  A default of 10 is used for any invalid value.
# The number of reflog changes to show on a reflog page before show the first,
#  prev, next pagination links.  A default of 10 is used for any invalid value.
#
# SINCE 1.3.0
web.pushesPerPage = 10
web.reflogChangesPerPage = 10
# Registered file extensions to ignore during Lucene indexing
#
src/main/java/com/gitblit/git/ReceiveHook.java
@@ -45,7 +45,7 @@
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ClientLogger;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
/**
@@ -256,7 +256,7 @@
        // update push log
        try {
            PushLogUtils.updatePushLog(user, rp.getRepository(), commands);
            RefLogUtils.updateRefLog(user, rp.getRepository(), commands);
            logger.debug(MessageFormat.format("{0} push log updated", repository.name));
        } catch (Exception e) {
            logger.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
src/main/java/com/gitblit/models/DailyLogEntry.java
@@ -28,7 +28,7 @@
 * 
 * @author James Moger
 */
public class DailyLogEntry extends PushLogEntry implements Serializable {
public class DailyLogEntry extends RefLogEntry implements Serializable {
    private static final long serialVersionUID = 1L;
src/main/java/com/gitblit/models/RefLogEntry.java
File was renamed from src/main/java/com/gitblit/models/PushLogEntry.java
@@ -39,7 +39,7 @@
 * 
 * @author James Moger
 */
public class PushLogEntry implements Serializable, Comparable<PushLogEntry> {
public class RefLogEntry implements Serializable, Comparable<RefLogEntry> {
    private static final long serialVersionUID = 1L;
@@ -67,7 +67,7 @@
     * @param user
     *            the user who pushed
     */
    public PushLogEntry(String repository, Date date, UserModel user) {
    public RefLogEntry(String repository, Date date, UserModel user) {
        this.repository = repository;
        this.date = date;
        this.user = user;
@@ -317,7 +317,7 @@
    }
    @Override
    public int compareTo(PushLogEntry o) {
    public int compareTo(RefLogEntry o) {
        // reverse chronological order
        return o.date.compareTo(date);
    }
src/main/java/com/gitblit/utils/RefLogUtils.java
File was renamed from src/main/java/com/gitblit/utils/PushLogUtils.java
@@ -41,6 +41,7 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
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;
@@ -55,23 +56,23 @@
import com.gitblit.Constants;
import com.gitblit.models.DailyLogEntry;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.PushLogEntry;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.UserModel;
/**
 * Utility class for maintaining a pushlog within a git repository on an
 * Utility class for maintaining a reflog within a git repository on an
 * orphan branch.
 * 
 * @author James Moger
 *
 */
public class PushLogUtils {
public class RefLogUtils {
    
    public static final String GB_PUSHES = "refs/gitblit/pushes";
    private static final String GB_REFLOG = "refs/gitblit/reflog";
    static final Logger LOGGER = LoggerFactory.getLogger(PushLogUtils.class);
    private static final Logger LOGGER = LoggerFactory.getLogger(RefLogUtils.class);
    /**
     * Log an error message and exception.
@@ -97,17 +98,39 @@
    }
    /**
     * Returns a RefModel for the gb-pushes branch in the repository. If the
     * Returns a RefModel for the reflog branch in the repository. If the
     * branch can not be found, null is returned.
     * 
     * @param repository
     * @return a refmodel for the gb-pushes branch or null
     * @return a refmodel for the reflog branch or null
     */
    public static RefModel getPushLogBranch(Repository repository) {
    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";
        for (RefModel ref : refs) {
            if (ref.reference.getName().equals(GB_PUSHES)) {
            if (ref.reference.getName().equals(GB_REFLOG)) {
                return ref;
            } else if (ref.reference.getName().equals(GB_PUSHES)) {
                pushLog = ref;
            }
        }
        if (pushLog != null) {
            // rename refs/gitblit/pushes to refs/gitblit/reflog
            RefRename cmd;
            try {
                cmd = repository.renameRef(GB_PUSHES, GB_REFLOG);
                cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
                cmd.setRefLogMessage("renamed " + GB_PUSHES + " => " + GB_REFLOG);
                Result res = cmd.rename();
                switch (res) {
                case RENAMED:
                    return getRefLogBranch(repository);
                default:
                    LOGGER.error("failed to rename " + GB_PUSHES + " => " + GB_REFLOG + " (" + res.name() + ")");
                }
            } catch (IOException e) {
                LOGGER.error("failed to rename pushlog", e);
            }
        }
        return null;
@@ -133,25 +156,25 @@
    }
    
    /**
     * Updates a push log.
     * Updates the reflog with the received commands.
     * 
     * @param user
     * @param repository
     * @param commands
     * @return true, if the update was successful
     */
    public static boolean updatePushLog(UserModel user, Repository repository,
    public static boolean updateRefLog(UserModel user, Repository repository,
            Collection<ReceiveCommand> commands) {
        RefModel pushlogBranch = getPushLogBranch(repository);
        if (pushlogBranch == null) {
            JGitUtils.createOrphanBranch(repository, GB_PUSHES, null);
        RefModel reflogBranch = getRefLogBranch(repository);
        if (reflogBranch == null) {
            JGitUtils.createOrphanBranch(repository, GB_REFLOG, null);
        }
        
        boolean success = false;
        String message = "push";
        
        try {
            ObjectId headId = repository.resolve(GB_PUSHES + "^{commit}");
            ObjectId headId = repository.resolve(GB_REFLOG + "^{commit}");
            ObjectInserter odi = repository.newObjectInserter();
            try {
                // Create the in-memory index of the push log entry
@@ -184,7 +207,7 @@
                RevWalk revWalk = new RevWalk(repository);
                try {
                    RevCommit revCommit = revWalk.parseCommit(commitId);
                    RefUpdate ru = repository.updateRef(GB_PUSHES);
                    RefUpdate ru = repository.updateRef(GB_REFLOG);
                    ru.setNewObjectId(commitId);
                    ru.setExpectedOldObjectId(headId);
                    ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
@@ -201,7 +224,7 @@
                                ru.getRef(), rc);
                    default:
                        throw new JGitInternalException(MessageFormat.format(
                                JGitText.get().updatingRefFailed, GB_PUSHES, commitId.toString(),
                                JGitText.get().updatingRefFailed, GB_REFLOG, commitId.toString(),
                                rc));
                    }
                } finally {
@@ -211,7 +234,7 @@
                odi.release();
            }
        } catch (Throwable t) {
            error(t, repository, "Failed to commit pushlog entry to {0}");
            error(t, repository, "Failed to commit reflog entry to {0}");
        }
        return success;
    }
@@ -316,25 +339,25 @@
        return inCoreIndex;
    }
    
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository) {
        return getPushLog(repositoryName, repository, null, 0, -1);
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository) {
        return getRefLog(repositoryName, repository, null, 0, -1);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int maxCount) {
        return getPushLog(repositoryName, repository, null, 0, maxCount);
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, int maxCount) {
        return getRefLog(repositoryName, repository, null, 0, maxCount);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int offset, int maxCount) {
        return getPushLog(repositoryName, repository, null, offset, maxCount);
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, int offset, int maxCount) {
        return getRefLog(repositoryName, repository, null, offset, maxCount);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate) {
        return getPushLog(repositoryName, repository, minimumDate, 0, -1);
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, Date minimumDate) {
        return getRefLog(repositoryName, repository, minimumDate, 0, -1);
    }
    
    /**
     * Returns the list of push log entries as they were recorded by Gitblit.
     * Each PushLogEntry may represent multiple ref updates.
     * Returns the list of reflog entries as they were recorded by Gitblit.
     * Each RefLogEntry may represent multiple ref updates.
     * 
     * @param repositoryName
     * @param repository
@@ -344,10 +367,10 @@
     *             if < 0, all pushes are returned.
     * @return a list of push log entries
     */
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository,
    public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository,
            Date minimumDate, int offset, int maxCount) {
        List<PushLogEntry> list = new ArrayList<PushLogEntry>();
        RefModel ref = getPushLogBranch(repository);
        List<RefLogEntry> list = new ArrayList<RefLogEntry>();
        RefModel ref = getRefLogBranch(repository);
        if (ref == null) {
            return list;
        }
@@ -358,9 +381,9 @@
        Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
        List<RevCommit> pushes;
        if (minimumDate == null) {
            pushes = JGitUtils.getRevLog(repository, GB_PUSHES, offset, maxCount);
            pushes = JGitUtils.getRevLog(repository, GB_REFLOG, offset, maxCount);
        } else {
            pushes = JGitUtils.getRevLog(repository, GB_PUSHES, minimumDate);
            pushes = JGitUtils.getRevLog(repository, GB_REFLOG, minimumDate);
        }
        for (RevCommit push : pushes) {
            if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
@@ -371,7 +394,7 @@
            UserModel user = newUserModelFrom(push.getAuthorIdent());
            Date date = push.getAuthorIdent().getWhen();
            
            PushLogEntry log = new PushLogEntry(repositoryName, date, user);
            RefLogEntry log = new RefLogEntry(repositoryName, date, user);
            list.add(log);
            List<PathChangeModel> changedRefs = JGitUtils.getFilesInCommit(repository, push);
            for (PathChangeModel change : changedRefs) {
@@ -410,8 +433,8 @@
     * @param maxCount
     * @return a list of push log entries separated by ref
     */
    public static List<PushLogEntry> getPushLogByRef(String repositoryName, Repository repository, int maxCount) {
        return getPushLogByRef(repositoryName, repository, 0, maxCount);
    public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository, int maxCount) {
        return getLogByRef(repositoryName, repository, 0, maxCount);
    }
    
    /**
@@ -424,65 +447,65 @@
     * @param maxCount
     * @return a list of push log entries separated by ref
     */
    public static List<PushLogEntry> getPushLogByRef(String repositoryName, Repository repository,  int offset,
    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
        Map<String, List<PushLogEntry>> refMap = new HashMap<String, List<PushLogEntry>>();
        List<PushLogEntry> pushes = getPushLog(repositoryName, repository, offset, maxCount);
        for (PushLogEntry push : pushes) {
            for (String ref : push.getChangedRefs()) {
        Map<String, List<RefLogEntry>> refMap = new HashMap<String, List<RefLogEntry>>();
        List<RefLogEntry> refLog = getRefLog(repositoryName, repository, offset, maxCount);
        for (RefLogEntry entry : refLog) {
            for (String ref : entry.getChangedRefs()) {
                if (!refMap.containsKey(ref)) {
                    refMap.put(ref, new ArrayList<PushLogEntry>());
                    refMap.put(ref, new ArrayList<RefLogEntry>());
                }
                
                // construct new ref-specific push log entry
                PushLogEntry refPush;
                if (push instanceof DailyLogEntry) {
                // construct new ref-specific ref change entry
                RefLogEntry refChange;
                if (entry instanceof DailyLogEntry) {
                    // simulated push log from commits grouped by date
                    refPush = new DailyLogEntry(push.repository, push.date);
                    refChange = new DailyLogEntry(entry.repository, entry.date);
                } else {
                    // real push log entry
                    refPush = new PushLogEntry(push.repository, push.date, push.user);
                    refChange = new RefLogEntry(entry.repository, entry.date, entry.user);
                }
                refPush.updateRef(ref, push.getChangeType(ref), push.getOldId(ref), push.getNewId(ref));
                refPush.addCommits(push.getCommits(ref));
                refMap.get(ref).add(refPush);
                refChange.updateRef(ref, entry.getChangeType(ref), entry.getOldId(ref), entry.getNewId(ref));
                refChange.addCommits(entry.getCommits(ref));
                refMap.get(ref).add(refChange);
            }
        }
        
        // merge individual ref pushes into master list
        List<PushLogEntry> refPushLog = new ArrayList<PushLogEntry>();
        for (List<PushLogEntry> refPush : refMap.values()) {
            refPushLog.addAll(refPush);
        // merge individual ref changes into master list
        List<RefLogEntry> mergedRefLog = new ArrayList<RefLogEntry>();
        for (List<RefLogEntry> refPush : refMap.values()) {
            mergedRefLog.addAll(refPush);
        }
        
        // sort ref push log
        Collections.sort(refPushLog);
        // sort ref log
        Collections.sort(mergedRefLog);
        
        return refPushLog;
        return mergedRefLog;
    }
    
    /**
     * Returns the list of pushes separated by ref (e.g. each ref has it's own
     * PushLogEntry object).
     * Returns the list of ref changes separated by ref (e.g. each ref has it's own
     * RefLogEntry object).
     *  
     * @param repositoryName
     * @param repository
     * @param minimumDate
     * @return a list of push log entries separated by ref
     * @return a list of ref log entries separated by ref
     */
    public static List<PushLogEntry> getPushLogByRef(String repositoryName, Repository repository,  Date minimumDate) {
    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
        Map<String, List<PushLogEntry>> refMap = new HashMap<String, List<PushLogEntry>>();
        List<PushLogEntry> pushes = getPushLog(repositoryName, repository, minimumDate);
        for (PushLogEntry push : pushes) {
        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()) {
                if (!refMap.containsKey(ref)) {
                    refMap.put(ref, new ArrayList<PushLogEntry>());
                    refMap.put(ref, new ArrayList<RefLogEntry>());
                }
                // construct new ref-specific push log entry
                PushLogEntry refPush = new PushLogEntry(push.repository, push.date, push.user);
                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));
                refMap.get(ref).add(refPush);
@@ -490,8 +513,8 @@
        }
        
        // merge individual ref pushes into master list
        List<PushLogEntry> refPushLog = new ArrayList<PushLogEntry>();
        for (List<PushLogEntry> refPush : refMap.values()) {
        List<RefLogEntry> refPushLog = new ArrayList<RefLogEntry>();
        for (List<RefLogEntry> refPush : refMap.values()) {
            refPushLog.addAll(refPush);
        }
        
@@ -562,7 +585,7 @@
                                    Date tagDate = commit.getAuthorIdent().getWhen();
                                    tags.put(dateStr, new DailyLogEntry(repositoryName, tagDate, tagUser));
                                }
                                PushLogEntry tagEntry = tags.get(dateStr);
                                RefLogEntry tagEntry = tags.get(dateStr);
                                tagEntry.updateRef(ref.getName(), ReceiveCommand.Type.CREATE);
                                tagEntry.addCommit(ref.getName(), commit);
                            } else if (ref.getName().startsWith(Constants.R_PULL)) {
@@ -572,7 +595,7 @@
                                    Date commitDate = commit.getAuthorIdent().getWhen();
                                    pulls.put(dateStr, new DailyLogEntry(repositoryName, commitDate, commitUser));
                                }
                                PushLogEntry pullEntry = pulls.get(dateStr);
                                RefLogEntry pullEntry = pulls.get(dateStr);
                                pullEntry.updateRef(ref.getName(), ReceiveCommand.Type.CREATE);
                                pullEntry.addCommit(ref.getName(), commit);
                            }
src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -44,18 +44,18 @@
import com.gitblit.wicket.pages.GitSearchPage;
import com.gitblit.wicket.pages.GravatarProfilePage;
import com.gitblit.wicket.pages.HistoryPage;
import com.gitblit.wicket.pages.DashboardPage;
import com.gitblit.wicket.pages.LogPage;
import com.gitblit.wicket.pages.LogoutPage;
import com.gitblit.wicket.pages.LuceneSearchPage;
import com.gitblit.wicket.pages.MarkdownPage;
import com.gitblit.wicket.pages.MetricsPage;
import com.gitblit.wicket.pages.MyDashboardPage;
import com.gitblit.wicket.pages.OverviewPage;
import com.gitblit.wicket.pages.PatchPage;
import com.gitblit.wicket.pages.ProjectPage;
import com.gitblit.wicket.pages.ProjectsPage;
import com.gitblit.wicket.pages.PushesPage;
import com.gitblit.wicket.pages.RawPage;
import com.gitblit.wicket.pages.ReflogPage;
import com.gitblit.wicket.pages.RepositoriesPage;
import com.gitblit.wicket.pages.ReviewProposalPage;
import com.gitblit.wicket.pages.SummaryPage;
@@ -69,7 +69,7 @@
public class GitBlitWebApp extends WebApplication {
    public final static Class<? extends BasePage> HOME_PAGE_CLASS = DashboardPage.class;
    public final static Class<? extends BasePage> HOME_PAGE_CLASS = MyDashboardPage.class;
    
    @Override
    public void init() {
@@ -98,7 +98,7 @@
        mount("/repositories", RepositoriesPage.class);
        mount("/overview", OverviewPage.class, "r", "h");
        mount("/summary", SummaryPage.class, "r");
        mount("/pushes", PushesPage.class, "r", "h");
        mount("/reflog", ReflogPage.class, "r", "h");
        mount("/commits", LogPage.class, "r", "h");
        mount("/log", LogPage.class, "r", "h");
        mount("/tags", TagsPage.class, "r");
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -462,8 +462,7 @@
gb.at = at
gb.of = of
gb.in = in
gb.morePushes = all pushes...
gb.pushes = pushes
gb.moreChanges = all changes...
gb.pushedNCommitsTo = pushed {0} commits to
gb.pushedOneCommitTo = pushed 1 commit to
gb.commitsTo = {0} commits to
@@ -488,8 +487,12 @@
gb.starredRepositories = starred repositories
gb.failedToUpdateUser = Failed to update user account!
gb.myRepositories = my repositories
gb.noActivity = there has been no recent commit activity
gb.noActivity = there has been no activity in the last {0} days
gb.findSomeRepositories = find some repositories
gb.metricAuthorExclusions = author metric exclusions
gb.myDashboard = my dashboard
gb.failedToFindAccount = failed to find user account ''{0}''
gb.failedToFindAccount = failed to find user account ''{0}''
gb.reflog = reflog
gb.active = active
gb.starred = starred
gb.owned = owned
src/main/java/com/gitblit/wicket/pages/DashboardPage.html
File was deleted
src/main/java/com/gitblit/wicket/pages/DashboardPage.java
@@ -15,49 +15,40 @@
 */
package com.gitblit.wicket.pages;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.HeaderContributor;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.DailyLogEntry;
import com.gitblit.models.Metric;
import com.gitblit.models.PushLogEntry;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
@@ -66,19 +57,17 @@
import com.gitblit.wicket.charting.GoogleCharts;
import com.gitblit.wicket.charting.GooglePieChart;
import com.gitblit.wicket.ng.NgController;
import com.gitblit.wicket.panels.DigestsPanel;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.PushesPanel;
public class DashboardPage extends RootPage {
public abstract class DashboardPage extends RootPage {
    public DashboardPage() {
        super();
        setup(null);
    }
    public DashboardPage(PageParameters params) {
        super(params);
        setup(params);
    }
    @Override
@@ -86,115 +75,50 @@
        return true;
    }
    private void setup(PageParameters params) {
        setupPage("", "");
        // check to see if we should display a login message
        boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
        if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
            String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit");
            String message = readMarkdown(messageSource, "login.mkd");
            Component repositoriesMessage = new Label("repositoriesMessage", message);
            add(repositoriesMessage.setEscapeModelStrings(false));
            add(new Label("digests"));
            add(new Label("active").setVisible(false));
            add(new Label("starred").setVisible(false));
            add(new Label("owned").setVisible(false));
            add(new Label("feedheader").setVisible(false));
            return;
        }
        // Load the markdown welcome message
        String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit");
        String message = readMarkdown(messageSource, "welcome.mkd");
        Component repositoriesMessage = new Label("repositoriesMessage", message)
                .setEscapeModelStrings(false).setVisible(message.length() > 0);
        add(repositoriesMessage);
        UserModel user = GitBlitWebSession.get().getUser();
        if (user == null) {
            user = UserModel.ANONYMOUS;
        }
        Comparator<RepositoryModel> lastUpdateSort = new Comparator<RepositoryModel>() {
            @Override
            public int compare(RepositoryModel o1, RepositoryModel o2) {
                return o2.lastChange.compareTo(o1.lastChange);
            }
        };
        // parameters
        int daysBack = params == null ? 0 : WicketUtils.getDaysBack(params);
        if (daysBack < 1) {
            daysBack = 7;
        }
    protected void addActivity(UserModel user, Collection<RepositoryModel> repositories, int daysBack) {
        Calendar c = Calendar.getInstance();
        c.add(Calendar.DATE, -1*daysBack);
        Date minimumDate = c.getTime();
        TimeZone timezone = getTimeZone();
        
        // build repo lists
        List<RepositoryModel> starred = new ArrayList<RepositoryModel>();
        List<RepositoryModel> owned = new ArrayList<RepositoryModel>();
        List<RepositoryModel> active = new ArrayList<RepositoryModel>();
        for (RepositoryModel model : getRepositoryModels()) {
            if (model.isUsersPersonalRepository(user.username) || model.isOwner(user.username)) {
                owned.add(model);
            }
            if (user.getPreferences().isStarredRepository(model.name)) {
                starred.add(model);
            }
            if (model.isShowActivity() && model.lastChange.after(minimumDate)) {
                active.add(model);
            }
        }
        Collections.sort(owned, lastUpdateSort);
        Collections.sort(starred, lastUpdateSort);
        Collections.sort(active, lastUpdateSort);
        Set<RepositoryModel> feedSources = new HashSet<RepositoryModel>();
        feedSources.addAll(starred);
        if (feedSources.isEmpty()) {
            feedSources.addAll(active);
        }
        // create daily commit digest feed
        List<PushLogEntry> pushes = new ArrayList<PushLogEntry>();
        for (RepositoryModel model : feedSources) {
        List<DailyLogEntry> digests = new ArrayList<DailyLogEntry>();
        for (RepositoryModel model : repositories) {
            Repository repository = GitBlit.self().getRepository(model.name);
            List<DailyLogEntry> entries = PushLogUtils.getDailyLogByRef(model.name, repository, minimumDate, timezone);
            pushes.addAll(entries);
            List<DailyLogEntry> entries = RefLogUtils.getDailyLogByRef(model.name, repository, minimumDate, timezone);
            digests.addAll(entries);
            repository.close();
        }
        
        if (pushes.size() == 0) {
        Fragment activityFragment = new Fragment("activity", "activityFragment", this);
        add(activityFragment);
        if (digests.size() == 0) {
            // quiet or no starred repositories
            if (feedSources.size() == 0) {
            if (repositories.size() == 0) {
                if (UserModel.ANONYMOUS.equals(user)) {
                    add(new Label("digests", getString("gb.noActivity")));
                    activityFragment.add(new Label("digests", MessageFormat.format(getString("gb.noActivity"), daysBack)));
                } else {
                    add(new LinkPanel("digests", null, getString("gb.findSomeRepositories"), RepositoriesPage.class));
                    activityFragment.add(new LinkPanel("digests", null, getString("gb.findSomeRepositories"), RepositoriesPage.class));
                }
            } else {
                add(new Label("digests", getString("gb.noActivity")));
                activityFragment.add(new Label("digests", MessageFormat.format(getString("gb.noActivity"), daysBack)));
            }
        } else {
            // show daily commit digest feed
            Collections.sort(pushes);
            add(new PushesPanel("digests", pushes));
            Collections.sort(digests);
            DigestsPanel digestsPanel = new DigestsPanel("digests", digests);
            WicketUtils.setCssStyle(digestsPanel,  "margin-top:-20px");
            activityFragment.add(digestsPanel);
        }
        
        // add the nifty charts
        if (!ArrayUtils.isEmpty(pushes)) {
        if (!ArrayUtils.isEmpty(digests)) {
            // aggregate author exclusions
            Set<String> authorExclusions = new TreeSet<String>();
            for (String author : GitBlit.getStrings(Keys.web.metricAuthorExclusions)) {
                authorExclusions.add(author.toLowerCase());
            }
            for (RepositoryModel model : feedSources) {
            for (RepositoryModel model : repositories) {
                if (!ArrayUtils.isEmpty(model.metricAuthorExclusions)) {
                    for (String author : model.metricAuthorExclusions) {
                        authorExclusions.add(author.toLowerCase());
@@ -202,40 +126,10 @@
                }
            }
            addCharts(pushes, authorExclusions, daysBack);
            addCharts(activityFragment, digests, authorExclusions, daysBack);
        } else {
            add(new Label("feedheader").setVisible(false));
        }
        // active repository list
        if (starred.isEmpty()) {
            Fragment activeView = createNgList("active", "activeListFragment", "activeCtrl", active);
            add(activeView);
        } else {
            add(new Label("active").setVisible(false));
        }
        // starred repository list
        if (ArrayUtils.isEmpty(starred)) {
            add(new Label("starred").setVisible(false));
        } else {
            Fragment starredView = createNgList("starred", "starredListFragment", "starredCtrl", starred);
            add(starredView);
        }
        // owned repository list
        if (ArrayUtils.isEmpty(owned)) {
            add(new Label("owned").setVisible(false));
        } else {
            Fragment ownedView = createNgList("owned", "ownedListFragment", "ownedCtrl", owned);
            if (user.canCreate) {
                // create button
                ownedView.add(new LinkPanel("create", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class));
            } else {
                // no button
                ownedView.add(new Label("create").setVisible(false));
            }
            add(ownedView);
            activityFragment.add(new Label("charts").setVisible(false));
            activityFragment.add(new Label("feedheader").setVisible(false));
        }
    }
    
@@ -259,6 +153,7 @@
            item.n = name;
            item.p = path;
            item.r = repo.name;
            item.i = repo.description;
            item.s = GitBlit.self().getStarCount(repo);
            item.t = getTimeUtils().timeAgo(repo.lastChange);
            item.d = df.format(repo.lastChange);
@@ -295,104 +190,32 @@
        pages.add(menu);
    }
    private String readMarkdown(String messageSource, String resource) {
        String message = "";
        if (messageSource.equalsIgnoreCase("gitblit")) {
            // Read default message
            message = readDefaultMarkdown(resource);
        } else {
            // Read user-supplied message
            if (!StringUtils.isEmpty(messageSource)) {
                File file = GitBlit.getFileOrFolder(messageSource);
                if (file.exists()) {
                    try {
                        FileInputStream fis = new FileInputStream(file);
                        InputStreamReader reader = new InputStreamReader(fis,
                                Constants.CHARACTER_ENCODING);
                        message = MarkdownUtils.transformMarkdown(reader);
                        reader.close();
                    } catch (Throwable t) {
                        message = getString("gb.failedToRead") + " " + file;
                        warn(message, t);
                    }
                } else {
                    message = messageSource + " " + getString("gb.isNotValidFile");
                }
            }
        }
        return message;
    }
    private String readDefaultMarkdown(String file) {
        String base = file.substring(0, file.lastIndexOf('.'));
        String ext = file.substring(file.lastIndexOf('.'));
        String lc = getLanguageCode();
        String cc = getCountryCode();
        // try to read file_en-us.ext, file_en.ext, file.ext
        List<String> files = new ArrayList<String>();
        if (!StringUtils.isEmpty(lc)) {
            if (!StringUtils.isEmpty(cc)) {
                files.add(base + "_" + lc + "-" + cc + ext);
                files.add(base + "_" + lc + "_" + cc + ext);
            }
            files.add(base + "_" + lc + ext);
        }
        files.add(file);
        for (String name : files) {
            String message;
            InputStreamReader reader = null;
            try {
                InputStream is = getClass().getResourceAsStream("/" + name);
                if (is == null) {
                    continue;
                }
                reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                return message;
            } catch (Throwable t) {
                message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
                error(message, t, false);
                return message;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
            }
        }
        return MessageFormat.format(getString("gb.failedToReadMessage"), file);
    }
    /**
     * Creates the daily activity line chart, the active repositories pie chart,
     * and the active authors pie chart
     * 
     * @param recentPushes
     * @param recentChanges
     * @param authorExclusions
     * @param daysBack
     */
    private void addCharts(List<PushLogEntry> recentPushes, Set<String> authorExclusions, int daysBack) {
    protected void addCharts(Fragment frag, List<DailyLogEntry> recentChanges, Set<String> authorExclusions, int daysBack) {
        // activity metrics
        Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
        Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
        // aggregate repository and author metrics
        int totalCommits = 0;
        for (PushLogEntry push : recentPushes) {
        for (RefLogEntry change : recentChanges) {
            // aggregate repository metrics
            String repository = StringUtils.stripDotGit(push.repository);
            String repository = StringUtils.stripDotGit(change.repository);
            if (!repositoryMetrics.containsKey(repository)) {
                repositoryMetrics.put(repository, new Metric(repository));
            }
            repositoryMetrics.get(repository).count += 1;
            
            for (RepositoryCommit commit : push.getCommits()) {
            for (RepositoryCommit commit : change.getCommits()) {
                totalCommits++;
                String author = StringUtils.removeNewlines(commit.getAuthorIdent().getName());
                String authorName = author.toLowerCase();
@@ -406,7 +229,7 @@
            }
        }
        
        add(new Label("feedheader", MessageFormat.format(getString("gb.recentActivityStats"),
        frag.add(new Label("feedheader", MessageFormat.format(getString("gb.recentActivityStats"),
                daysBack, totalCommits, authorMetrics.size())));
        // build google charts
@@ -430,10 +253,11 @@
        chart.setShowLegend(false);
        charts.addChart(chart);
        add(new HeaderContributor(charts));
        add(new HeaderContributor(charts));
        frag.add(new Fragment("charts", "chartsFragment", this));
    }
    
    class RepoListItem implements Serializable {
    protected class RepoListItem implements Serializable {
        private static final long serialVersionUID = 1L;
        
@@ -442,6 +266,7 @@
        String p; // project/path
        String t; // time ago
        String d; // last updated
        String i; // information/description
        long s; // stars
        String c; // html color
        int wc; // working copy, 1 = true
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html
New file
@@ -0,0 +1,158 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:extend>
<div class="container">
    <div class="row" style="padding-top:5px;">
        <div class="span7">
            <div class="hidden-phone markdown" style="padding-bottom: 30px;" wicket:id="repositoriesMessage">[repositories message]</div>
            <div wicket:id="activity"></div>
        </div>
        <div class="span5">
            <div wicket:id="repositoryTabs"></div>
        </div>
    </div>
</div>
<wicket:fragment wicket:id="anonymousTabsFragment">
    <ul class="nav nav-pills">
        <li class="active"><a href="#recent" data-toggle="tab"><wicket:message key="gb.active">[active]</wicket:message></a></li>
        <li><a href="#projects" data-toggle="tab"><wicket:message key="gb.projects">[projects]</wicket:message></a></li>
    </ul>
    <div class="tab-content">
        <div class="tab-pane active" id="recent">
            <div wicket:id="active">[recently active]</div>
        </div>
        <div class="tab-pane" id="projects">
            <div wicket:id="projectList">[all projects]</div>
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="authenticatedTabsFragment">
    <ul class="nav nav-pills">
        <li class="active"><a href="#starred" data-toggle="tab"><wicket:message key="gb.starred">[starred]</wicket:message></a></li>
        <li><a href="#owned" data-toggle="tab"><wicket:message key="gb.owned">[owned]</wicket:message></a></li>
        <li><a href="#recent" data-toggle="tab"><wicket:message key="gb.active">[active]</wicket:message></a></li>
        <li><a href="#projects" data-toggle="tab"><wicket:message key="gb.projects">[projects]</wicket:message></a></li>
    </ul>
    <div class="tab-content">
        <div class="tab-pane active" id="starred">
            <div wicket:id="starred">[starred repositories]</div>
        </div>
        <div class="tab-pane" id="owned">
            <div wicket:id="owned">[my repositories]</div>
        </div>
        <div class="tab-pane" id="recent">
            <div wicket:id="active">[recently active]</div>
        </div>
        <div class="tab-pane" id="projects">
            <div wicket:id="projectList">[all projects]</div>
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="activityFragment">
    <div class="dashboardTitle"><wicket:message key="gb.recentActivity"></wicket:message> <small><span wicket:id="feedheader"></span></small></div>
    <div class="hidden-phone hidden-tablet"  style="text-align:center;">
        <div wicket:id="charts"></div>
    </div>
    <div wicket:id="digests"></div>
</wicket:fragment>
<wicket:fragment wicket:id="chartsFragment">
    <table>
        <tr>
            <td><div id="chartRepositories" style="display:inline-block;width: 175px; height:175px"></div></td>
            <td><div id="chartAuthors" style="display:inline-block;width: 175px; height: 175px;"></div></td>
        </tr>
    </table>
</wicket:fragment>
<wicket:fragment wicket:id="starredListFragment">
    <div ng-controller="starredCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
        <div class="header" style="padding: 5px;border: none;"><i class="icon-star"></i> <wicket:message key="gb.starredRepositories"></wicket:message> ({{starred.length}})
            <div style="padding: 5px 0px 0px;">
                <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
            </div>
        </div>
        <div ng-repeat="item in starred | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
            <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
            <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
            <span class="link hidden-tablet hidden-phone" style="color: #aaa;" title="{{item.d}}">{{item.t}}</span>
            <span ng-show="item.s" class="pull-right">
                <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
            </span>
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="ownedListFragment">
    <div ng-controller="ownedCtrl" style="border: 1px solid #ddd;border-radius: 4px;">
        <div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.myRepositories"></wicket:message> ({{owned.length}})
            <div class="hidden-phone pull-right">
                <span wicket:id="create"></span>
            </div>
            <div style="padding: 5px 0px 0px;">
                <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
            </div>
        </div>
        <div ng-repeat="item in owned | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
            <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
            <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
            <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
            <span ng-show="item.s" class="pull-right">
                <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
            </span>
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="activeListFragment">
    <div ng-controller="activeCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
        <div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.activeRepositories"></wicket:message> ({{active.length}})
            <div style="padding: 5px 0px 0px;">
                <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
            </div>
        </div>
        <div ng-repeat="item in active | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
            <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
            <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
            <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
            <span ng-show="item.s" class="pull-right">
                <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
            </span>
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="projectListFragment">
    <div ng-controller="projectListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
        <div class="header" style="padding: 5px;border: none;"><i class="icon-folder-close"></i> <wicket:message key="gb.projects"></wicket:message> ({{projectList.length}})
            <div style="padding: 5px 0px 0px;">
                <input type="text" ng-model="query.n" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
            </div>
        </div>
        <div ng-repeat="item in projectList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
            <a href="project/{{item.p}}" title="{{item.i}}"><b>{{item.n}}</b></a>
            <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
            <span class="pull-right">
                <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;" wicket:message="title:gb.repositories">{{item.c | number}}</span>
            </span>
        </div>
    </div>
</wicket:fragment>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java
New file
@@ -0,0 +1,311 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.pages;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.HeaderContributor;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Fragment;
import org.eclipse.jgit.lib.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.ng.NgController;
import com.gitblit.wicket.panels.LinkPanel;
public class MyDashboardPage extends DashboardPage {
    public MyDashboardPage() {
        super();
        setup(null);
    }
    public MyDashboardPage(PageParameters params) {
        super(params);
        setup(params);
    }
    @Override
    protected boolean reusePageParameters() {
        return true;
    }
    private void setup(PageParameters params) {
        setupPage("", "");
        // check to see if we should display a login message
        boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
        if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
            String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit");
            String message = readMarkdown(messageSource, "login.mkd");
            Component repositoriesMessage = new Label("repositoriesMessage", message);
            add(repositoriesMessage.setEscapeModelStrings(false));
            add(new Label("activity").setVisible(false));
            add(new Label("repositoryTabs").setVisible(false));
            return;
        }
        // Load the markdown welcome message
        String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit");
        String message = readMarkdown(messageSource, "welcome.mkd");
        Component repositoriesMessage = new Label("repositoriesMessage", message)
                .setEscapeModelStrings(false).setVisible(message.length() > 0);
        add(repositoriesMessage);
        UserModel user = GitBlitWebSession.get().getUser();
        if (user == null) {
            user = UserModel.ANONYMOUS;
        }
        // parameters
        int daysBack = params == null ? 0 : WicketUtils.getDaysBack(params);
        if (daysBack < 1) {
            daysBack = 7;
        }
        Calendar c = Calendar.getInstance();
        c.add(Calendar.DATE, -1*daysBack);
        Date minimumDate = c.getTime();
        // build repo lists
        List<RepositoryModel> starred = new ArrayList<RepositoryModel>();
        List<RepositoryModel> owned = new ArrayList<RepositoryModel>();
        List<RepositoryModel> active = new ArrayList<RepositoryModel>();
        for (RepositoryModel model : getRepositoryModels()) {
            if (model.isUsersPersonalRepository(user.username) || model.isOwner(user.username)) {
                owned.add(model);
            }
            if (user.getPreferences().isStarredRepository(model.name)) {
                starred.add(model);
            }
            if (model.isShowActivity() && model.lastChange.after(minimumDate)) {
                active.add(model);
            }
        }
        Comparator<RepositoryModel> lastUpdateSort = new Comparator<RepositoryModel>() {
            @Override
            public int compare(RepositoryModel o1, RepositoryModel o2) {
                return o2.lastChange.compareTo(o1.lastChange);
            }
        };
        Collections.sort(owned, lastUpdateSort);
        Collections.sort(starred, lastUpdateSort);
        Collections.sort(active, lastUpdateSort);
        Set<RepositoryModel> feed = new HashSet<RepositoryModel>();
        feed.addAll(starred);
        feed.addAll(owned);
        if (feed.isEmpty()) {
            feed.addAll(active);
        }
        addActivity(user, feed, daysBack);
        Fragment repositoryTabs;
        if (UserModel.ANONYMOUS.equals(user)) {
            repositoryTabs = new Fragment("repositoryTabs", "anonymousTabsFragment", this);
        } else {
            repositoryTabs = new Fragment("repositoryTabs", "authenticatedTabsFragment", this);
        }
        add(repositoryTabs);
        Fragment projectList = createProjectList();
        repositoryTabs.add(projectList);
        // active repository list
        if (active.isEmpty()) {
            repositoryTabs.add(new Label("active").setVisible(false));
        } else {
            Fragment activeView = createNgList("active", "activeListFragment", "activeCtrl", active);
            repositoryTabs.add(activeView);
        }
        // starred repository list
        if (ArrayUtils.isEmpty(starred)) {
            repositoryTabs.add(new Label("starred").setVisible(false));
        } else {
            Fragment starredView = createNgList("starred", "starredListFragment", "starredCtrl", starred);
            repositoryTabs.add(starredView);
        }
        // owned repository list
        if (ArrayUtils.isEmpty(owned)) {
            repositoryTabs.add(new Label("owned").setVisible(false));
        } else {
            Fragment ownedView = createNgList("owned", "ownedListFragment", "ownedCtrl", owned);
            if (user.canCreate) {
                // create button
                ownedView.add(new LinkPanel("create", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class));
            } else {
                // no button
                ownedView.add(new Label("create").setVisible(false));
            }
            repositoryTabs.add(ownedView);
        }
    }
    private String readMarkdown(String messageSource, String resource) {
        String message = "";
        if (messageSource.equalsIgnoreCase("gitblit")) {
            // Read default message
            message = readDefaultMarkdown(resource);
        } else {
            // Read user-supplied message
            if (!StringUtils.isEmpty(messageSource)) {
                File file = GitBlit.getFileOrFolder(messageSource);
                if (file.exists()) {
                    try {
                        FileInputStream fis = new FileInputStream(file);
                        InputStreamReader reader = new InputStreamReader(fis,
                                Constants.CHARACTER_ENCODING);
                        message = MarkdownUtils.transformMarkdown(reader);
                        reader.close();
                    } catch (Throwable t) {
                        message = getString("gb.failedToRead") + " " + file;
                        warn(message, t);
                    }
                } else {
                    message = messageSource + " " + getString("gb.isNotValidFile");
                }
            }
        }
        return message;
    }
    private String readDefaultMarkdown(String file) {
        String base = file.substring(0, file.lastIndexOf('.'));
        String ext = file.substring(file.lastIndexOf('.'));
        String lc = getLanguageCode();
        String cc = getCountryCode();
        // try to read file_en-us.ext, file_en.ext, file.ext
        List<String> files = new ArrayList<String>();
        if (!StringUtils.isEmpty(lc)) {
            if (!StringUtils.isEmpty(cc)) {
                files.add(base + "_" + lc + "-" + cc + ext);
                files.add(base + "_" + lc + "_" + cc + ext);
            }
            files.add(base + "_" + lc + ext);
        }
        files.add(file);
        for (String name : files) {
            String message;
            InputStreamReader reader = null;
            try {
                InputStream is = getClass().getResourceAsStream("/" + name);
                if (is == null) {
                    continue;
                }
                reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                return message;
            } catch (Throwable t) {
                message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
                error(message, t, false);
                return message;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
            }
        }
        return MessageFormat.format(getString("gb.failedToReadMessage"), file);
    }
    protected Fragment createProjectList() {
        String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
        final DateFormat df = new SimpleDateFormat(format);
        df.setTimeZone(getTimeZone());
        List<ProjectModel> projects = GitBlit.self().getProjectModels(getRepositoryModels(), false);
        Collections.sort(projects, new Comparator<ProjectModel>() {
            @Override
            public int compare(ProjectModel o1, ProjectModel o2) {
                return o2.lastChange.compareTo(o1.lastChange);
            }
        });
        List<ProjectListItem> list = new ArrayList<ProjectListItem>();
        for (ProjectModel proj : projects) {
            if (proj.isUserProject() || proj.repositories.isEmpty()) {
                // exclude user projects from list
                continue;
            }
            ProjectListItem item = new ProjectListItem();
            item.p = proj.name;
            item.n = StringUtils.isEmpty(proj.title) ? proj.name : proj.title;
            item.i = proj.description;
            item.t = getTimeUtils().timeAgo(proj.lastChange);
            item.d = df.format(proj.lastChange);
            item.c = proj.repositories.size();
            list.add(item);
        }
        // inject an AngularJS controller with static data
        NgController ctrl = new NgController("projectListCtrl");
        ctrl.addVariable("projectList", list);
        add(new HeaderContributor(ctrl));
        Fragment fragment = new Fragment("projectList", "projectListFragment", this);
        return fragment;
    }
    protected class ProjectListItem implements Serializable {
        private static final long serialVersionUID = 1L;
        String p; // path
        String n; // name
        String t; // time ago
        String d; // last updated
        String i; // information/description
        long c;   // repository count
    }
}
src/main/java/com/gitblit/wicket/pages/OverviewPage.html
@@ -48,7 +48,7 @@
        <div class="span6">
            <div class="hidden-tablet" style="padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid #ddd;" wicket:id="repositoryUrlPanel">[repository url panel]</div>
        
            <div wicket:id="pushesPanel">[pushes panel]</div>
            <div wicket:id="reflogPanel">[reflog panel]</div>
        </div>
    </div>
src/main/java/com/gitblit/wicket/pages/OverviewPage.java
@@ -41,7 +41,7 @@
import com.gitblit.wicket.charting.GoogleLineChart;
import com.gitblit.wicket.panels.BranchesPanel;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.PushesPanel;
import com.gitblit.wicket.panels.ReflogPanel;
import com.gitblit.wicket.panels.RepositoryUrlPanel;
import com.gitblit.wicket.panels.TagsPanel;
@@ -113,9 +113,9 @@
        add(new RepositoryUrlPanel("repositoryUrlPanel", false, user, model));
        int pushCount = GitBlit.getInteger(Keys.web.overviewPushCount, 5);
        PushesPanel pushes = new PushesPanel("pushesPanel", getRepositoryModel(), r, pushCount, 0, false);
        add(pushes);
        int reflogCount = GitBlit.getInteger(Keys.web.overviewReflogCount, 5);
        ReflogPanel reflog = new ReflogPanel("reflogPanel", getRepositoryModel(), r, reflogCount, 0);
        add(reflog);
        add(new TagsPanel("tagsPanel", repositoryName, r, numberRefs).hideIfEmpty());
        add(new BranchesPanel("branchesPanel", getRepositoryModel(), r, numberRefs, false).hideIfEmpty());
src/main/java/com/gitblit/wicket/pages/ProjectPage.html
@@ -6,66 +6,70 @@
<body>
<wicket:extend>
<div class="container">
    <div class="row">
        <div class="span12">
            <h2><span wicket:id="projectTitle"></span> <small><span wicket:id="projectDescription"></span></small>
                <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
                    <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
                </a>
            </h2>
            <div class="markdown" wicket:id="projectMessage">[project message]</div>
        <div class="container">
            <div class="row" style="padding-top:5px;">
                <div class="span12">
                    <div class="dashboardTitle">
                        <span wicket:id="projectTitle"></span>
                        <small><span wicket:id="projectDescription"></span></small>
                        <a
                            class="hidden-phone hidden-tablet brand"
                            style="text-decoration: none;" wicket:id="syndication"
                            wicket:message="title:gb.feed"> <img
                            style="border: 0px; vertical-align: middle;" src="feed_16x16.png"></img>
                        </a>
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="span7">
                    <div class="markdown" style="padding-bottom: 30px;" wicket:id="projectMessage">[project message]</div>
                    <div wicket:id="activity">[activity panel]</div>
                </div>
                <div class="span5">
                    <div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div>
                    <div wicket:id="repositoryList">[repository list]</div>
                </div>
            </div>
        </div>
<wicket:fragment wicket:id="activityFragment">
    <div class="dashboardTitle"><wicket:message key="gb.recentActivity"></wicket:message> <small><span wicket:id="feedheader"></span></small></div>
    <div class="hidden-phone hidden-tablet"  style="text-align:center;">
        <div wicket:id="charts"></div>
    </div>
    <div wicket:id="digests"></div>
</wicket:fragment>
    <div class="tabbable">
        <!-- tab titles -->
        <ul class="nav nav-tabs">
            <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
            <li ><a href="#activity" data-toggle="tab"><wicket:message key="gb.activity"></wicket:message></a></li>
        </ul>
        <!-- tab content -->
        <div class="tab-content">
            <!-- repositories tab -->
            <div class="tab-pane active" id="repositories">
                <!-- markdown -->
                <div class="row">
                    <div class="span12">
                        <div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div>
                    </div>
                </div>
                <div class="row">
                    <div class="span6" wicket:id="repositoryList">
                        <span wicket:id="repository"></span>
                    </div>
                </div>
            </div>
            <!-- activity tab -->
            <div class="tab-pane" id="activity">
                <div class="pageTitle">
                    <h2><wicket:message key="gb.recentActivity"></wicket:message><small> <span class="hidden-phone">/ <span wicket:id="subheader">[days back]</span></span></small></h2>
                </div>
                <div class="hidden-phone" style="height: 155px;text-align: center;">
                    <table>
                    <tr>
                        <td><span class="hidden-tablet" id="chartDaily"></span></td>
                        <td><span id="chartRepositories"></span></td>
                        <td><span id="chartAuthors"></span></td>
                    </tr>
                    </table>
                </div>
                <div wicket:id="activityPanel">[activity panel]</div>
            </div>
<wicket:fragment wicket:id="chartsFragment">
    <table>
        <tr>
            <td><div id="chartRepositories" style="display:inline-block;width: 175px; height:175px"></div></td>
            <td><div id="chartAuthors" style="display:inline-block;width: 175px; height: 175px;"></div></td>
        </tr>
    </table>
</wicket:fragment>
        
<wicket:fragment wicket:id="repositoryListFragment">
    <div ng-controller="repositoryListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
        <div class="header" style="padding: 5px;border: none;"><img style="vertical-align: middle;" src="git-black-16x16.png"/> <wicket:message key="gb.repositories"></wicket:message> ({{repositoryList.length}})
            <div style="padding: 5px 0px 0px;">
                <input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
            </div>
        </div>
        <div ng-repeat="item in repositoryList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
            <b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
            <a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
            <span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
            <span ng-show="item.s" class="pull-right">
                <span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
            </span>
        </div>
    </div>
    </div>
</wicket:extend>
</wicket:fragment>
    </wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -15,34 +15,23 @@
 */
package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.HeaderContributor;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.markup.html.panel.Fragment;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.SyndicationServlet;
import com.gitblit.models.Activity;
import com.gitblit.models.Metric;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.ActivityUtils;
import com.gitblit.models.UserModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
@@ -52,14 +41,8 @@
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.charting.GoogleChart;
import com.gitblit.wicket.charting.GoogleCharts;
import com.gitblit.wicket.charting.GoogleLineChart;
import com.gitblit.wicket.charting.GooglePieChart;
import com.gitblit.wicket.panels.ActivityPanel;
import com.gitblit.wicket.panels.ProjectRepositoryPanel;
public class ProjectPage extends RootPage {
public class ProjectPage extends DashboardPage {
    
    List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
@@ -72,10 +55,9 @@
        super(params);
        setup(params);
    }
    @Override
    protected boolean reusePageParameters() {
        return true;
    protected Class<? extends BasePage> getRootNavPageClass() {
        return RepositoriesPage.class;
    }
    private void setup(PageParameters params) {
@@ -118,8 +100,20 @@
                .setEscapeModelStrings(false).setVisible(rmessage.length() > 0);
        add(repositoriesMessage);
        List<RepositoryModel> repositories = getRepositories(params);
        UserModel user = GitBlitWebSession.get().getUser();
        if (user == null) {
            user = UserModel.ANONYMOUS;
        }
        int daysBack = params == null ? 0 : WicketUtils.getDaysBack(params);
        if (daysBack < 1) {
            daysBack = 7;
        }
        // reset the daysback parameter so that we have a complete project
        // repository list.  the recent activity will be built up by the
        // reflog utils.
        params.put("db", 0);
        
        List<RepositoryModel> repositories = getRepositories(params);
        Collections.sort(repositories, new Comparator<RepositoryModel>() {
            @Override
            public int compare(RepositoryModel o1, RepositoryModel o2) {
@@ -128,144 +122,20 @@
            }
        });
        final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<RepositoryModel> item) {
                final RepositoryModel entry = item.getModelObject();
                ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository",
                        getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
                item.add(row);
            }
        };
        add(dataView);
        // project activity
        // parameters
        int daysBack = WicketUtils.getDaysBack(params);
        if (daysBack < 1) {
            daysBack = 14;
        }
        String objectId = WicketUtils.getObject(params);
        List<Activity> recentActivity = ActivityUtils.getRecentActivity(repositories,
                daysBack, objectId, getTimeZone());
        if (recentActivity.size() == 0) {
            // no activity, skip graphs and activity panel
            add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityNone"),
                    daysBack)));
            add(new Label("activityPanel"));
        addActivity(user, repositories, daysBack);
        if (repositories.isEmpty()) {
            add(new Label("repositoryList").setVisible(false));
        } else {
            // calculate total commits and total authors
            int totalCommits = 0;
            Set<String> uniqueAuthors = new HashSet<String>();
            for (Activity activity : recentActivity) {
                totalCommits += activity.getCommitCount();
                uniqueAuthors.addAll(activity.getAuthorMetrics().keySet());
            }
            int totalAuthors = uniqueAuthors.size();
            // add the subheader with stat numbers
            add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityStats"),
                    daysBack, totalCommits, totalAuthors)));
            // create the activity charts
            GoogleCharts charts = createCharts(recentActivity);
            add(new HeaderContributor(charts));
            // add activity panel
            add(new ActivityPanel("activityPanel", recentActivity));
            Fragment activeView = createNgList("repositoryList", "repositoryListFragment", "repositoryListCtrl", repositories);
            add(activeView);
        }
    }
    
    /**
     * Creates the daily activity line chart, the active repositories pie chart,
     * and the active authors pie chart
     *
     * @param recentActivity
     * @return
     */
    private GoogleCharts createCharts(List<Activity> recentActivity) {
        // activity metrics
        Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
        Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
        // aggregate repository and author metrics
        for (Activity activity : recentActivity) {
            // aggregate author metrics
            for (Map.Entry<String, Metric> entry : activity.getAuthorMetrics().entrySet()) {
                String author = entry.getKey();
                if (!authorMetrics.containsKey(author)) {
                    authorMetrics.put(author, new Metric(author));
                }
                authorMetrics.get(author).count += entry.getValue().count;
            }
            // aggregate repository metrics
            for (Map.Entry<String, Metric> entry : activity.getRepositoryMetrics().entrySet()) {
                String repository = StringUtils.stripDotGit(entry.getKey());
                if (!repositoryMetrics.containsKey(repository)) {
                    repositoryMetrics.put(repository, new Metric(repository));
                }
                repositoryMetrics.get(repository).count += entry.getValue().count;
            }
        }
        // build google charts
        int w = 310;
        int h = 150;
        GoogleCharts charts = new GoogleCharts();
        // sort in reverse-chronological order and then reverse that
        Collections.sort(recentActivity);
        Collections.reverse(recentActivity);
        // daily line chart
        GoogleChart chart = new GoogleLineChart("chartDaily", getString("gb.dailyActivity"), "day",
                getString("gb.commits"));
        SimpleDateFormat df = new SimpleDateFormat("MMM dd");
        df.setTimeZone(getTimeZone());
        for (Activity metric : recentActivity) {
            chart.addValue(df.format(metric.startDate), metric.getCommitCount());
        }
        chart.setWidth(w);
        chart.setHeight(h);
        charts.addChart(chart);
        // active repositories pie chart
        chart = new GooglePieChart("chartRepositories", getString("gb.activeRepositories"),
                getString("gb.repository"), getString("gb.commits"));
        for (Metric metric : repositoryMetrics.values()) {
            chart.addValue(metric.name, metric.count);
        }
        chart.setWidth(w);
        chart.setHeight(h);
        charts.addChart(chart);
        // active authors pie chart
        chart = new GooglePieChart("chartAuthors", getString("gb.activeAuthors"),
                getString("gb.author"), getString("gb.commits"));
        for (Metric metric : authorMetrics.values()) {
            chart.addValue(metric.name, metric.count);
        }
        chart.setWidth(w);
        chart.setHeight(h);
        charts.addChart(chart);
        return charts;
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
                ProjectPage.class);
        projects.menuItems.addAll(getProjectsMenu());
        pages.add(0, projects);
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
                ProjectPage.class);
@@ -277,10 +147,15 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
        }
        pages.add(menu);
        DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
                ProjectPage.class);
        projects.menuItems.addAll(getProjectsMenu());
        pages.add(projects);
    }
    
    @Override
src/main/java/com/gitblit/wicket/pages/ProjectsPage.html
@@ -7,7 +7,6 @@
<body>
<wicket:extend>
<div class="container">
    <div class="markdown" style="padding-bottom:5px;" wicket:id="projectsMessage">[projects message]</div>
    
    <table class="repositories">
        <thead>
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
@@ -15,29 +15,17 @@
 */
package com.gitblit.wicket.pages;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.resource.ContextRelativeResource;
import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.eclipse.jgit.lib.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
@@ -63,6 +51,11 @@
    }
    
    @Override
    protected Class<? extends BasePage> getRootNavPageClass() {
        return RepositoriesPage.class;
    }
    @Override
    protected List<ProjectModel> getProjectModels() {
        return GitBlit.self().getProjectModels(getRepositoryModels(), false);
    }
@@ -72,20 +65,9 @@
        // check to see if we should display a login message
        boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
        if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
            String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit");
            String message = readMarkdown(messageSource, "login.mkd");
            Component repositoriesMessage = new Label("projectsMessage", message);
            add(repositoriesMessage.setEscapeModelStrings(false));
            add(new Label("projectsPanel"));
            return;
        }
        // Load the markdown welcome message
        String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit");
        String message = readMarkdown(messageSource, "welcome.mkd");
        Component projectsMessage = new Label("projectsMessage", message).setEscapeModelStrings(
                false).setVisible(message.length() > 0);
        add(projectsMessage);
        List<ProjectModel> projects = getProjects(params);
@@ -130,20 +112,12 @@
            }
        };
        add(dataView);
        // push the panel down if we are hiding the admin controls and the
        // welcome message
        if (!showAdmin && !projectsMessage.isVisible()) {
            WicketUtils.setCssStyle(dataView, "padding-top:5px;");
        }
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
        PageParameters params = getPageParameters();
        
        pages.add(0, new PageRegistration("gb.projects", ProjectsPage.class, params));
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
                ProjectsPage.class);
        // preserve time filter option on repository choices
@@ -158,78 +132,5 @@
        }
        pages.add(menu);
    }
    private String readMarkdown(String messageSource, String resource) {
        String message = "";
        if (messageSource.equalsIgnoreCase("gitblit")) {
            // Read default message
            message = readDefaultMarkdown(resource);
        } else {
            // Read user-supplied message
            if (!StringUtils.isEmpty(messageSource)) {
                File file = new File(messageSource);
                if (file.exists()) {
                    try {
                        FileInputStream fis = new FileInputStream(file);
                        InputStreamReader reader = new InputStreamReader(fis,
                                Constants.CHARACTER_ENCODING);
                        message = MarkdownUtils.transformMarkdown(reader);
                        reader.close();
                    } catch (Throwable t) {
                        message = getString("gb.failedToRead") + " " + file;
                        warn(message, t);
                    }
                } else {
                    message = messageSource + " " + getString("gb.isNotValidFile");
                }
            }
        }
        return message;
    }
    private String readDefaultMarkdown(String file) {
        String base = file.substring(0, file.lastIndexOf('.'));
        String ext = file.substring(file.lastIndexOf('.'));
        String lc = getLanguageCode();
        String cc = getCountryCode();
        // try to read file_en-us.ext, file_en.ext, file.ext
        List<String> files = new ArrayList<String>();
        if (!StringUtils.isEmpty(lc)) {
            if (!StringUtils.isEmpty(cc)) {
                files.add(base + "_" + lc + "-" + cc + ext);
                files.add(base + "_" + lc + "_" + cc + ext);
            }
            files.add(base + "_" + lc + ext);
        }
        files.add(file);
        for (String name : files) {
            String message;
            InputStreamReader reader = null;
            try {
                ContextRelativeResource res = WicketUtils.getResource(name);
                InputStream is = res.getResourceStream().getInputStream();
                reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                return message;
            } catch (ResourceStreamNotFoundException t) {
                continue;
            } catch (Throwable t) {
                message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
                error(message, t, false);
                return message;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
            }
        }
        return MessageFormat.format(getString("gb.failedToReadMessage"), file);
    }
}
src/main/java/com/gitblit/wicket/pages/ReflogPage.html
File was renamed from src/main/java/com/gitblit/wicket/pages/PushesPage.html
@@ -12,8 +12,8 @@
        <a wicket:id="firstPage"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPage">&laquo; <wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPage"><wicket:message key="gb.pageNext"></wicket:message> &raquo;</a> 
    </div>
    
    <!-- push log -->
    <div style="margin-top:5px;" wicket:id="pushesPanel">[push log panel]</div>
    <!-- ref log -->
    <div style="margin-top:5px;" wicket:id="reflogPanel">[reflog panel]</div>
    <!-- pager links -->
    <div style="padding-bottom:5px;">
src/main/java/com/gitblit/wicket/pages/ReflogPage.java
File was renamed from src/main/java/com/gitblit/wicket/pages/PushesPage.java
@@ -19,11 +19,11 @@
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.PushesPanel;
import com.gitblit.wicket.panels.ReflogPanel;
public class PushesPage extends RepositoryPage {
public class ReflogPage extends RepositoryPage {
    public PushesPage(PageParameters params) {
    public ReflogPage(PageParameters params) {
        super(params);
        addSyndicationDiscoveryLink();
@@ -32,24 +32,24 @@
        int prevPage = Math.max(0, pageNumber - 1);
        int nextPage = pageNumber + 1;
        PushesPanel pushesPanel = new PushesPanel("pushesPanel", getRepositoryModel(), getRepository(), -1,
                pageNumber - 1, false);
        boolean hasMore = pushesPanel.hasMore();
        add(pushesPanel);
        ReflogPanel reflogPanel = new ReflogPanel("reflogPanel", getRepositoryModel(), getRepository(), -1,
                pageNumber - 1);
        boolean hasMore = reflogPanel.hasMore();
        add(reflogPanel);
        add(new BookmarkablePageLink<Void>("firstPage", PushesPage.class,
        add(new BookmarkablePageLink<Void>("firstPage", ReflogPage.class,
                WicketUtils.newObjectParameter(repositoryName, objectId))
                .setEnabled(pageNumber > 1));
        add(new BookmarkablePageLink<Void>("prevPage", PushesPage.class,
        add(new BookmarkablePageLink<Void>("prevPage", ReflogPage.class,
                WicketUtils.newLogPageParameter(repositoryName, objectId, prevPage))
                .setEnabled(pageNumber > 1));
        add(new BookmarkablePageLink<Void>("nextPage", PushesPage.class,
        add(new BookmarkablePageLink<Void>("nextPage", ReflogPage.class,
                WicketUtils.newLogPageParameter(repositoryName, objectId, nextPage))
                .setEnabled(hasMore));
    }
    @Override
    protected String getPageName() {
        return getString("gb.pushes");
        return getString("gb.reflog");
    }
}
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -60,7 +60,7 @@
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TicgitUtils;
import com.gitblit.wicket.GitBlitWebSession;
@@ -187,12 +187,12 @@
        RepositoryModel model = getRepositoryModel();
        // standard links
        if (PushLogUtils.getPushLogBranch(r) == null) {
        if (RefLogUtils.getRefLogBranch(r) == null) {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
        } else {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
//            pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
            pages.put("pushes", new PageRegistration("gb.pushes", PushesPage.class, params));
            pages.put("reflog", new PageRegistration("gb.reflog", ReflogPage.class, params));
        }        
        pages.put("commits", new PageRegistration("gb.commits", LogPage.class, params));
        pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -119,7 +119,7 @@
        // navigation links
        List<PageRegistration> pages = new ArrayList<PageRegistration>();
        if (!authenticateView || (authenticateView && GitBlitWebSession.get().isLoggedIn())) {
            pages.add(new PageRegistration(GitBlitWebSession.get().isLoggedIn() ? "gb.myDashboard" : "gb.dashboard", DashboardPage.class,
            pages.add(new PageRegistration(GitBlitWebSession.get().isLoggedIn() ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
                    getRootPageParameters()));
            pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
                    getRootPageParameters()));
src/main/java/com/gitblit/wicket/panels/DigestsPanel.html
File was renamed from src/main/java/com/gitblit/wicket/panels/PushesPanel.html
@@ -6,15 +6,15 @@
<body>
<wicket:panel>
<div wicket:id="push" class="push">
<div wicket:id="change" class="reflog">
    <table style="padding: 3px 0px;">
    <tr>
        <td class="icon hidden-phone"><i wicket:id="pushIcon"></i></td>
        <td class="icon hidden-phone"><i wicket:id="changeIcon"></i></td>
        <td style="padding-left: 7px;vertical-align:middle;">
            <div>
                <span style="color:#aaa;" wicket:id="whenPushed"></span> <span wicket:id="refRewind" class="alert alert-error" style="padding: 1px 5px;font-size: 10px;font-weight: bold;margin-left: 10px;">[rewind]</span>
                <span class="when" wicket:id="whenChanged"></span>
            </div>
            <div style="font-weight:bold;"><span wicket:id="whoPushed">[pusher]</span> <span wicket:id="whatPushed"></span><span wicket:id="refPushed"></span> <span wicket:id="repoPreposition"></span> <span wicket:id="repoPushed"></span> <span wicket:id="byAuthors"></span></div>
            <div style="font-weight:bold;"><span wicket:id="whoChanged">[who changed]</span> <span wicket:id="whatChanged"></span><span wicket:id="refChanged"></span> <span wicket:id="repoPreposition"></span> <span wicket:id="repoChanged"></span> <span wicket:id="byAuthors"></span></div>
        </td>
    </tr>
    <tr>
@@ -37,7 +37,6 @@
    </tr>    
    </table>
</div>
<div wicket:id="morePushes">[more...]</div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/DigestsPanel.java
New file
@@ -0,0 +1,273 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.panels;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.DailyLogEntry;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.CommitPage;
import com.gitblit.wicket.pages.ComparePage;
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.TagPage;
import com.gitblit.wicket.pages.TreePage;
import com.gitblit.wicket.pages.UserPage;
public class DigestsPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final boolean hasChanges;
    private boolean hasMore;
    public DigestsPanel(String wicketId, List<DailyLogEntry> digests) {
        super(wicketId);
        hasChanges = digests.size() > 0;
        final int hashLen = GitBlit.getInteger(Keys.web.shortCommitIdLength, 6);
        String dateFormat = GitBlit.getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
        final TimeZone timezone = getTimeZone();
        final DateFormat df = new SimpleDateFormat(dateFormat);
        df.setTimeZone(timezone);
        final Calendar cal = Calendar.getInstance(timezone);
        ListDataProvider<DailyLogEntry> dp = new ListDataProvider<DailyLogEntry>(digests);
        DataView<DailyLogEntry> pushView = new DataView<DailyLogEntry>("change", dp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<DailyLogEntry> logItem) {
                final DailyLogEntry change = logItem.getModelObject();
                String fullRefName = change.getChangedRefs().get(0);
                String shortRefName = fullRefName;
                boolean isTag = false;
                if (shortRefName.startsWith(Constants.R_HEADS)) {
                    shortRefName = shortRefName.substring(Constants.R_HEADS.length());
                } else if (shortRefName.startsWith(Constants.R_TAGS)) {
                    shortRefName = shortRefName.substring(Constants.R_TAGS.length());
                    isTag = true;
                }
                String fuzzydate;
                TimeUtils tu = getTimeUtils();
                Date pushDate = change.date;
                if (TimeUtils.isToday(pushDate, timezone)) {
                    fuzzydate = tu.today();
                } else if (TimeUtils.isYesterday(pushDate, timezone)) {
                    fuzzydate = tu.yesterday();
                } else {
                    // calculate a fuzzy time ago date
                    cal.setTime(pushDate);
                    cal.set(Calendar.HOUR_OF_DAY, 0);
                    cal.set(Calendar.MINUTE, 0);
                    cal.set(Calendar.SECOND, 0);
                    cal.set(Calendar.MILLISECOND, 0);
                    pushDate = cal.getTime();
                    fuzzydate = getTimeUtils().timeAgo(pushDate);
                }
                logItem.add(new Label("whenChanged", fuzzydate + ", " + df.format(pushDate)));
                Label changeIcon = new Label("changeIcon");
                // use the repository hash color to differentiate the icon.
                String color = StringUtils.getColor(StringUtils.stripDotGit(change.repository));
                WicketUtils.setCssStyle(changeIcon, "color: " + color);
                if (isTag) {
                    WicketUtils.setCssClass(changeIcon, "iconic-tag");
                } else {
                    WicketUtils.setCssClass(changeIcon, "iconic-loop");
                }
                logItem.add(changeIcon);
                if (!isTag) {
                    logItem.add(new Label("whoChanged").setVisible(false));
                } else {
                    if (change.user.username.equals(change.user.emailAddress) && change.user.emailAddress.indexOf('@') > -1) {
                        // username is an email address can not link - 1.2.1 push log bug
                        logItem.add(new Label("whoChanged", change.user.getDisplayName()));
                    } else {
                        // link to user account page
                        logItem.add(new LinkPanel("whoChanged", null, change.user.getDisplayName(),
                                UserPage.class, WicketUtils.newUsernameParameter(change.user.username)));
                    }
                }
                String preposition = "gb.of";
                boolean isDelete = false;
                String what;
                String by = null;
                switch(change.getChangeType(fullRefName)) {
                case CREATE:
                    if (isTag) {
                        // new tag
                        what = getString("gb.createdNewTag");
                        preposition = "gb.in";
                    } else {
                        // new branch
                        what = getString("gb.createdNewBranch");
                        preposition = "gb.in";
                    }
                    break;
                case DELETE:
                    isDelete = true;
                    if (isTag) {
                        what = getString("gb.deletedTag");
                    } else {
                        what = getString("gb.deletedBranch");
                    }
                    preposition = "gb.from";
                    break;
                default:
                    what = MessageFormat.format(change.getCommitCount() > 1 ? getString("gb.commitsTo") : getString("gb.oneCommitTo"), change.getCommitCount());
                    if (change.getAuthorCount() == 1) {
                        by = MessageFormat.format(getString("gb.byOneAuthor"), change.getAuthorIdent().getName());
                    } else {
                        by = MessageFormat.format(getString("gb.byNAuthors"), change.getAuthorCount());
                    }
                    break;
                }
                logItem.add(new Label("whatChanged", what));
                logItem.add(new Label("byAuthors", by).setVisible(!StringUtils.isEmpty(by)));
                if (isDelete) {
                    // can't link to deleted ref
                    logItem.add(new Label("refChanged", shortRefName));
                } else if (isTag) {
                    // link to tag
                    logItem.add(new LinkPanel("refChanged", null, shortRefName,
                            TagPage.class, WicketUtils.newObjectParameter(change.repository, fullRefName)));
                } else {
                    // link to tree
                    logItem.add(new LinkPanel("refChanged", null, shortRefName,
                        TreePage.class, WicketUtils.newObjectParameter(change.repository, fullRefName)));
                }
                // to/from/etc
                logItem.add(new Label("repoPreposition", getString(preposition)));
                String repoName = StringUtils.stripDotGit(change.repository);
                logItem.add(new LinkPanel("repoChanged", null, repoName,
                        SummaryPage.class, WicketUtils.newRepositoryParameter(change.repository)));
                int maxCommitCount = 5;
                List<RepositoryCommit> commits = change.getCommits();
                if (commits.size() > maxCommitCount) {
                    commits = new ArrayList<RepositoryCommit>(commits.subList(0,  maxCommitCount));
                }
                // compare link
                String compareLinkText = null;
                if ((change.getCommitCount() <= maxCommitCount) && (change.getCommitCount() > 1)) {
                    compareLinkText = MessageFormat.format(getString("gb.viewComparison"), commits.size());
                } else if (change.getCommitCount() > maxCommitCount) {
                    int diff = change.getCommitCount() - maxCommitCount;
                    compareLinkText = MessageFormat.format(diff > 1 ? getString("gb.nMoreCommits") : getString("gb.oneMoreCommit"), diff);
                }
                if (StringUtils.isEmpty(compareLinkText)) {
                    logItem.add(new Label("compareLink").setVisible(false));
                } else {
                    String endRangeId = change.getNewId(fullRefName);
                    String startRangeId = change.getOldId(fullRefName);
                    logItem.add(new LinkPanel("compareLink", null, compareLinkText, ComparePage.class, WicketUtils.newRangeParameter(change.repository, startRangeId, endRangeId)));
                }
                final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
                ListDataProvider<RepositoryCommit> cdp = new ListDataProvider<RepositoryCommit>(commits);
                DataView<RepositoryCommit> commitsView = new DataView<RepositoryCommit>("commit", cdp) {
                    private static final long serialVersionUID = 1L;
                    public void populateItem(final Item<RepositoryCommit> commitItem) {
                        final RepositoryCommit commit = commitItem.getModelObject();
                        // author gravatar
                        commitItem.add(new GravatarImage("commitAuthor", commit.getAuthorIdent().getName(),
                                commit.getAuthorIdent().getEmailAddress(), null, 16, false, false));
                        // merge icon
                        if (commit.getParentCount() > 1) {
                            commitItem.add(WicketUtils.newImage("commitIcon", "commit_merge_16x16.png"));
                        } else {
                            commitItem.add(WicketUtils.newBlankImage("commitIcon"));
                        }
                        // short message
                        String shortMessage = commit.getShortMessage();
                        String trimmedMessage = shortMessage;
                        if (commit.getRefs() != null && commit.getRefs().size() > 0) {
                            trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
                        } else {
                            trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
                        }
                        LinkPanel shortlog = new LinkPanel("commitShortMessage", "list",
                                trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
                                        change.repository, commit.getName()));
                        if (!shortMessage.equals(trimmedMessage)) {
                            WicketUtils.setHtmlTooltip(shortlog, shortMessage);
                        }
                        commitItem.add(shortlog);
                        // commit hash link
                        LinkPanel commitHash = new LinkPanel("hashLink", null, commit.getName().substring(0, hashLen),
                                CommitPage.class, WicketUtils.newObjectParameter(
                                        change.repository, commit.getName()));
                        WicketUtils.setCssClass(commitHash, "shortsha1");
                        WicketUtils.setHtmlTooltip(commitHash, commit.getName());
                        commitItem.add(commitHash);
                        if (showSwatch) {
                            // set repository color
                            String color = StringUtils.getColor(StringUtils.stripDotGit(change.repository));
                            WicketUtils.setCssStyle(commitItem, MessageFormat.format("border-left: 2px solid {0};", color));
                        }
                    }
                };
                logItem.add(commitsView);
            }
        };
        add(pushView);
    }
    public boolean hasMore() {
        return hasMore;
    }
    public boolean hideIfEmpty() {
        setVisible(hasChanges);
        return hasChanges;
    }
}
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -70,8 +70,6 @@
                    <wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
                    <span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
                </div>
                <div class="hidden-phone hidden-tablet" style="padding-top: 5px;" wicket:id="repositoryPrimaryUrl">[repository primary url]</div>
            </div>
        </div>
    </div>
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -192,7 +192,5 @@
        }
        add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0)));
        add(new RepositoryUrlPanel("repositoryPrimaryUrl", true, user, entry));
    }
}
src/main/java/com/gitblit/wicket/panels/PushesPanel.java
File was deleted
src/main/java/com/gitblit/wicket/panels/ReflogPanel.html
copy from src/main/java/com/gitblit/wicket/panels/PushesPanel.html copy to src/main/java/com/gitblit/wicket/panels/ReflogPanel.html
File was copied from src/main/java/com/gitblit/wicket/panels/PushesPanel.html
@@ -6,15 +6,15 @@
<body>
<wicket:panel>
<div wicket:id="push" class="push">
<div wicket:id="change" class="reflog">
    <table style="padding: 3px 0px;">
    <tr>
        <td class="icon hidden-phone"><i wicket:id="pushIcon"></i></td>
        <td class="icon hidden-phone"><i wicket:id="changeIcon"></i></td>
        <td style="padding-left: 7px;vertical-align:middle;">
            <div>
                <span style="color:#aaa;" wicket:id="whenPushed"></span> <span wicket:id="refRewind" class="alert alert-error" style="padding: 1px 5px;font-size: 10px;font-weight: bold;margin-left: 10px;">[rewind]</span>
                <span class="when" wicket:id="whenChanged"></span> <span wicket:id="refRewind" class="alert alert-error" style="padding: 1px 5px;font-size: 10px;font-weight: bold;margin-left: 10px;">[rewind]</span>
            </div>
            <div style="font-weight:bold;"><span wicket:id="whoPushed">[pusher]</span> <span wicket:id="whatPushed"></span><span wicket:id="refPushed"></span> <span wicket:id="repoPreposition"></span> <span wicket:id="repoPushed"></span> <span wicket:id="byAuthors"></span></div>
            <div style="font-weight:bold;"><span wicket:id="whoChanged">[change author]</span> <span wicket:id="whatChanged"></span><span wicket:id="refChanged"></span> <span wicket:id="byAuthors"></span></div>
        </td>
    </tr>
    <tr>
@@ -37,7 +37,7 @@
    </tr>    
    </table>
</div>
<div wicket:id="morePushes">[more...]</div>
<div wicket:id="moreChanges">[more...]</div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/ReflogPanel.java
New file
@@ -0,0 +1,305 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.panels;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.StringResourceModel;
import org.eclipse.jgit.lib.Repository;
import com.gitblit.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.CommitPage;
import com.gitblit.wicket.pages.ComparePage;
import com.gitblit.wicket.pages.ReflogPage;
import com.gitblit.wicket.pages.TagPage;
import com.gitblit.wicket.pages.TreePage;
import com.gitblit.wicket.pages.UserPage;
public class ReflogPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final boolean hasChanges;
    private boolean hasMore;
    public ReflogPanel(String wicketId, final RepositoryModel model, Repository r, int limit, int pageOffset) {
        super(wicketId);
        boolean pageResults = limit <= 0;
        int changesPerPage = GitBlit.getInteger(Keys.web.reflogChangesPerPage, 10);
        if (changesPerPage <= 1) {
            changesPerPage = 10;
        }
        List<RefLogEntry> changes;
        if (pageResults) {
            changes = RefLogUtils.getLogByRef(model.name, r, pageOffset * changesPerPage, changesPerPage);
        } else {
            changes = RefLogUtils.getLogByRef(model.name, r, limit);
        }
        // inaccurate way to determine if there are more commits.
        // works unless commits.size() represents the exact end.
        hasMore = changes.size() >= changesPerPage;
        hasChanges = changes.size() > 0;
        setup(changes);
        // determine to show pager, more, or neither
        if (limit <= 0) {
            // no display limit
            add(new Label("moreChanges").setVisible(false));
        } else {
            if (pageResults) {
                // paging
                add(new Label("moreChanges").setVisible(false));
            } else {
                // more
                if (changes.size() == limit) {
                    // show more
                    add(new LinkPanel("moreChanges", "link", new StringResourceModel("gb.moreChanges",
                            this, null), ReflogPage.class,
                            WicketUtils.newRepositoryParameter(model.name)));
                } else {
                    // no more
                    add(new Label("moreChanges").setVisible(false));
                }
            }
        }
    }
    public ReflogPanel(String wicketId, List<RefLogEntry> changes) {
        super(wicketId);
        hasChanges = changes.size() > 0;
        setup(changes);
        add(new Label("moreChanges").setVisible(false));
    }
    protected void setup(List<RefLogEntry> changes) {
        final int hashLen = GitBlit.getInteger(Keys.web.shortCommitIdLength, 6);
        String dateFormat = GitBlit.getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
        final TimeZone timezone = getTimeZone();
        final DateFormat df = new SimpleDateFormat(dateFormat);
        df.setTimeZone(timezone);
        final Calendar cal = Calendar.getInstance(timezone);
        ListDataProvider<RefLogEntry> dp = new ListDataProvider<RefLogEntry>(changes);
        DataView<RefLogEntry> changeView = new DataView<RefLogEntry>("change", dp) {
            private static final long serialVersionUID = 1L;
            public void populateItem(final Item<RefLogEntry> changeItem) {
                final RefLogEntry change = changeItem.getModelObject();
                String fullRefName = change.getChangedRefs().get(0);
                String shortRefName = fullRefName;
                boolean isTag = false;
                if (shortRefName.startsWith(Constants.R_HEADS)) {
                    shortRefName = shortRefName.substring(Constants.R_HEADS.length());
                } else if (shortRefName.startsWith(Constants.R_TAGS)) {
                    shortRefName = shortRefName.substring(Constants.R_TAGS.length());
                    isTag = true;
                }
                String fuzzydate;
                TimeUtils tu = getTimeUtils();
                Date changeDate = change.date;
                if (TimeUtils.isToday(changeDate, timezone)) {
                    fuzzydate = tu.today();
                } else if (TimeUtils.isYesterday(changeDate, timezone)) {
                    fuzzydate = tu.yesterday();
                } else {
                    // calculate a fuzzy time ago date
                    cal.setTime(changeDate);
                    cal.set(Calendar.HOUR_OF_DAY, 0);
                    cal.set(Calendar.MINUTE, 0);
                    cal.set(Calendar.SECOND, 0);
                    cal.set(Calendar.MILLISECOND, 0);
                    changeDate = cal.getTime();
                    fuzzydate = getTimeUtils().timeAgo(changeDate);
                }
                changeItem.add(new Label("whenChanged", fuzzydate + ", " + df.format(changeDate)));
                Label changeIcon = new Label("changeIcon");
                if (isTag) {
                    WicketUtils.setCssClass(changeIcon, "iconic-tag");
                } else {
                    WicketUtils.setCssClass(changeIcon, "iconic-upload");
                }
                changeItem.add(changeIcon);
                if (change.user.username.equals(change.user.emailAddress) && change.user.emailAddress.indexOf('@') > -1) {
                    // username is an email address - 1.2.1 push log bug
                    changeItem.add(new Label("whoChanged", change.user.getDisplayName()));
                } else {
                    // link to user account page
                    changeItem.add(new LinkPanel("whoChanged", null, change.user.getDisplayName(),
                            UserPage.class, WicketUtils.newUsernameParameter(change.user.username)));
                }
                boolean isDelete = false;
                boolean isRewind = false;
                String what;
                String by = null;
                switch(change.getChangeType(fullRefName)) {
                case CREATE:
                    if (isTag) {
                        // new tag
                        what = getString("gb.pushedNewTag");
                    } else {
                        // new branch
                        what = getString("gb.pushedNewBranch");
                    }
                    break;
                case DELETE:
                    isDelete = true;
                    if (isTag) {
                        what = getString("gb.deletedTag");
                    } else {
                        what = getString("gb.deletedBranch");
                    }
                    break;
                case UPDATE_NONFASTFORWARD:
                    isRewind = true;
                default:
                    what = MessageFormat.format(change.getCommitCount() > 1 ? getString("gb.pushedNCommitsTo") : getString("gb.pushedOneCommitTo") , change.getCommitCount());
                    if (change.getAuthorCount() == 1) {
                        by = MessageFormat.format(getString("gb.byOneAuthor"), change.getAuthorIdent().getName());
                    } else {
                        by = MessageFormat.format(getString("gb.byNAuthors"), change.getAuthorCount());
                    }
                    break;
                }
                changeItem.add(new Label("whatChanged", what));
                changeItem.add(new Label("byAuthors", by).setVisible(!StringUtils.isEmpty(by)));
                changeItem.add(new Label("refRewind", getString("gb.rewind")).setVisible(isRewind));
                if (isDelete) {
                    // can't link to deleted ref
                    changeItem.add(new Label("refChanged", shortRefName));
                } else if (isTag) {
                    // link to tag
                    changeItem.add(new LinkPanel("refChanged", null, shortRefName,
                            TagPage.class, WicketUtils.newObjectParameter(change.repository, fullRefName)));
                } else {
                    // link to tree
                    changeItem.add(new LinkPanel("refChanged", null, shortRefName,
                        TreePage.class, WicketUtils.newObjectParameter(change.repository, fullRefName)));
                }
                int maxCommitCount = 5;
                List<RepositoryCommit> commits = change.getCommits();
                if (commits.size() > maxCommitCount) {
                    commits = new ArrayList<RepositoryCommit>(commits.subList(0,  maxCommitCount));
                }
                // compare link
                String compareLinkText = null;
                if ((change.getCommitCount() <= maxCommitCount) && (change.getCommitCount() > 1)) {
                    compareLinkText = MessageFormat.format(getString("gb.viewComparison"), commits.size());
                } else if (change.getCommitCount() > maxCommitCount) {
                    int diff = change.getCommitCount() - maxCommitCount;
                    compareLinkText = MessageFormat.format(diff > 1 ? getString("gb.nMoreCommits") : getString("gb.oneMoreCommit"), diff);
                }
                if (StringUtils.isEmpty(compareLinkText)) {
                    changeItem.add(new Label("compareLink").setVisible(false));
                } else {
                    String endRangeId = change.getNewId(fullRefName);
                    String startRangeId = change.getOldId(fullRefName);
                    changeItem.add(new LinkPanel("compareLink", null, compareLinkText, ComparePage.class, WicketUtils.newRangeParameter(change.repository, startRangeId, endRangeId)));
                }
                ListDataProvider<RepositoryCommit> cdp = new ListDataProvider<RepositoryCommit>(commits);
                DataView<RepositoryCommit> commitsView = new DataView<RepositoryCommit>("commit", cdp) {
                    private static final long serialVersionUID = 1L;
                    public void populateItem(final Item<RepositoryCommit> commitItem) {
                        final RepositoryCommit commit = commitItem.getModelObject();
                        // author gravatar
                        commitItem.add(new GravatarImage("commitAuthor", commit.getAuthorIdent().getName(),
                                commit.getAuthorIdent().getEmailAddress(), null, 16, false, false));
                        // merge icon
                        if (commit.getParentCount() > 1) {
                            commitItem.add(WicketUtils.newImage("commitIcon", "commit_merge_16x16.png"));
                        } else {
                            commitItem.add(WicketUtils.newBlankImage("commitIcon"));
                        }
                        // short message
                        String shortMessage = commit.getShortMessage();
                        String trimmedMessage = shortMessage;
                        if (commit.getRefs() != null && commit.getRefs().size() > 0) {
                            trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
                        } else {
                            trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
                        }
                        LinkPanel shortlog = new LinkPanel("commitShortMessage", "list",
                                trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
                                        change.repository, commit.getName()));
                        if (!shortMessage.equals(trimmedMessage)) {
                            WicketUtils.setHtmlTooltip(shortlog, shortMessage);
                        }
                        commitItem.add(shortlog);
                        // commit hash link
                        LinkPanel commitHash = new LinkPanel("hashLink", null, commit.getName().substring(0, hashLen),
                                CommitPage.class, WicketUtils.newObjectParameter(
                                        change.repository, commit.getName()));
                        WicketUtils.setCssClass(commitHash, "shortsha1");
                        WicketUtils.setHtmlTooltip(commitHash, commit.getName());
                        commitItem.add(commitHash);
                    }
                };
                changeItem.add(commitsView);
            }
        };
        add(changeView);
    }
    public boolean hasMore() {
        return hasMore;
    }
    public boolean hideIfEmpty() {
        setVisible(hasChanges);
        return hasChanges;
    }
}
src/main/resources/gitblit.css
@@ -129,23 +129,39 @@
    background-color: #002060;
}
div.push {
div.reflog {
    border-bottom: 1px solid #ddd;
    margin-bottom: 5px;
    padding-bottom: 5px;
}
div.push .icon {
div.reflog .icon {
    font-size: 42px;
    line-height: 42px;
}
div.push i {
div.reflog .when {
    color: #aaa;
}
div.reflog i {
    font-size: 42px;
    color: #bbb;
    vertical-align: middle;
}
div.dashboardTitle {
    font-size: 1.75em;
    padding-bottom: 5px;
    margin-bottom: 10px;
    border-bottom: 1px solid #ccc;
}
div.dashboardTitle small {
    color: #888;
    font-size: 0.7em;
}
.repositorynavbar {
    background-color: #fbfbfb;
    border-bottom: 1px solid #ccc;
src/test/java/com/gitblit/tests/GitServletTest.java
@@ -38,12 +38,12 @@
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.PushLogEntry;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.utils.RefLogUtils;
public class GitServletTest {
@@ -788,7 +788,7 @@
        String name = "refchecks/ticgit.git";
        File refChecks = new File(GitBlitSuite.REPOSITORIES, name);
        Repository repository = new FileRepositoryBuilder().setGitDir(refChecks).build();
        List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
        List<RefLogEntry> pushes = RefLogUtils.getRefLog(name, repository);
        GitBlitSuite.close(repository);
        assertTrue("Repository has an empty push log!", pushes.size() > 0);
    }
src/test/java/com/gitblit/tests/PushLogTest.java
@@ -25,8 +25,8 @@
import org.eclipse.jgit.util.FS;
import org.junit.Test;
import com.gitblit.models.PushLogEntry;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.models.RefLogEntry;
import com.gitblit.utils.RefLogUtils;
public class PushLogTest {
@@ -35,7 +35,7 @@
        String name = "~james/helloworld.git";
        File gitDir = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, name), FS.DETECTED);
        Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build();
        List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
        List<RefLogEntry> pushes = RefLogUtils.getRefLog(name, repository);
        GitBlitSuite.close(repository);
    }
}