From f8bb95d50ad925ab16a0a167bc553f036434a2d7 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sat, 05 Jan 2013 15:04:40 -0500
Subject: [PATCH] Draft mechanism to record a push log as a hidden orphan branch

---
 src/com/gitblit/utils/PushLogUtils.java          |  344 ++++++++++++++++++++++
 src/com/gitblit/wicket/panels/RefsPanel.java     |    6 
 src/com/gitblit/models/Activity.java             |   83 -----
 src/com/gitblit/models/RepositoryCommit.java     |  104 ++++++
 src/com/gitblit/GitServlet.java                  |   55 +++
 tests/com/gitblit/tests/GitServletTest.java      |   15 +
 src/com/gitblit/utils/IssueUtils.java            |   16 
 tests/com/gitblit/tests/PushLogTest.java         |   37 ++
 src/com/gitblit/wicket/panels/ActivityPanel.java |    2 
 src/com/gitblit/utils/ActivityUtils.java         |    2 
 src/com/gitblit/models/PushLogEntry.java         |  166 +++++++++++
 src/com/gitblit/Constants.java                   |    2 
 src/com/gitblit/utils/JGitUtils.java             |   14 
 13 files changed, 751 insertions(+), 95 deletions(-)

diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index ca33269..4dd1471 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -88,6 +88,8 @@
 	
 	public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ssZ";
 	
+	public static final String R_GITBLIT = "refs/gitblit/";
+	
 	public static String getGitBlitVersion() {
 		return NAME + " v" + VERSION;
 	}
diff --git a/src/com/gitblit/GitServlet.java b/src/com/gitblit/GitServlet.java
index 94a51be..05f38b9 100644
--- a/src/com/gitblit/GitServlet.java
+++ b/src/com/gitblit/GitServlet.java
@@ -29,6 +29,7 @@
 import java.util.Enumeration;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.servlet.ServletConfig;
@@ -37,7 +38,9 @@
 import javax.servlet.http.HttpServletRequest;
 
 import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
+import org.eclipse.jgit.http.server.resolver.DefaultUploadPackFactory;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PostReceiveHook;
@@ -45,6 +48,8 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.RefFilter;
+import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.slf4j.Logger;
@@ -55,7 +60,9 @@
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ClientLogger;
 import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.IssueUtils;
 import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.PushLogUtils;
 import com.gitblit.utils.StringUtils;
 
 /**
@@ -129,6 +136,35 @@
 				}
 				
 				return rp;
+			}
+		});
+		
+		// override the default upload pack to exclude gitblit refs
+		setUploadPackFactory(new DefaultUploadPackFactory() {
+			@Override
+			public UploadPack create(final HttpServletRequest req, final Repository db)
+					throws ServiceNotEnabledException, ServiceNotAuthorizedException {
+				UploadPack up = super.create(req, db);
+				RefFilter refFilter = new RefFilter() {
+					@Override
+					public Map<String, Ref> filter(Map<String, Ref> refs) {
+						// admin accounts can access all refs 
+						UserModel user = GitBlit.self().authenticate(req);
+						if (user == null) {
+							user = UserModel.ANONYMOUS;
+						}
+						if (user.canAdmin()) {
+							return refs;
+						}
+
+						// normal users can not clone gitblit refs
+						refs.remove(IssueUtils.GB_ISSUES);
+						refs.remove(PushLogUtils.GB_PUSHES);
+						return refs;
+					}
+				};
+				up.setRefFilter(refFilter);
+				return up;
 			}
 		});
 		super.init(new GitblitServletConfig(config));
@@ -264,12 +300,11 @@
 				logger.info("skipping post-receive hooks, no refs created, updated, or removed");
 				return;
 			}
-			RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
-			Set<String> scripts = new LinkedHashSet<String>();
-			scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
-			scripts.addAll(repository.postReceiveScripts);
+
 			UserModel user = getUserModel(rp);
-			runGroovy(repository, user, commands, rp, scripts);
+			RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
+
+			// log ref changes
 			for (ReceiveCommand cmd : commands) {
 				if (Result.OK.equals(cmd.getResult())) {
 					// add some logging for important ref changes
@@ -288,6 +323,16 @@
 					}
 				}
 			}
+
+			// update push log
+			PushLogUtils.updatePushLog(user, rp.getRepository(), commands);
+			logger.info(MessageFormat.format("{0} push log updated", repository.name));
+			
+			// run Groovy hook scripts 
+			Set<String> scripts = new LinkedHashSet<String>();
+			scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
+			scripts.addAll(repository.postReceiveScripts);
+			runGroovy(repository, user, commands, rp, scripts);
 			
 			// Experimental
 			// runNativeScript(rp, "hooks/post-receive", commands);
diff --git a/src/com/gitblit/models/Activity.java b/src/com/gitblit/models/Activity.java
index 7e0cb4b..59405c7 100644
--- a/src/com/gitblit/models/Activity.java
+++ b/src/com/gitblit/models/Activity.java
@@ -25,7 +25,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import com.gitblit.utils.StringUtils;
@@ -126,87 +125,5 @@
 	public int compareTo(Activity o) {
 		// reverse chronological order
 		return o.startDate.compareTo(startDate);
-	}
-
-	/**
-	 * Model class to represent a RevCommit, it's source repository, and the
-	 * branch. This class is used by the activity page.
-	 * 
-	 * @author James Moger
-	 */
-	public static class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
-
-		private static final long serialVersionUID = 1L;
-
-		public final String repository;
-
-		public final String branch;
-
-		private final RevCommit commit;
-
-		private List<RefModel> refs;
-
-		public RepositoryCommit(String repository, String branch, RevCommit commit) {
-			this.repository = repository;
-			this.branch = branch;
-			this.commit = commit;
-		}
-
-		public void setRefs(List<RefModel> refs) {
-			this.refs = refs;
-		}
-
-		public List<RefModel> getRefs() {
-			return refs;
-		}
-
-		public String getName() {
-			return commit.getName();
-		}
-
-		public String getShortName() {
-			return commit.getName().substring(0, 8);
-		}
-
-		public String getShortMessage() {
-			return commit.getShortMessage();
-		}
-
-		public int getParentCount() {
-			return commit.getParentCount();
-		}
-
-		public PersonIdent getAuthorIdent() {
-			return commit.getAuthorIdent();
-		}
-
-		public PersonIdent getCommitterIdent() {
-			return commit.getCommitterIdent();
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (o instanceof RepositoryCommit) {
-				RepositoryCommit commit = (RepositoryCommit) o;
-				return repository.equals(commit.repository) && getName().equals(commit.getName());
-			}
-			return false;
-		}
-		
-		@Override
-		public int hashCode() {
-			return (repository + commit).hashCode();
-		}
-
-		@Override
-		public int compareTo(RepositoryCommit o) {
-			// reverse-chronological order
-			if (commit.getCommitTime() > o.commit.getCommitTime()) {
-				return -1;
-			} else if (commit.getCommitTime() < o.commit.getCommitTime()) {
-				return 1;
-			}
-			return 0;
-		}
 	}
 }
diff --git a/src/com/gitblit/models/PushLogEntry.java b/src/com/gitblit/models/PushLogEntry.java
new file mode 100644
index 0000000..32a7d00
--- /dev/null
+++ b/src/com/gitblit/models/PushLogEntry.java
@@ -0,0 +1,166 @@
+/*
+ * 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.models;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Model class to represent a push into a repository.
+ * 
+ * @author James Moger
+ */
+public class PushLogEntry implements Serializable, Comparable<PushLogEntry> {
+
+	private static final long serialVersionUID = 1L;
+
+	public final String repository;
+	
+	public final Date date;
+	
+	public final UserModel user;
+
+	private final Set<RepositoryCommit> commits;
+
+	/**
+	 * Constructor for specified duration of push from start date.
+	 * 
+	 * @param repository
+	 *            the repository that received the push
+	 * @param date
+	 *            the date of the push
+	 * @param user
+	 *            the user who pushed
+	 */
+	public PushLogEntry(String repository, Date date, UserModel user) {
+		this.repository = repository;
+		this.date = date;
+		this.user = user;
+		this.commits = new LinkedHashSet<RepositoryCommit>();
+	}
+
+	/**
+	 * Adds a commit to the push entry object as long as the commit is not a
+	 * duplicate.
+	 * 
+	 * @param branch
+	 * @param commit
+	 * @return a RepositoryCommit, if one was added. Null if this is duplicate
+	 *         commit
+	 */
+	public RepositoryCommit addCommit(String branch, RevCommit commit) {
+		RepositoryCommit commitModel = new RepositoryCommit(repository, branch, commit);
+		if (commits.add(commitModel)) {
+			return commitModel;
+		}
+		return null;
+	}
+	
+	/**
+	 * Returns the list of branches changed by the push.
+	 * 
+	 * @return a list of branches
+	 */
+	public List<String> getChangedBranches() {
+		return getChangedRefs(Constants.R_HEADS);
+	}
+	
+	/**
+	 * Returns the list of tags changed by the push.
+	 * 
+	 * @return a list of tags
+	 */
+	public List<String> getChangedTags() {
+		return getChangedRefs(Constants.R_TAGS);
+	}
+
+	/**
+	 * Gets the changed refs in the push.
+	 * 
+	 * @param baseRef
+	 * @return the changed refs
+	 */
+	protected List<String> getChangedRefs(String baseRef) {
+		Set<String> refs = new HashSet<String>();
+		for (RepositoryCommit commit : commits) {
+			if (baseRef == null || commit.branch.startsWith(baseRef)) {
+				refs.add(commit.branch);
+			}
+		}
+		List<String> list = new ArrayList<String>(refs);
+		Collections.sort(list);
+		return list;
+	}
+	
+	/**
+	 * The total number of commits in the push.
+	 * 
+	 * @return the number of commits in the push
+	 */
+	public int getCommitCount() {
+		return commits.size();
+	}
+	
+	/**
+	 * Returns all commits in the push.
+	 * 
+	 * @return a list of commits
+	 */
+	public List<RepositoryCommit> getCommits() {
+		List<RepositoryCommit> list = new ArrayList<RepositoryCommit>(commits);
+		Collections.sort(list);
+		return list;
+	}
+	
+	/**
+	 * Returns all commits that belong to a particular ref
+	 * 
+	 * @param ref
+	 * @return a list of commits
+	 */
+	public List<RepositoryCommit> getCommits(String ref) {
+		List<RepositoryCommit> list = new ArrayList<RepositoryCommit>();
+		for (RepositoryCommit commit : commits) {
+			if (commit.branch.equals(ref)) {
+				list.add(commit);
+			}
+		}
+		Collections.sort(list);
+		return list;
+	}
+
+	@Override
+	public int compareTo(PushLogEntry o) {
+		// reverse chronological order
+		return o.date.compareTo(date);
+	}
+	
+	@Override
+	public String toString() {
+		return MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1} pushed {2,number,0} commit{3} to {4} ",
+				date, user.getDisplayName(), commits.size(), commits.size() == 1 ? "":"s", repository);
+	}
+}
diff --git a/src/com/gitblit/models/RepositoryCommit.java b/src/com/gitblit/models/RepositoryCommit.java
new file mode 100644
index 0000000..3a98f61
--- /dev/null
+++ b/src/com/gitblit/models/RepositoryCommit.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2011 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.models;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Model class to represent a RevCommit, it's source repository, and the branch.
+ * This class is used by the activity page.
+ * 
+ * @author James Moger
+ */
+public class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
+
+	private static final long serialVersionUID = 1L;
+
+	public final String repository;
+
+	public final String branch;
+
+	private final RevCommit commit;
+
+	private List<RefModel> refs;
+
+	public RepositoryCommit(String repository, String branch, RevCommit commit) {
+		this.repository = repository;
+		this.branch = branch;
+		this.commit = commit;
+	}
+
+	public void setRefs(List<RefModel> refs) {
+		this.refs = refs;
+	}
+
+	public List<RefModel> getRefs() {
+		return refs;
+	}
+
+	public String getName() {
+		return commit.getName();
+	}
+
+	public String getShortName() {
+		return commit.getName().substring(0, 8);
+	}
+
+	public String getShortMessage() {
+		return commit.getShortMessage();
+	}
+
+	public int getParentCount() {
+		return commit.getParentCount();
+	}
+
+	public PersonIdent getAuthorIdent() {
+		return commit.getAuthorIdent();
+	}
+
+	public PersonIdent getCommitterIdent() {
+		return commit.getCommitterIdent();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (o instanceof RepositoryCommit) {
+			RepositoryCommit commit = (RepositoryCommit) o;
+			return repository.equals(commit.repository) && getName().equals(commit.getName());
+		}
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return (repository + commit).hashCode();
+	}
+
+	@Override
+	public int compareTo(RepositoryCommit o) {
+		// reverse-chronological order
+		if (commit.getCommitTime() > o.commit.getCommitTime()) {
+			return -1;
+		} else if (commit.getCommitTime() < o.commit.getCommitTime()) {
+			return 1;
+		}
+		return 0;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/utils/ActivityUtils.java b/src/com/gitblit/utils/ActivityUtils.java
index ef3a55e..732fdeb 100644
--- a/src/com/gitblit/utils/ActivityUtils.java
+++ b/src/com/gitblit/utils/ActivityUtils.java
@@ -36,9 +36,9 @@
 
 import com.gitblit.GitBlit;
 import com.gitblit.models.Activity;
-import com.gitblit.models.Activity.RepositoryCommit;
 import com.gitblit.models.GravatarProfile;
 import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
 import com.gitblit.models.RepositoryModel;
 import com.google.gson.reflect.TypeToken;
 
diff --git a/src/com/gitblit/utils/IssueUtils.java b/src/com/gitblit/utils/IssueUtils.java
index 7b24ccf..1b90c7d 100644
--- a/src/com/gitblit/utils/IssueUtils.java
+++ b/src/com/gitblit/utils/IssueUtils.java
@@ -76,9 +76,9 @@
 		public abstract boolean accept(IssueModel issue);
 	}
 
-	public static final String GB_ISSUES = "refs/heads/gb-issues";
+	public static final String GB_ISSUES = "refs/gitblit/issues";
 
-	static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
+	static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.class);
 
 	/**
 	 * Log an error message and exception.
@@ -111,7 +111,13 @@
 	 * @return a refmodel for the gb-issues branch or null
 	 */
 	public static RefModel getIssuesBranch(Repository repository) {
-		return JGitUtils.getBranch(repository, "gb-issues");
+		List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+		for (RefModel ref : refs) {
+			if (ref.reference.getName().equals(GB_ISSUES)) {
+				return ref;
+			}
+		}
+		return null;
 	}
 
 	/**
@@ -394,7 +400,7 @@
 	public static IssueModel createIssue(Repository repository, Change change) {
 		RefModel issuesBranch = getIssuesBranch(repository);
 		if (issuesBranch == null) {
-			JGitUtils.createOrphanBranch(repository, "gb-issues", null);
+			JGitUtils.createOrphanBranch(repository, GB_ISSUES, null);
 		}
 
 		if (StringUtils.isEmpty(change.author)) {
@@ -471,7 +477,7 @@
 		RefModel issuesBranch = getIssuesBranch(repository);
 
 		if (issuesBranch == null) {
-			throw new RuntimeException("gb-issues branch does not exist!");
+			throw new RuntimeException(GB_ISSUES + " does not exist!");
 		}
 
 		if (StringUtils.isEmpty(issueId)) {
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index beaa27d..099036e 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -1457,6 +1457,20 @@
 			int maxCount) {
 		return getRefs(repository, Constants.R_NOTES, fullName, maxCount);
 	}
+	
+	/**
+	 * Returns the list of refs in the specified base ref. If repository does 
+	 * not exist or is empty, an empty list is returned.
+	 * 
+	 * @param repository
+	 * @param fullName
+	 *            if true, /refs/yadayadayada is returned. If false,
+	 *            yadayadayada is returned.
+	 * @return list of refs
+	 */
+	public static List<RefModel> getRefs(Repository repository, String baseRef) {
+		return getRefs(repository, baseRef, true, -1);
+	}
 
 	/**
 	 * Returns a list of references in the repository matching "refs". If the
diff --git a/src/com/gitblit/utils/PushLogUtils.java b/src/com/gitblit/utils/PushLogUtils.java
new file mode 100644
index 0000000..a3b1d66
--- /dev/null
+++ b/src/com/gitblit/utils/PushLogUtils.java
@@ -0,0 +1,344 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.PushLogEntry;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * Utility class for maintaining a pushlog within a git repository on an
+ * orphan branch.
+ * 
+ * @author James Moger
+ *
+ */
+public class PushLogUtils {
+	
+	public static final String GB_PUSHES = "refs/gitblit/pushes";
+
+	static final Logger LOGGER = LoggerFactory.getLogger(PushLogUtils.class);
+
+	/**
+	 * Log an error message and exception.
+	 * 
+	 * @param t
+	 * @param repository
+	 *            if repository is not null it MUST be the {0} parameter in the
+	 *            pattern.
+	 * @param pattern
+	 * @param objects
+	 */
+	private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+		List<Object> parameters = new ArrayList<Object>();
+		if (objects != null && objects.length > 0) {
+			for (Object o : objects) {
+				parameters.add(o);
+			}
+		}
+		if (repository != null) {
+			parameters.add(0, repository.getDirectory().getAbsolutePath());
+		}
+		LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+	}
+
+	/**
+	 * Returns a RefModel for the gb-pushes 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
+	 */
+	public static RefModel getPushLogBranch(Repository repository) {
+		List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+		for (RefModel ref : refs) {
+			if (ref.reference.getName().equals(GB_PUSHES)) {
+				return ref;
+			}
+		}
+		return null;
+	}
+	
+	/**
+	 * Updates a push log.
+	 * 
+	 * @param user
+	 * @param repository
+	 * @param commands
+	 * @return true, if the update was successful
+	 */
+	public static boolean updatePushLog(UserModel user, Repository repository,
+			Collection<ReceiveCommand> commands) {
+		RefModel pushlogBranch = getPushLogBranch(repository);
+		if (pushlogBranch == null) {
+			JGitUtils.createOrphanBranch(repository, GB_PUSHES, null);
+		}
+		
+		boolean success = false;
+		String message = "push";
+		
+		try {
+			ObjectId headId = repository.resolve(GB_PUSHES + "^{commit}");
+			ObjectInserter odi = repository.newObjectInserter();
+			try {
+				// Create the in-memory index of the push log entry
+				DirCache index = createIndex(repository, headId, commands);
+				ObjectId indexTreeId = index.writeTree(odi);
+
+				PersonIdent ident = new PersonIdent(user.getDisplayName(), 
+						user.emailAddress == null ? user.username:user.emailAddress);
+
+				// Create a commit object
+				CommitBuilder commit = new CommitBuilder();
+				commit.setAuthor(ident);
+				commit.setCommitter(ident);
+				commit.setEncoding(Constants.CHARACTER_ENCODING);
+				commit.setMessage(message);
+				commit.setParentId(headId);
+				commit.setTreeId(indexTreeId);
+
+				// Insert the commit into the repository
+				ObjectId commitId = odi.insert(commit);
+				odi.flush();
+
+				RevWalk revWalk = new RevWalk(repository);
+				try {
+					RevCommit revCommit = revWalk.parseCommit(commitId);
+					RefUpdate ru = repository.updateRef(GB_PUSHES);
+					ru.setNewObjectId(commitId);
+					ru.setExpectedOldObjectId(headId);
+					ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+					Result rc = ru.forceUpdate();
+					switch (rc) {
+					case NEW:
+					case FORCED:
+					case FAST_FORWARD:
+						success = true;
+						break;
+					case REJECTED:
+					case LOCK_FAILURE:
+						throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
+								ru.getRef(), rc);
+					default:
+						throw new JGitInternalException(MessageFormat.format(
+								JGitText.get().updatingRefFailed, GB_PUSHES, commitId.toString(),
+								rc));
+					}
+				} finally {
+					revWalk.release();
+				}
+			} finally {
+				odi.release();
+			}
+		} catch (Throwable t) {
+			error(t, repository, "Failed to commit pushlog entry to {0}");
+		}
+		return success;
+	}
+	
+	/**
+	 * Creates an in-memory index of the push log entry.
+	 * 
+	 * @param repo
+	 * @param headId
+	 * @param commands
+	 * @return an in-memory index
+	 * @throws IOException
+	 */
+	private static DirCache createIndex(Repository repo, ObjectId headId, 
+			Collection<ReceiveCommand> commands) throws IOException {
+
+		DirCache inCoreIndex = DirCache.newInCore();
+		DirCacheBuilder dcBuilder = inCoreIndex.builder();
+		ObjectInserter inserter = repo.newObjectInserter();
+
+		long now = System.currentTimeMillis();
+		Set<String> ignorePaths = new TreeSet<String>();
+		try {
+			// add receive commands to the temporary index
+			for (ReceiveCommand command : commands) {
+				// use the ref names as the path names
+				String path = command.getRefName();
+				ignorePaths.add(path);
+
+				StringBuilder change = new StringBuilder();
+				change.append(command.getType().name()).append(' ');
+				switch (command.getType()) {
+				case CREATE:
+					change.append(ObjectId.zeroId().getName());
+					change.append(' ');
+					change.append(command.getNewId().getName());
+					break;
+				case UPDATE:
+				case UPDATE_NONFASTFORWARD:
+					change.append(command.getOldId().getName());
+					change.append(' ');
+					change.append(command.getNewId().getName());
+					break;
+				case DELETE:
+					change = null;
+					break;
+				}
+				if (change == null) {
+					// ref deleted
+					continue;
+				}
+				String content = change.toString();
+				
+				// create an index entry for this attachment
+				final DirCacheEntry dcEntry = new DirCacheEntry(path);
+				dcEntry.setLength(content.length());
+				dcEntry.setLastModified(now);
+				dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+				// insert object
+				dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")));
+
+				// add to temporary in-core index
+				dcBuilder.add(dcEntry);
+			}
+
+			// Traverse HEAD to add all other paths
+			TreeWalk treeWalk = new TreeWalk(repo);
+			int hIdx = -1;
+			if (headId != null)
+				hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+			treeWalk.setRecursive(true);
+
+			while (treeWalk.next()) {
+				String path = treeWalk.getPathString();
+				CanonicalTreeParser hTree = null;
+				if (hIdx != -1)
+					hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+				if (!ignorePaths.contains(path)) {
+					// add entries from HEAD for all other paths
+					if (hTree != null) {
+						// create a new DirCacheEntry with data retrieved from
+						// HEAD
+						final DirCacheEntry dcEntry = new DirCacheEntry(path);
+						dcEntry.setObjectId(hTree.getEntryObjectId());
+						dcEntry.setFileMode(hTree.getEntryFileMode());
+
+						// add to temporary in-core index
+						dcBuilder.add(dcEntry);
+					}
+				}
+			}
+
+			// release the treewalk
+			treeWalk.release();
+
+			// finish temporary in-core index used for this commit
+			dcBuilder.finish();
+		} finally {
+			inserter.release();
+		}
+		return inCoreIndex;
+	}
+
+	public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository) {
+		return getPushLog(repositoryName, repository, null, -1);
+	}
+
+	public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int maxCount) {
+		return getPushLog(repositoryName, repository, null, maxCount);
+	}
+
+	public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate) {
+		return getPushLog(repositoryName, repository, minimumDate, -1);
+	}
+	
+	public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate, int maxCount) {
+		List<PushLogEntry> list = new ArrayList<PushLogEntry>();
+		RefModel ref = getPushLogBranch(repository);
+		if (ref == null) {
+			return list;
+		}
+		List<RevCommit> pushes;
+		if (minimumDate == null) {
+			pushes = JGitUtils.getRevLog(repository, GB_PUSHES, 0, maxCount);
+		} else {
+			pushes = JGitUtils.getRevLog(repository, GB_PUSHES, minimumDate);
+		}
+		for (RevCommit push : pushes) {
+			if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
+				// skip gitblit/internal commits
+				continue;
+			}
+			Date date = push.getAuthorIdent().getWhen();
+			UserModel user = new UserModel(push.getAuthorIdent().getEmailAddress());
+			user.displayName = push.getAuthorIdent().getName();
+			PushLogEntry log = new PushLogEntry(repositoryName, date, user);
+			list.add(log);
+			List<PathChangeModel> changedRefs = JGitUtils.getFilesInCommit(repository, push);
+			for (PathChangeModel change : changedRefs) {
+				switch (change.changeType) {
+				case DELETE:
+					break;
+				case ADD:
+				case MODIFY:
+					String content = JGitUtils.getStringContent(repository, push.getTree(), change.path);
+					String [] fields = content.split(" ");
+					String oldId = fields[1];
+					String newId = fields[2];
+					List<RevCommit> pushedCommits = JGitUtils.getRevLog(repository, oldId, newId);
+					for (RevCommit pushedCommit : pushedCommits) {
+						log.addCommit(change.path, pushedCommit);
+					}
+					break;
+				default:
+					break;
+				}
+			}
+		}
+		Collections.sort(list);
+		return list;
+	}
+}
diff --git a/src/com/gitblit/wicket/panels/ActivityPanel.java b/src/com/gitblit/wicket/panels/ActivityPanel.java
index 6caee3e..669c36b 100644
--- a/src/com/gitblit/wicket/panels/ActivityPanel.java
+++ b/src/com/gitblit/wicket/panels/ActivityPanel.java
@@ -27,7 +27,7 @@
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
 import com.gitblit.models.Activity;
-import com.gitblit.models.Activity.RepositoryCommit;
+import com.gitblit.models.RepositoryCommit;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.pages.CommitDiffPage;
diff --git a/src/com/gitblit/wicket/panels/RefsPanel.java b/src/com/gitblit/wicket/panels/RefsPanel.java
index b467642..3ba22c0 100644
--- a/src/com/gitblit/wicket/panels/RefsPanel.java
+++ b/src/com/gitblit/wicket/panels/RefsPanel.java
@@ -129,8 +129,14 @@
 					name = name.substring(Constants.R_TAGS.length());
 					cssClass = "tagRef";
 				} else if (name.startsWith(Constants.R_NOTES)) {
+					// codereview refs
 					linkClass = CommitPage.class;
 					cssClass = "otherRef";
+				} else if (name.startsWith(com.gitblit.Constants.R_GITBLIT)) {
+					// gitblit refs
+					linkClass = LogPage.class;
+					cssClass = "otherRef";
+					name = name.substring(com.gitblit.Constants.R_GITBLIT.length());
 				}
 
 				Component c = new LinkPanel("refName", null, name, linkClass,
diff --git a/tests/com/gitblit/tests/GitServletTest.java b/tests/com/gitblit/tests/GitServletTest.java
index 7dce07d..771c4b9 100644
--- a/tests/com/gitblit/tests/GitServletTest.java
+++ b/tests/com/gitblit/tests/GitServletTest.java
@@ -7,9 +7,11 @@
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.text.MessageFormat;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.jgit.api.CloneCommand;
@@ -18,6 +20,7 @@
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.file.FileRepository;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
@@ -34,9 +37,11 @@
 import com.gitblit.Constants.AuthorizationControl;
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
+import com.gitblit.models.PushLogEntry;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.PushLogUtils;
 
 public class GitServletTest {
 
@@ -756,4 +761,14 @@
 		GitBlitSuite.close(git);
 		GitBlit.self().deleteUser(user.username);
 	}
+	
+	@Test
+	public void testPushLog() throws IOException {
+		String name = "refchecks/ticgit.git";
+		File refChecks = new File(GitBlitSuite.REPOSITORIES, name);
+		FileRepository repository = new FileRepository(refChecks);
+		List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
+		GitBlitSuite.close(repository);
+		assertTrue("Repository has an empty push log!", pushes.size() > 0);
+	}
 }
diff --git a/tests/com/gitblit/tests/PushLogTest.java b/tests/com/gitblit/tests/PushLogTest.java
new file mode 100644
index 0000000..aa4cf41
--- /dev/null
+++ b/tests/com/gitblit/tests/PushLogTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.tests;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.junit.Test;
+
+import com.gitblit.models.PushLogEntry;
+import com.gitblit.utils.PushLogUtils;
+
+public class PushLogTest {
+
+	@Test
+	public void testPushLog() throws IOException {
+		String name = "~james/helloworld.git";
+		FileRepository repository = new FileRepository(new File(GitBlitSuite.REPOSITORIES, name));
+		List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
+		GitBlitSuite.close(repository);
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1