From a125cf6876e0edc5a2498df57a9df06d60b1f572 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 02 Jun 2011 22:40:23 -0400
Subject: [PATCH] Unit testing. Start of git-notes display feature.

---
 src/com/gitblit/utils/StringUtils.java               |    8 
 src/com/gitblit/wicket/pages/EditRepositoryPage.java |   19 
 tests/com/gitblit/tests/MarkdownUtilsTest.java       |   36 ++
 src/com/gitblit/utils/MarkdownUtils.java             |   21 
 tests/com/gitblit/tests/DiffUtilsTest.java           |    2 
 src/com/gitblit/wicket/panels/CommitLegendPanel.java |   15 
 tests/com/gitblit/tests/MetricUtilsTest.java         |   13 
 distrib/gitblit.properties                           |    7 
 src/com/gitblit/wicket/pages/CommitPage.html         |    8 
 tests/com/gitblit/tests/GitBlitSuite.java            |   48 ++
 src/com/gitblit/wicket/pages/BlobDiffPage.java       |    2 
 src/com/gitblit/wicket/pages/MetricsPage.java        |   10 
 src/com/gitblit/GitBlit.java                         |    2 
 src/com/gitblit/wicket/pages/CommitPage.java         |   24 +
 src/com/gitblit/utils/DiffUtils.java                 |   15 
 tests/com/gitblit/tests/JGitUtilsTest.java           |  202 +++++++++-
 src/com/gitblit/utils/TicgitUtils.java               |   13 
 tests/com/gitblit/tests/TicgitUtilsTest.java         |   28 +
 distrib/users.properties                             |    2 
 src/com/gitblit/utils/MetricUtils.java               |  102 ++---
 src/com/gitblit/wicket/pages/CommitDiffPage.java     |    2 
 src/com/gitblit/models/GitNote.java                  |   31 +
 src/com/gitblit/wicket/pages/SummaryPage.java        |    2 
 src/com/gitblit/utils/JGitUtils.java                 |  454 ++++++++++--------------
 24 files changed, 656 insertions(+), 410 deletions(-)

diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index c17674a..5c6603a 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -17,9 +17,12 @@
 # if false, each exported repository must have a .git/git-daemon-export-ok file
 git.exportAll = true
 
-# Search repositories folder for nested repositories
+# Search the repositories folder subfolders for other repositories.
+# Repositories MAY NOT be nested (i.e. one repository within another)
+# but they may be grouped together in subfolders.
 # e.g. c:/gitrepos/libraries/mylibrary.git
-git.nestedRepositories = true
+#      c:/gitrepos/libraries/myotherlibrary.git
+git.searchRepositoriesSubfolders = true
 
 #
 # Authentication Settings
diff --git a/distrib/users.properties b/distrib/users.properties
index 233e9f9..a7ebc67 100644
--- a/distrib/users.properties
+++ b/distrib/users.properties
@@ -1,3 +1,3 @@
 ## Git:Blit realm file format: username=password,\#permission,repository1,repository2...
-#Tue May 31 11:19:53 EDT 2011
+#Thu Jun 02 22:11:15 EDT 2011
 admin=admin,\#admin
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 7182d9b..595a5ee 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -142,7 +142,7 @@
 
 	public List<String> getRepositoryList() {
 		return JGitUtils.getRepositoryList(repositoriesFolder, exportAll,
-				storedSettings.getBoolean(Keys.git.nestedRepositories, true));
+				storedSettings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
 	}
 
 	public Repository getRepository(String repositoryName) {
diff --git a/src/com/gitblit/models/GitNote.java b/src/com/gitblit/models/GitNote.java
new file mode 100644
index 0000000..0083637
--- /dev/null
+++ b/src/com/gitblit/models/GitNote.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+public class GitNote implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	public String content;
+	public RefModel notesRef;	
+
+	public GitNote(RefModel notesRef, String text) {
+		this.notesRef = notesRef;
+		this.content = text;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/utils/DiffUtils.java b/src/com/gitblit/utils/DiffUtils.java
index 969cb91..fab9f43 100644
--- a/src/com/gitblit/utils/DiffUtils.java
+++ b/src/com/gitblit/utils/DiffUtils.java
@@ -30,12 +30,23 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.gitblit.utils.JGitUtils.DiffOutputType;
-
 public class DiffUtils {
 
 	private static final Logger LOGGER = LoggerFactory.getLogger(DiffUtils.class);
 
+	public static enum DiffOutputType {
+		PLAIN, GITWEB, GITBLIT;
+
+		public static DiffOutputType forName(String name) {
+			for (DiffOutputType type : values()) {
+				if (type.name().equalsIgnoreCase(name)) {
+					return type;
+				}
+			}
+			return null;
+		}
+	}
+
 	public static String getCommitDiff(Repository r, RevCommit commit, DiffOutputType outputType) {
 		return getDiff(r, null, commit, null, outputType);
 	}
diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java
index 32602b9..fd2eaf3 100644
--- a/src/com/gitblit/utils/JGitUtils.java
+++ b/src/com/gitblit/utils/JGitUtils.java
@@ -21,7 +21,6 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -31,7 +30,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -52,6 +50,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -67,19 +66,31 @@
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
 import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.models.GitNote;
 import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
-import com.gitblit.models.TicketModel;
-import com.gitblit.models.TicketModel.Comment;
 
 public class JGitUtils {
 
 	static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
+
+	public static String getDisplayName(PersonIdent person) {
+		if (StringUtils.isEmpty(person.getEmailAddress())) {
+			return person.getName();
+		}
+		final StringBuilder r = new StringBuilder();
+		r.append(person.getName());
+		r.append(" <");
+		r.append(person.getEmailAddress());
+		r.append('>');
+		return r.toString().trim();
+	}
 
 	public static Repository createRepository(File repositoriesFolder, String name, boolean bare) {
 		Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(bare).call();
@@ -87,57 +98,37 @@
 	}
 
 	public static List<String> getRepositoryList(File repositoriesFolder, boolean exportAll,
-			boolean readNested) {
+			boolean searchSubfolders) {
 		List<String> list = new ArrayList<String>();
-		list.addAll(getNestedRepositories(repositoriesFolder, repositoriesFolder, exportAll,
-				readNested));
+		if (repositoriesFolder == null || !repositoriesFolder.exists()) {
+			return list;
+		}
+		list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder,
+				exportAll, searchSubfolders));
 		Collections.sort(list);
 		return list;
 	}
 
-	public static List<String> getNestedRepositories(File repositoriesFolder, File folder,
-			boolean exportAll, boolean readNested) {
+	private static List<String> getRepositoryList(String basePath, File searchFolder,
+			boolean exportAll, boolean searchSubfolders) {
 		List<String> list = new ArrayList<String>();
-		if (folder == null || !folder.exists()) {
-			return list;
-		}
-		String basefile = repositoriesFolder.getAbsolutePath();
-		for (File file : folder.listFiles()) {
-			if (file.isDirectory() && !file.getName().equalsIgnoreCase(Constants.DOT_GIT)) {
-				// if this is a git repository add it to the list
-				//
-				// first look for standard folder/.git structure
-				File gitFolder = new File(file, Constants.DOT_GIT);
-				boolean isGitRepository = gitFolder.exists() && gitFolder.isDirectory();
+		for (File file : searchFolder.listFiles()) {
+			if (file.isDirectory()) {
+				File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED);
+				if (gitDir != null) {
+					boolean exportRepository = exportAll
+							|| new File(gitDir, "git-daemon-export-ok").exists();
 
-				// then look for folder.git/HEAD or folder/HEAD and
-				// folder/config
-				if (!isGitRepository) {
-					if ((file.getName().endsWith(Constants.DOT_GIT_EXT) && new File(file,
-							Constants.HEAD).exists())
-							|| (new File(file, "config").exists() && new File(file, Constants.HEAD)
-									.exists())) {
-						gitFolder = file;
-						isGitRepository = true;
+					if (!exportRepository) {
+						continue;
 					}
-				}
-				boolean exportRepository = isGitRepository
-						&& (exportAll || new File(gitFolder, "git-daemon-export-ok").exists());
-
-				if (exportRepository) {
-					// determine repository name relative to repositories folder
-					String filename = file.getAbsolutePath();
-					String repo = filename.substring(basefile.length()).replace('\\', '/');
-					if (repo.charAt(0) == '/') {
-						repo = repo.substring(1);
-					}
-					list.add(repo);
-				}
-
-				// look for nested repositories
-				if (readNested) {
-					list.addAll(getNestedRepositories(repositoriesFolder, file, exportAll,
-							readNested));
+					// determine repository name relative to base path
+					String repository = StringUtils.getRelativePath(basePath,
+							file.getAbsolutePath());
+					list.add(repository);
+				} else if (searchSubfolders) {
+					// look for repositories in subfolders
+					list.addAll(getRepositoryList(basePath, file, exportAll, searchSubfolders));
 				}
 			}
 		}
@@ -151,18 +142,18 @@
 		if (StringUtils.isEmpty(branch)) {
 			branch = Constants.HEAD;
 		}
+		RevCommit commit = null;
 		try {
 			RevWalk walk = new RevWalk(r);
 			walk.sort(RevSort.REVERSE);
 			RevCommit head = walk.parseCommit(r.resolve(branch));
 			walk.markStart(head);
-			RevCommit commit = walk.next();
+			commit = walk.next();
 			walk.dispose();
-			return commit;
 		} catch (Throwable t) {
 			LOGGER.error("Failed to determine first commit", t);
 		}
-		return null;
+		return commit;
 	}
 
 	public static Date getFirstChange(Repository r, String branch) {
@@ -197,13 +188,17 @@
 		return getCommitDate(commit);
 	}
 
+	public static Date getCommitDate(RevCommit commit) {
+		return new Date(commit.getCommitTime() * 1000L);
+	}
+
 	public static RevCommit getCommit(Repository r, String objectId) {
-		RevCommit commit = null;
 		if (!hasCommits(r)) {
 			return null;
 		}
+		RevCommit commit = null;
 		try {
-			if (objectId == null || objectId.trim().length() == 0) {
+			if (StringUtils.isEmpty(objectId)) {
 				objectId = Constants.HEAD;
 			}
 			ObjectId object = r.resolve(objectId);
@@ -231,22 +226,6 @@
 		return refs;
 	}
 
-	public static Map<ObjectId, List<String>> getRefs(Repository r, String baseRef) {
-		Map<ObjectId, List<String>> refs = new HashMap<ObjectId, List<String>>();
-		Map<AnyObjectId, Set<Ref>> allRefs = r.getAllRefsByPeeledObjectId();
-		for (Entry<AnyObjectId, Set<Ref>> setRefs : allRefs.entrySet()) {
-			List<String> list = new ArrayList<String>();
-			for (Ref setRef : setRefs.getValue()) {
-				String name = setRef.getName();
-				if (name.startsWith(baseRef)) {
-					list.add(name);
-				}
-			}
-			refs.put(setRefs.getKey().toObjectId(), list);
-		}
-		return refs;
-	}
-
 	/**
 	 * Lookup an entry stored in a tree, failing if not present.
 	 * 
@@ -257,13 +236,17 @@
 	 * @return the parsed object entry at this path
 	 * @throws Exception
 	 */
-	public static RevObject getRevObject(Repository r, final RevTree tree, final String path) {
-		RevObject ro = null;
+	public static byte[] getRawContent(Repository r, RevCommit commit, final String path) {
 		RevWalk rw = new RevWalk(r);
 		TreeWalk tw = new TreeWalk(r);
 		tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
+		byte[] content = null;
 		try {
-			tw.reset(tree);
+			if (commit == null) {
+				ObjectId object = r.resolve(Constants.HEAD);
+				commit = rw.parseCommit(object);
+			}
+			tw.reset(commit.getTree());
 			while (tw.next()) {
 				if (tw.isSubtree() && !path.equals(tw.getPathString())) {
 					tw.enterSubtree();
@@ -271,123 +254,104 @@
 				}
 				ObjectId entid = tw.getObjectId(0);
 				FileMode entmode = tw.getFileMode(0);
-				ro = rw.lookupAny(entid, entmode.getObjectType());
+				RevObject ro = rw.lookupAny(entid, entmode.getObjectType());
 				rw.parseBody(ro);
+				ByteArrayOutputStream os = new ByteArrayOutputStream();
+				ObjectLoader ldr = r.open(ro.getId(), Constants.OBJ_BLOB);
+				byte[] tmp = new byte[4096];
+				InputStream in = ldr.openStream();
+				int n;
+				while ((n = in.read(tmp)) > 0) {
+					os.write(tmp, 0, n);
+				}
+				in.close();
+				content = os.toByteArray();
 			}
 		} catch (Throwable t) {
-			LOGGER.error("Can't find " + path + " in tree " + tree.name(), t);
+			LOGGER.error("Can't find " + path + " in tree " + commit.getTree().name(), t);
 		} finally {
-			if (rw != null) {
-				rw.dispose();
-			}
+			rw.dispose();
+			tw.release();
 		}
-		return ro;
-	}
-
-	public static byte[] getRawContent(Repository r, RevBlob blob) {
-		ByteArrayOutputStream os = new ByteArrayOutputStream();
-		try {
-			ObjectLoader ldr = r.open(blob.getId(), Constants.OBJ_BLOB);
-			byte[] tmp = new byte[1024];
-			InputStream in = ldr.openStream();
-			int n;
-			while ((n = in.read(tmp)) > 0) {
-				os.write(tmp, 0, n);
-			}
-			in.close();
-		} catch (Throwable t) {
-			LOGGER.error("Failed to read raw content", t);
-		}
-		return os.toByteArray();
-	}
-
-	public static String getRawContentAsString(Repository r, RevBlob blob) {
-		byte[] content = getRawContent(r, blob);
-		return new String(content, Charset.forName(Constants.CHARACTER_ENCODING));
+		return content;
 	}
 
 	public static String getRawContentAsString(Repository r, RevCommit commit, String blobPath) {
-		RevObject obj = getRevObject(r, commit.getTree(), blobPath);
-		byte[] content = getRawContent(r, (RevBlob) obj);
+		byte[] content = getRawContent(r, commit, blobPath);
+		if (content == null) {
+			return null;
+		}
 		return new String(content, Charset.forName(Constants.CHARACTER_ENCODING));
-	}
-
-	public static List<PathModel> getFilesInPath(Repository r, String basePath, String objectId) {
-		RevCommit commit = getCommit(r, objectId);
-		return getFilesInPath(r, basePath, commit);
 	}
 
 	public static List<PathModel> getFilesInPath(Repository r, String basePath, RevCommit commit) {
 		List<PathModel> list = new ArrayList<PathModel>();
-		if (commit == null) {
+		if (!hasCommits(r)) {
 			return list;
 		}
-		final TreeWalk walk = new TreeWalk(r);
+		if (commit == null) {
+			commit = getCommit(r, Constants.HEAD);
+		}
+		final TreeWalk tw = new TreeWalk(r);
 		try {
-			walk.addTree(commit.getTree());
-			if (basePath != null && basePath.length() > 0) {
+			tw.addTree(commit.getTree());
+			if (!StringUtils.isEmpty(basePath)) {
 				PathFilter f = PathFilter.create(basePath);
-				walk.setFilter(f);
-				walk.setRecursive(false);
+				tw.setFilter(f);
+				tw.setRecursive(false);
 				boolean foundFolder = false;
-				while (walk.next()) {
-					if (!foundFolder && walk.isSubtree()) {
-						walk.enterSubtree();
+				while (tw.next()) {
+					if (!foundFolder && tw.isSubtree()) {
+						tw.enterSubtree();
 					}
-					if (walk.getPathString().equals(basePath)) {
+					if (tw.getPathString().equals(basePath)) {
 						foundFolder = true;
 						continue;
 					}
 					if (foundFolder) {
-						list.add(getPathModel(walk, basePath, commit));
+						list.add(getPathModel(tw, basePath, commit));
 					}
 				}
 			} else {
-				walk.setRecursive(false);
-				while (walk.next()) {
-					list.add(getPathModel(walk, null, commit));
+				tw.setRecursive(false);
+				while (tw.next()) {
+					list.add(getPathModel(tw, null, commit));
 				}
 			}
 		} catch (IOException e) {
 			LOGGER.error("Failed to get files for commit " + commit.getName(), e);
 		} finally {
-			walk.release();
+			tw.release();
 		}
 		Collections.sort(list);
 		return list;
 	}
 
-	public static List<PathChangeModel> getFilesInCommit(Repository r, String commitId) {
-		RevCommit commit = getCommit(r, commitId);
-		return getFilesInCommit(r, commit);
-	}
-
 	public static List<PathChangeModel> getFilesInCommit(Repository r, RevCommit commit) {
 		List<PathChangeModel> list = new ArrayList<PathChangeModel>();
-		if (commit == null) {
-			LOGGER.warn("getFilesInCommit for NULL commit");
-			return list;
-		}
+		RevWalk rw = new RevWalk(r);
+		TreeWalk tw = new TreeWalk(r);
 		try {
-			final RevWalk rw = new RevWalk(r);
-
+			if (commit == null) {
+				ObjectId object = r.resolve(Constants.HEAD);
+				commit = rw.parseCommit(object);
+			}
 			RevTree commitTree = commit.getTree();
 
-			final TreeWalk walk = new TreeWalk(r);
-			walk.reset();
-			walk.setRecursive(true);
+			tw.reset();
+			tw.setRecursive(true);
 			if (commit.getParentCount() == 0) {
-				walk.addTree(commitTree);
-				while (walk.next()) {
-					list.add(new PathChangeModel(walk.getPathString(), walk.getPathString(), 0,
-							walk.getRawMode(0), commit.getId().getName(), ChangeType.ADD));
+				tw.addTree(commitTree);
+				while (tw.next()) {
+					list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
+							.getRawMode(0), commit.getId().getName(), ChangeType.ADD));
 				}
 			} else {
 				RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
 				RevTree parentTree = parent.getTree();
-				walk.addTree(parentTree);
-				walk.addTree(commitTree);
-				walk.setFilter(TreeFilter.ANY_DIFF);
+				tw.addTree(parentTree);
+				tw.addTree(commitTree);
+				tw.setFilter(TreeFilter.ANY_DIFF);
 
 				RawTextComparator cmp = RawTextComparator.DEFAULT;
 				DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
@@ -409,6 +373,9 @@
 			}
 		} catch (Throwable t) {
 			LOGGER.error("failed to determine files in commit!", t);
+		} finally {
+			rw.dispose();
+			tw.release();
 		}
 		return list;
 	}
@@ -416,84 +383,51 @@
 	public static List<PathModel> getDocuments(Repository r, List<String> extensions) {
 		List<PathModel> list = new ArrayList<PathModel>();
 		RevCommit commit = getCommit(r, Constants.HEAD);
-		final TreeWalk walk = new TreeWalk(r);
+		final TreeWalk tw = new TreeWalk(r);
 		try {
-			walk.addTree(commit.getTree());
+			tw.addTree(commit.getTree());
 			if (extensions != null && extensions.size() > 0) {
 				Collection<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
 				for (String extension : extensions) {
 					if (extension.charAt(0) == '.') {
-						suffixFilters.add(PathSuffixFilter.create(extension));
+						suffixFilters.add(PathSuffixFilter.create("\\" + extension));
 					} else {
 						// escape the . since this is a regexp filter
 						suffixFilters.add(PathSuffixFilter.create("\\." + extension));
 					}
 				}
 				TreeFilter filter = OrTreeFilter.create(suffixFilters);
-				walk.setFilter(filter);
-				walk.setRecursive(true);
-				while (walk.next()) {
-					list.add(getPathModel(walk, null, commit));
-				}
-			} else {
-				while (walk.next()) {
-					list.add(getPathModel(walk, null, commit));
-				}
+				tw.setFilter(filter);
+				tw.setRecursive(true);
+			}
+			while (tw.next()) {
+				list.add(getPathModel(tw, null, commit));
 			}
 		} catch (IOException e) {
-			LOGGER.error("Failed to get files for commit " + commit.getName(), e);
+			LOGGER.error("Failed to get documents for commit " + commit.getName(), e);
 		} finally {
-			walk.release();
+			tw.release();
 		}
 		Collections.sort(list);
 		return list;
 	}
 
-	public static Map<ChangeType, AtomicInteger> getChangedPathsStats(List<PathChangeModel> paths) {
-		Map<ChangeType, AtomicInteger> stats = new HashMap<ChangeType, AtomicInteger>();
-		for (PathChangeModel path : paths) {
-			if (!stats.containsKey(path.changeType)) {
-				stats.put(path.changeType, new AtomicInteger(0));
-			}
-			stats.get(path.changeType).incrementAndGet();
-		}
-		return stats;
-	}
-
-	public static enum DiffOutputType {
-		PLAIN, GITWEB, GITBLIT;
-
-		public static DiffOutputType forName(String name) {
-			for (DiffOutputType type : values()) {
-				if (type.name().equalsIgnoreCase(name)) {
-					return type;
-				}
-			}
-			return null;
-		}
-	}
-
-	private static PathModel getPathModel(TreeWalk walk, String basePath, RevCommit commit) {
+	private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) {
 		String name;
 		long size = 0;
-		if (basePath == null) {
-			name = walk.getPathString();
+		if (StringUtils.isEmpty(basePath)) {
+			name = tw.getPathString();
 		} else {
-			try {
-				name = walk.getPathString().substring(basePath.length() + 1);
-			} catch (Throwable t) {
-				name = walk.getPathString();
-			}
+			name = tw.getPathString().substring(basePath.length() + 1);
 		}
 		try {
-			if (!walk.isSubtree()) {
-				size = walk.getObjectReader()
-						.getObjectSize(walk.getObjectId(0), Constants.OBJ_BLOB);
+			if (!tw.isSubtree()) {
+				size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB);
 			}
 		} catch (Throwable t) {
 			LOGGER.error("Failed to retrieve blob size", t);
 		}
-		return new PathModel(name, walk.getPathString(), size, walk.getFileMode(0).getBits(),
+		return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(),
 				commit.getName());
 	}
 
@@ -510,11 +444,9 @@
 		} else if (FileMode.GITLINK.equals(mode)) {
 			// FIXME gitlink permissions
 			return "gitlink";
-		} else if (FileMode.MISSING.equals(mode)) {
-			// FIXME missing permissions
-			return "missing";
 		}
-		return "" + mode;
+		// FIXME missing permissions
+		return "missing";
 	}
 
 	public static List<RevCommit> getRevLog(Repository r, int maxCount) {
@@ -532,19 +464,19 @@
 			return list;
 		}
 		try {
-			if (objectId == null || objectId.trim().length() == 0) {
+			if (StringUtils.isEmpty(objectId)) {
 				objectId = Constants.HEAD;
 			}
-			RevWalk walk = new RevWalk(r);
+			RevWalk rw = new RevWalk(r);
 			ObjectId object = r.resolve(objectId);
-			walk.markStart(walk.parseCommit(object));
+			rw.markStart(rw.parseCommit(object));
 			if (!StringUtils.isEmpty(path)) {
 				TreeFilter filter = AndTreeFilter.create(
 						PathFilterGroup.createFromStrings(Collections.singleton(path)),
 						TreeFilter.ANY_DIFF);
-				walk.setTreeFilter(filter);
+				rw.setTreeFilter(filter);
 			}
-			Iterable<RevCommit> revlog = walk;
+			Iterable<RevCommit> revlog = rw;
 			if (offset > 0) {
 				int count = 0;
 				for (RevCommit rev : revlog) {
@@ -564,9 +496,9 @@
 					}
 				}
 			}
-			walk.dispose();
+			rw.dispose();
 		} catch (Throwable t) {
-			LOGGER.error("Failed to determine last change", t);
+			LOGGER.error("Failed to get revlog", t);
 		}
 		return list;
 	}
@@ -580,9 +512,10 @@
 					return type;
 				}
 			}
-			return null;
+			return COMMIT;
 		}
 
+		@Override
 		public String toString() {
 			return name().toLowerCase();
 		}
@@ -596,11 +529,11 @@
 			return list;
 		}
 		try {
-			if (objectId == null || objectId.trim().length() == 0) {
+			if (StringUtils.isEmpty(objectId)) {
 				objectId = Constants.HEAD;
 			}
-			RevWalk walk = new RevWalk(r);
-			walk.setRevFilter(new RevFilter() {
+			RevWalk rw = new RevWalk(r);
+			rw.setRevFilter(new RevFilter() {
 
 				@Override
 				public RevFilter clone() {
@@ -610,25 +543,30 @@
 				@Override
 				public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException,
 						MissingObjectException, IncorrectObjectTypeException, IOException {
+					boolean include = false;
 					switch (type) {
 					case AUTHOR:
-						return (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1)
+						include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1)
 								|| (commit.getAuthorIdent().getEmailAddress().toLowerCase()
 										.indexOf(lcValue) > -1);
+						break;
 					case COMMITTER:
-						return (commit.getCommitterIdent().getName().toLowerCase().indexOf(lcValue) > -1)
+						include = (commit.getCommitterIdent().getName().toLowerCase()
+								.indexOf(lcValue) > -1)
 								|| (commit.getCommitterIdent().getEmailAddress().toLowerCase()
 										.indexOf(lcValue) > -1);
+						break;
 					case COMMIT:
-						return commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1;
+						include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1;
+						break;
 					}
-					return false;
+					return include;
 				}
 
 			});
 			ObjectId object = r.resolve(objectId);
-			walk.markStart(walk.parseCommit(object));
-			Iterable<RevCommit> revlog = walk;
+			rw.markStart(rw.parseCommit(object));
+			Iterable<RevCommit> revlog = rw;
 			if (offset > 0) {
 				int count = 0;
 				for (RevCommit rev : revlog) {
@@ -648,9 +586,9 @@
 					}
 				}
 			}
-			walk.dispose();
+			rw.dispose();
 		} catch (Throwable t) {
-			LOGGER.error("Failed to determine last change", t);
+			LOGGER.error("Failed to search revlogs", t);
 		}
 		return list;
 	}
@@ -667,7 +605,11 @@
 		return getRefs(r, Constants.R_REMOTES, maxCount);
 	}
 
-	public static List<RefModel> getRefs(Repository r, String refs, int maxCount) {
+	public static List<RefModel> getNotes(Repository r, int maxCount) {
+		return getRefs(r, Constants.R_NOTES, maxCount);
+	}
+
+	private static List<RefModel> getRefs(Repository r, String refs, int maxCount) {
 		List<RefModel> list = new ArrayList<RefModel>();
 		try {
 			Map<String, Ref> map = r.getRefDatabase().getRefs(refs);
@@ -687,45 +629,32 @@
 		return list;
 	}
 
-	public static Ref getRef(Repository r, String id) {
-		// FIXME
-		try {
-			Map<String, Ref> map = r.getRefDatabase().getRefs(id);
-			for (Entry<String, Ref> entry : map.entrySet()) {
-				return entry.getValue();
+	public static List<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
+		List<GitNote> list = new ArrayList<GitNote>();
+		List<RefModel> notesRefs = getNotes(repository, -1);
+		for (RefModel notesRef : notesRefs) {
+			RevCommit notes = JGitUtils.getCommit(repository, notesRef.getName());
+			StringBuilder sb = new StringBuilder(commit.getName());
+			sb.insert(2, '/');
+			String text = getRawContentAsString(repository, notes, sb.toString());
+			if (!StringUtils.isEmpty(text)) {
+				GitNote gitNote = new GitNote(notesRef, text);
+				list.add(gitNote);
 			}
-		} catch (IOException e) {
-			LOGGER.error("Failed to retrieve ref " + id, e);
 		}
-		return null;
-	}
-
-	public static Date getCommitDate(RevCommit commit) {
-		return new Date(commit.getCommitTime() * 1000L);
-	}
-
-	public static String getDisplayName(PersonIdent person) {
-		final StringBuilder r = new StringBuilder();
-		r.append(person.getName());
-		r.append(" <");
-		r.append(person.getEmailAddress());
-		r.append('>');
-		return r.toString();
+		return list;
 	}
 
 	public static StoredConfig readConfig(Repository r) {
 		StoredConfig c = r.getConfig();
-		if (c != null) {
-			try {
-				c.load();
-			} catch (ConfigInvalidException cex) {
-				LOGGER.error("Repository configuration is invalid!", cex);
-			} catch (IOException cex) {
-				LOGGER.error("Could not open repository configuration!", cex);
-			}
-			return c;
+		try {
+			c.load();
+		} catch (ConfigInvalidException cex) {
+			LOGGER.error("Repository configuration is invalid!", cex);
+		} catch (IOException cex) {
+			LOGGER.error("Could not open repository configuration!", cex);
 		}
-		return null;
+		return c;
 	}
 
 	public static boolean zip(Repository r, String basePath, String objectId, OutputStream os)
@@ -734,26 +663,27 @@
 		if (commit == null) {
 			return false;
 		}
-		final RevWalk rw = new RevWalk(r);
-		final TreeWalk walk = new TreeWalk(r);
+		boolean success = false;
+		RevWalk rw = new RevWalk(r);
+		TreeWalk tw = new TreeWalk(r);
 		try {
-			walk.addTree(commit.getTree());
+			tw.addTree(commit.getTree());
 			ZipOutputStream zos = new ZipOutputStream(os);
 			zos.setComment("Generated by Git:Blit");
-			if (basePath != null && basePath.length() > 0) {
+			if (!StringUtils.isEmpty(basePath)) {
 				PathFilter f = PathFilter.create(basePath);
-				walk.setFilter(f);
+				tw.setFilter(f);
 			}
-			walk.setRecursive(true);
-			while (walk.next()) {
-				ZipEntry entry = new ZipEntry(walk.getPathString());
-				entry.setSize(walk.getObjectReader().getObjectSize(walk.getObjectId(0),
+			tw.setRecursive(true);
+			while (tw.next()) {
+				ZipEntry entry = new ZipEntry(tw.getPathString());
+				entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0),
 						Constants.OBJ_BLOB));
 				entry.setComment(commit.getName());
 				zos.putNextEntry(entry);
 
-				ObjectId entid = walk.getObjectId(0);
-				FileMode entmode = walk.getFileMode(0);
+				ObjectId entid = tw.getObjectId(0);
+				FileMode entmode = tw.getFileMode(0);
 				RevBlob blob = (RevBlob) rw.lookupAny(entid, entmode.getObjectType());
 				rw.parseBody(blob);
 
@@ -767,13 +697,13 @@
 				in.close();
 			}
 			zos.finish();
-			return true;
+			success = true;
 		} catch (IOException e) {
 			LOGGER.error("Failed to zip files from commit " + commit.getName(), e);
 		} finally {
-			walk.release();
+			tw.release();
 			rw.dispose();
 		}
-		return false;
+		return success;
 	}
 }
diff --git a/src/com/gitblit/utils/MarkdownUtils.java b/src/com/gitblit/utils/MarkdownUtils.java
index 15d59cf..697fa99 100644
--- a/src/com/gitblit/utils/MarkdownUtils.java
+++ b/src/com/gitblit/utils/MarkdownUtils.java
@@ -26,22 +26,10 @@
 public class MarkdownUtils {
 
 	public static String transformMarkdown(String markdown) throws java.text.ParseException {
-		// Read raw markdown content and transform it to html
-		StringReader reader = new StringReader(markdown);
-		StringWriter writer = new StringWriter();
 		try {
-			Markdown md = new Markdown();
-			md.transform(reader, writer);
-			return writer.toString();
-		} catch (ParseException p) {
-			throw new java.text.ParseException(p.getMessage(), 0);
-		} finally {
-			reader.close();
-			try {
-				writer.close();
-			} catch (IOException e) {
-				// IGNORE
-			}
+			return transformMarkdown(new StringReader(markdown));
+		} catch (NullPointerException p) {
+			throw new java.text.ParseException("Markdown string is null!", 0);
 		}
 	}
 
@@ -51,7 +39,7 @@
 		try {
 			Markdown md = new Markdown();
 			md.transform(markdownReader, writer);
-			return writer.toString();
+			return writer.toString().trim();
 		} catch (ParseException p) {
 			throw new java.text.ParseException(p.getMessage(), 0);
 		} finally {
@@ -67,5 +55,4 @@
 			}
 		}
 	}
-
 }
diff --git a/src/com/gitblit/utils/MetricUtils.java b/src/com/gitblit/utils/MetricUtils.java
index b1da273..4ca9f36 100644
--- a/src/com/gitblit/utils/MetricUtils.java
+++ b/src/com/gitblit/utils/MetricUtils.java
@@ -42,45 +42,7 @@
 	public static List<Metric> getDateMetrics(Repository r, boolean includeTotal, String format) {
 		Metric total = new Metric("TOTAL");
 		final Map<String, Metric> metricMap = new HashMap<String, Metric>();
-	
-		if (JGitUtils.hasCommits(r)) {			
-			try {
-				RevWalk walk = new RevWalk(r);
-				ObjectId object = r.resolve(Constants.HEAD);
-				RevCommit lastCommit = walk.parseCommit(object);
-				walk.markStart(lastCommit);
-				SimpleDateFormat df = new SimpleDateFormat(format);
-				Iterable<RevCommit> revlog = walk;
-				for (RevCommit rev : revlog) {
-					Date d = JGitUtils.getCommitDate(rev);
-					String p = df.format(d);
-					if (!metricMap.containsKey(p)) {
-						metricMap.put(p, new Metric(p));
-					}
-					Metric m = metricMap.get(p);
-					m.count++;
-					total.count++;					
-				}
-			} catch (Throwable t) {
-				JGitUtils.LOGGER.error("Failed to mine log history for metrics", t);
-			}
-		}
-		List<String> keys = new ArrayList<String>(metricMap.keySet());
-		Collections.sort(keys);
-		List<Metric> metrics = new ArrayList<Metric>();
-		for (String key : keys) {
-			metrics.add(metricMap.get(key));
-		}
-		if (includeTotal) {
-			metrics.add(0, total);
-		}
-		return metrics;
-	}
 
-	public static List<Metric> getDateMetrics(Repository r, boolean includeTotal) {
-		Metric total = new Metric("TOTAL");
-		final Map<String, Metric> metricMap = new HashMap<String, Metric>();
-	
 		if (JGitUtils.hasCommits(r)) {
 			final List<RefModel> tags = JGitUtils.getTags(r, -1);
 			final Map<ObjectId, RefModel> tagMap = new HashMap<ObjectId, RefModel>();
@@ -90,25 +52,31 @@
 			try {
 				RevWalk walk = new RevWalk(r);
 				ObjectId object = r.resolve(Constants.HEAD);
-	
-				RevCommit firstCommit = JGitUtils.getFirstCommit(r, Constants.HEAD);
 				RevCommit lastCommit = walk.parseCommit(object);
-				int diffDays = (lastCommit.getCommitTime() - firstCommit.getCommitTime())
-						/ (60 * 60 * 24);
-				total.duration = diffDays;
-				DateFormat df;
-				if (diffDays <= 90) {
-					// Days
-					df = new SimpleDateFormat("yyyy-MM-dd");
-				} else if (diffDays > 90 && diffDays < 365) {
-					// Weeks
-					df = new SimpleDateFormat("yyyy-MM (w)");
-				} else {
-					// Months
-					df = new SimpleDateFormat("yyyy-MM");
-				}
 				walk.markStart(lastCommit);
-	
+
+				DateFormat df;
+				if (StringUtils.isEmpty(format)) {
+					// dynamically determine date format
+					RevCommit firstCommit = JGitUtils.getFirstCommit(r, Constants.HEAD);
+					int diffDays = (lastCommit.getCommitTime() - firstCommit.getCommitTime())
+							/ (60 * 60 * 24);
+					total.duration = diffDays;
+					if (diffDays <= 90) {
+						// Days
+						df = new SimpleDateFormat("yyyy-MM-dd");
+					} else if (diffDays > 90 && diffDays < 365) {
+						// Weeks
+						df = new SimpleDateFormat("yyyy-MM (w)");
+					} else {
+						// Months
+						df = new SimpleDateFormat("yyyy-MM");
+					}
+				} else {
+					// use specified date format
+					df = new SimpleDateFormat(format);
+				}
+
 				Iterable<RevCommit> revlog = walk;
 				for (RevCommit rev : revlog) {
 					Date d = JGitUtils.getCommitDate(rev);
@@ -125,7 +93,7 @@
 					}
 				}
 			} catch (Throwable t) {
-				JGitUtils.LOGGER.error("Failed to mine log history for metrics", t);
+				LOGGER.error("Failed to mine log history for date metrics", t);
 			}
 		}
 		List<String> keys = new ArrayList<String>(metricMap.keySet());
@@ -140,32 +108,38 @@
 		return metrics;
 	}
 
-	public static List<Metric> getAuthorMetrics(Repository r) {
-		Metric total = new Metric("TOTAL");
+	public static List<Metric> getAuthorMetrics(Repository r, boolean byEmail) {
 		final Map<String, Metric> metricMap = new HashMap<String, Metric>();
-	
+
 		if (JGitUtils.hasCommits(r)) {
 			try {
 				RevWalk walk = new RevWalk(r);
 				ObjectId object = r.resolve(Constants.HEAD);
 				RevCommit lastCommit = walk.parseCommit(object);
 				walk.markStart(lastCommit);
-	
+
 				Iterable<RevCommit> revlog = walk;
 				for (RevCommit rev : revlog) {
-					String p = rev.getAuthorIdent().getName();
-					if (StringUtils.isEmpty(p)) {
+					String p;
+					if (byEmail) {
 						p = rev.getAuthorIdent().getEmailAddress();
+						if (StringUtils.isEmpty(p)) {
+							p = rev.getAuthorIdent().getName();
+						}
+					} else {
+						p = rev.getAuthorIdent().getName();
+						if (StringUtils.isEmpty(p)) {
+							p = rev.getAuthorIdent().getEmailAddress();
+						}
 					}
 					if (!metricMap.containsKey(p)) {
 						metricMap.put(p, new Metric(p));
 					}
 					Metric m = metricMap.get(p);
 					m.count++;
-					total.count++;
 				}
 			} catch (Throwable t) {
-				JGitUtils.LOGGER.error("Failed to mine log history for metrics", t);
+				LOGGER.error("Failed to mine log history for author metrics", t);
 			}
 		}
 		List<String> keys = new ArrayList<String>(metricMap.keySet());
diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java
index fd6ca98..2a10a59 100644
--- a/src/com/gitblit/utils/StringUtils.java
+++ b/src/com/gitblit/utils/StringUtils.java
@@ -134,4 +134,12 @@
 		}
 		return "";
 	}
+	
+	public static String getRelativePath(String basePath, String fullPath) {		
+		String relativePath = fullPath.substring(basePath.length()).replace('\\', '/');
+		if (relativePath.charAt(0) == '/') {
+			relativePath = relativePath.substring(1);
+		}
+		return relativePath;
+	}
 }
diff --git a/src/com/gitblit/utils/TicgitUtils.java b/src/com/gitblit/utils/TicgitUtils.java
index 914b813..576de34 100644
--- a/src/com/gitblit/utils/TicgitUtils.java
+++ b/src/com/gitblit/utils/TicgitUtils.java
@@ -61,6 +61,9 @@
 
 	public static List<TicketModel> getTickets(Repository r) {
 		RefModel ticgitBranch = getTicketsBranch(r);
+		if (ticgitBranch == null) {
+			return null;
+		}
 		List<PathModel> paths = JGitUtils.getFilesInPath(r, null, ticgitBranch.commit);
 		List<TicketModel> tickets = new ArrayList<TicketModel>();
 		for (PathModel ticketFolder : paths) {
@@ -112,7 +115,7 @@
 						Comment c = new Comment(file.name, content);
 						ticket.comments.add(c);
 					} catch (ParseException e) {
-						e.printStackTrace();
+						LOGGER.error("Failed to parse ticket comment", e);
 					}
 				} else if (chunks[0].equals("TAG")) {
 					if (content.startsWith("TAG_")) {
@@ -126,13 +129,5 @@
 			}
 		}
 		Collections.sort(ticket.comments);
-	}
-
-	public static String getTicketContent(Repository r, String filePath) {
-		RefModel ticketsBranch = getTicketsBranch(r);
-		if (ticketsBranch != null) {
-			return JGitUtils.getRawContentAsString(r, ticketsBranch.commit, filePath);
-		}
-		return "";
 	}
 }
diff --git a/src/com/gitblit/wicket/pages/BlobDiffPage.java b/src/com/gitblit/wicket/pages/BlobDiffPage.java
index b94da01..a8f8b3c 100644
--- a/src/com/gitblit/wicket/pages/BlobDiffPage.java
+++ b/src/com/gitblit/wicket/pages/BlobDiffPage.java
@@ -24,8 +24,8 @@
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
 import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.DiffUtils.DiffOutputType;
 import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.JGitUtils.DiffOutputType;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
diff --git a/src/com/gitblit/wicket/pages/CommitDiffPage.java b/src/com/gitblit/wicket/pages/CommitDiffPage.java
index e7af833..4815b2c 100644
--- a/src/com/gitblit/wicket/pages/CommitDiffPage.java
+++ b/src/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -32,8 +32,8 @@
 import com.gitblit.Keys;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.DiffUtils.DiffOutputType;
 import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.JGitUtils.DiffOutputType;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
 import com.gitblit.wicket.panels.CommitLegendPanel;
diff --git a/src/com/gitblit/wicket/pages/CommitPage.html b/src/com/gitblit/wicket/pages/CommitPage.html
index 1b5fffe..bd317b7 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.html
+++ b/src/com/gitblit/wicket/pages/CommitPage.html
@@ -44,6 +44,14 @@
 	<!-- full message -->
 	<div class="commit_message" wicket:id="fullMessage">[commit message]</div>
 
+	<!--  git notes -->
+	<table style="padding-bottom:5px;">
+		<tr wicket:id="notes">
+			<td style="vertical-align:top;"><span class="headRef" wicket:id="refName"></span><br/><span wicket:id="authorName"></span><br/><span wicket:id="authorDate"></span></td>
+			<td><span wicket:id="noteContent"></span></td>
+		</tr>
+	</table>
+	
 	<!--  commit legend -->
 	<div style="text-align:right;" wicket:id="commitLegend"></div>
 	
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index bc0d879..3af9cf1 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -18,6 +18,7 @@
 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.html.link.BookmarkablePageLink;
@@ -33,8 +34,10 @@
 import com.gitblit.DownloadZipServlet;
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
+import com.gitblit.models.GitNote;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.JGitUtils.SearchType;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
@@ -112,6 +115,27 @@
 
 		addFullText("fullMessage", c.getFullMessage(), true);
 
+		// git notes
+		List<GitNote> notes = JGitUtils.getNotesOnCommit(r, c);
+		ListDataProvider<GitNote> notesDp = new ListDataProvider<GitNote>(notes);
+		DataView<GitNote> notesView = new DataView<GitNote>("notes", notesDp) {
+			private static final long serialVersionUID = 1L;
+
+			public void populateItem(final Item<GitNote> item) {
+				GitNote entry = item.getModelObject();
+				Component c = new LinkPanel("refName", null, entry.notesRef.displayName,
+						CommitPage.class, newCommitParameter(entry.notesRef.commit.getName()));
+				WicketUtils.setCssClass(c, "headRef");
+				item.add(c);
+				item.add(createPersonPanel("authorName", entry.notesRef.commit.getAuthorIdent(), SearchType.AUTHOR));
+				item.add(WicketUtils.createTimestampLabel("authorDate",
+						entry.notesRef.commit.getAuthorIdent().getWhen(), getTimeZone()));
+				item.add(new Label("noteContent", StringUtils.breakLinesForHtml(entry.content)).setEscapeModelStrings(false));
+			}
+		};
+		add(notesView.setVisible(notes.size() > 0));
+		
+		
 		// changed paths list
 		List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, c);
 		add(new CommitLegendPanel("commitLegend", paths));
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
index eb2a8e6..e5496a1 100644
--- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -19,7 +19,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -104,6 +103,22 @@
 
 					// automatically convert backslashes to forward slashes
 					repositoryModel.name = repositoryModel.name.replace('\\', '/');
+					// Automatically replace // with /
+					repositoryModel.name = repositoryModel.name.replace("//", "/");
+
+					// prohibit folder paths
+					if (repositoryModel.name.startsWith("/")) {						
+						error("Leading root folder references (/) are prohibited.");
+						return;
+					}
+					if (repositoryModel.name.startsWith("../")) {						
+						error("Relative folder references (../) are prohibited.");
+						return;
+					}
+					if (repositoryModel.name.contains("/../")) {
+						error("Relative folder references (../) are prohibited.");
+						return;
+					}
 
 					// confirm valid characters in repository name
 					char[] validChars = { '/', '.', '_', '-' };
@@ -120,7 +135,7 @@
 							}
 						}
 					}
-
+					
 					// confirm access restriction selection
 					if (repositoryModel.accessRestriction == null) {
 						error("Please select access restriction!");
diff --git a/src/com/gitblit/wicket/pages/MetricsPage.java b/src/com/gitblit/wicket/pages/MetricsPage.java
index 2186ae3..c6231e9 100644
--- a/src/com/gitblit/wicket/pages/MetricsPage.java
+++ b/src/com/gitblit/wicket/pages/MetricsPage.java
@@ -47,7 +47,7 @@
 	public MetricsPage(PageParameters params) {
 		super(params);
 		Repository r = getRepository();
-		insertLinePlot("commitsChart", MetricUtils.getDateMetrics(r, false));
+		insertLinePlot("commitsChart", MetricUtils.getDateMetrics(r, false, null));
 		insertBarPlot("dayOfWeekChart", getDayOfWeekMetrics(r));
 		insertLinePlot("timeOfDayChart", getTimeOfDayMetrics(r));
 		insertPieChart("authorsChart", getAuthorMetrics(r));
@@ -57,7 +57,7 @@
 		if ((metrics != null) && (metrics.size() > 0)) {
 			IChartData data = WicketUtils.getChartData(metrics);
 
-			ChartProvider provider = new ChartProvider(new Dimension(400, 100), ChartType.LINE,
+			ChartProvider provider = new ChartProvider(new Dimension(500, 100), ChartType.LINE,
 					data);
 			ChartAxis dateAxis = new ChartAxis(ChartAxisType.BOTTOM);
 			dateAxis.setLabels(new String[] { metrics.get(0).name,
@@ -82,7 +82,7 @@
 		if ((metrics != null) && (metrics.size() > 0)) {
 			IChartData data = WicketUtils.getChartData(metrics);
 
-			ChartProvider provider = new ChartProvider(new Dimension(400, 100),
+			ChartProvider provider = new ChartProvider(new Dimension(500, 100),
 					ChartType.BAR_VERTICAL_SET, data);
 			ChartAxis dateAxis = new ChartAxis(ChartAxisType.BOTTOM);
 			List<String> labels = new ArrayList<String>();
@@ -110,7 +110,7 @@
 			for (Metric metric : metrics) {
 				labels.add(metric.name);
 			}
-			ChartProvider provider = new ChartProvider(new Dimension(400, 200), ChartType.PIE, data);
+			ChartProvider provider = new ChartProvider(new Dimension(500, 200), ChartType.PIE, data);
 			provider.setPieLabels(labels.toArray(new String[labels.size()]));
 			add(new Chart(wicketId, provider));
 		} else {
@@ -164,7 +164,7 @@
 	}
 
 	private List<Metric> getAuthorMetrics(Repository repository) {
-		List<Metric> authors = MetricUtils.getAuthorMetrics(repository);
+		List<Metric> authors = MetricUtils.getAuthorMetrics(repository, true);
 		Collections.sort(authors, new Comparator<Metric>() {
 			@Override
 			public int compare(Metric o1, Metric o2) {
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.java b/src/com/gitblit/wicket/pages/SummaryPage.java
index c054fcc..a2d36d2 100644
--- a/src/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/com/gitblit/wicket/pages/SummaryPage.java
@@ -77,7 +77,7 @@
 		List<Metric> metrics = null;
 		Metric metricsTotal = null;
 		if (GitBlit.getBoolean(Keys.web.generateActivityGraph, true)) {
-			metrics = MetricUtils.getDateMetrics(r, true);
+			metrics = MetricUtils.getDateMetrics(r, true, null);
 			metricsTotal = metrics.remove(0);
 		}
 
diff --git a/src/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/com/gitblit/wicket/panels/CommitLegendPanel.java
index bbfa185..d875233 100644
--- a/src/com/gitblit/wicket/panels/CommitLegendPanel.java
+++ b/src/com/gitblit/wicket/panels/CommitLegendPanel.java
@@ -17,6 +17,7 @@
 
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -29,7 +30,6 @@
 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 
 import com.gitblit.models.PathModel.PathChangeModel;
-import com.gitblit.utils.JGitUtils;
 import com.gitblit.wicket.WicketUtils;
 
 public class CommitLegendPanel extends Panel {
@@ -38,7 +38,7 @@
 
 	public CommitLegendPanel(String id, List<PathChangeModel> paths) {
 		super(id);
-		final Map<ChangeType, AtomicInteger> stats = JGitUtils.getChangedPathsStats(paths);
+		final Map<ChangeType, AtomicInteger> stats = getChangedPathsStats(paths);
 		ListDataProvider<ChangeType> legendDp = new ListDataProvider<ChangeType>(
 				new ArrayList<ChangeType>(stats.keySet()));
 		DataView<ChangeType> legendsView = new DataView<ChangeType>("legend", legendDp) {
@@ -74,4 +74,15 @@
 		};
 		add(legendsView);
 	}
+	
+	protected Map<ChangeType, AtomicInteger> getChangedPathsStats(List<PathChangeModel> paths) {
+		Map<ChangeType, AtomicInteger> stats = new HashMap<ChangeType, AtomicInteger>();
+		for (PathChangeModel path : paths) {
+			if (!stats.containsKey(path.changeType)) {
+				stats.put(path.changeType, new AtomicInteger(0));
+			}
+			stats.get(path.changeType).incrementAndGet();
+		}
+		return stats;
+	}
 }
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/DiffUtilsTest.java b/tests/com/gitblit/tests/DiffUtilsTest.java
index 34cb853..e60a2a4 100644
--- a/tests/com/gitblit/tests/DiffUtilsTest.java
+++ b/tests/com/gitblit/tests/DiffUtilsTest.java
@@ -21,8 +21,8 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.DiffUtils.DiffOutputType;
 import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.JGitUtils.DiffOutputType;
 
 public class DiffUtilsTest extends TestCase {
 
diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java
index fcb5723..97e46c9 100644
--- a/tests/com/gitblit/tests/GitBlitSuite.java
+++ b/tests/com/gitblit/tests/GitBlitSuite.java
@@ -28,7 +28,9 @@
 
 import com.gitblit.FileSettings;
 import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
 import com.gitblit.JettyLoginService;
+import com.gitblit.models.RepositoryModel;
 
 public class GitBlitSuite extends TestSetup {
 	public static final File REPOSITORIES = new File("git");
@@ -42,6 +44,7 @@
 		suite.addTestSuite(TimeUtilsTest.class);
 		suite.addTestSuite(StringUtilsTest.class);
 		suite.addTestSuite(ByteFormatTest.class);
+		suite.addTestSuite(MarkdownUtilsTest.class);
 		suite.addTestSuite(JGitUtilsTest.class);
 		suite.addTestSuite(DiffUtilsTest.class);
 		suite.addTestSuite(MetricUtilsTest.class);
@@ -60,16 +63,21 @@
 
 	@Override
 	protected void setUp() throws Exception {
-		if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
-			cloneOrFetch("helloworld.git", "https://github.com/git/hello-world.git", true);
-			cloneOrFetch("nested/helloworld.git", "https://github.com/git/hello-world.git", true);
-			cloneOrFetch("ticgit.git", "https://github.com/jeffWelling/ticgit.git", true);
-		}
 		FileSettings settings = new FileSettings("distrib/gitblit.properties");
 		GitBlit.self().configureContext(settings);
 		JettyLoginService loginService = new JettyLoginService(new File("distrib/users.properties"));
 		loginService.loadUsers();
 		GitBlit.self().setLoginService(loginService);
+
+		if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
+			cloneOrFetch("helloworld.git", "https://github.com/git/hello-world.git", true);
+			cloneOrFetch("nested/helloworld.git", "https://github.com/git/hello-world.git", true);
+			cloneOrFetch("ticgit.git", "https://github.com/jeffWelling/ticgit.git", true);
+
+			enableTickets("ticgit.git");
+			enableDocs("ticgit.git");
+			showRemoteBranches("ticgit.git");
+		}
 	}
 
 	private void cloneOrFetch(String toFolder, String fromUrl, boolean bare) throws Exception {
@@ -92,4 +100,34 @@
 			System.out.println("done.");
 		}
 	}
+
+	private void enableTickets(String repositoryName) {
+		try {
+			RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+			model.useTickets = true;
+			GitBlit.self().editRepositoryModel(model.name, model, false);
+		} catch (GitBlitException g) {
+			g.printStackTrace();
+		}
+	}
+	
+	private void enableDocs(String repositoryName) {
+		try {
+			RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+			model.useDocs = true;
+			GitBlit.self().editRepositoryModel(model.name, model, false);
+		} catch (GitBlitException g) {
+			g.printStackTrace();
+		}
+	}
+	
+	private void showRemoteBranches(String repositoryName) {
+		try {
+			RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+			model.showRemoteBranches = true;
+			GitBlit.self().editRepositoryModel(model.name, model, false);
+		} catch (GitBlitException g) {
+			g.printStackTrace();
+		}
+	}
 }
diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java
index f1dcaac..6afa38b 100644
--- a/tests/com/gitblit/tests/JGitUtilsTest.java
+++ b/tests/com/gitblit/tests/JGitUtilsTest.java
@@ -17,24 +17,40 @@
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
 
 import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.PathModel;
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.models.RefModel;
 import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JGitUtils.SearchType;
 
 public class JGitUtilsTest extends TestCase {
+
+	public void testDisplayName() throws Exception {
+		assertTrue(JGitUtils.getDisplayName(new PersonIdent("Napoleon Bonaparte", "")).equals(
+				"Napoleon Bonaparte"));
+		assertTrue(JGitUtils.getDisplayName(new PersonIdent("", "someone@somewhere.com")).equals(
+				"<someone@somewhere.com>"));
+		assertTrue(JGitUtils.getDisplayName(
+				new PersonIdent("Napoleon Bonaparte", "someone@somewhere.com")).equals(
+				"Napoleon Bonaparte <someone@somewhere.com>"));
+	}
 
 	public void testFindRepositories() {
 		List<String> list = JGitUtils.getRepositoryList(null, true, true);
@@ -53,7 +69,7 @@
 
 	public void testFirstCommit() throws Exception {
 		assertTrue(JGitUtils.getFirstChange(null, null).equals(new Date(0)));
-		
+
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		RevCommit commit = JGitUtils.getFirstCommit(repository, null);
 		Date firstChange = JGitUtils.getFirstChange(repository, null);
@@ -63,18 +79,16 @@
 				commit.getName().equals("f554664a346629dc2b839f7292d06bad2db4aece"));
 		assertTrue(firstChange.equals(new Date(commit.getCommitTime() * 1000L)));
 	}
-	
+
 	public void testLastCommit() throws Exception {
 		assertTrue(JGitUtils.getLastChange(null).equals(new Date(0)));
-		
+
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		assertTrue(JGitUtils.getCommit(repository, null) != null);
 		Date date = JGitUtils.getLastChange(repository);
 		repository.close();
 		assertTrue("Could not get last repository change date!", date != null);
 	}
-
-	
 
 	public void testCreateRepository() throws Exception {
 		String[] repositories = { "NewTestRepository.git", "NewTestRepository" };
@@ -84,17 +98,16 @@
 					repositoryName, isBare);
 			File folder;
 			if (isBare) {
-				folder = new File(GitBlitSuite.REPOSITORIES, repositoryName);	
+				folder = new File(GitBlitSuite.REPOSITORIES, repositoryName);
 			} else {
 				folder = new File(GitBlitSuite.REPOSITORIES, repositoryName + "/.git");
-			}			
+			}
 			assertTrue(repository != null);
 			assertFalse(JGitUtils.hasCommits(repository));
 			assertTrue(JGitUtils.getFirstCommit(repository, null) == null);
 			assertTrue(JGitUtils.getFirstChange(repository, null).getTime() == folder
 					.lastModified());
-			assertTrue(JGitUtils.getLastChange(repository).getTime() == folder
-					.lastModified());
+			assertTrue(JGitUtils.getLastChange(repository).getTime() == folder.lastModified());
 			assertTrue(JGitUtils.getCommit(repository, null) == null);
 			repository.close();
 			assertTrue(GitBlit.self().deleteRepository(repositoryName));
@@ -102,6 +115,13 @@
 	}
 
 	public void testRefs() throws Exception {
+		Repository repository = GitBlitSuite.getTicgitRepository();
+		Map<ObjectId, List<String>> map = JGitUtils.getAllRefs(repository);
+		repository.close();
+		assertTrue(map.size() > 0);
+	}
+
+	public void testBranches() throws Exception {
 		Repository repository = GitBlitSuite.getTicgitRepository();
 		for (RefModel model : JGitUtils.getLocalBranches(repository, -1)) {
 			assertTrue(model.getName().startsWith(Constants.R_HEADS));
@@ -119,6 +139,12 @@
 					+ model.getName().hashCode());
 			assertTrue(model.getShortLog().equals(model.commit.getShortMessage()));
 		}
+		assertTrue(JGitUtils.getRemoteBranches(repository, 10).size() == 10);
+		repository.close();
+	}
+
+	public void testTags() throws Exception {
+		Repository repository = GitBlitSuite.getTicgitRepository();
 		for (RefModel model : JGitUtils.getTags(repository, -1)) {
 			if (model.getObjectId().getName().equals("283035e4848054ff1803cb0e690270787dc92399")) {
 				assertTrue("Not an annotated tag!", model.isAnnotatedTag());
@@ -133,23 +159,26 @@
 		repository.close();
 	}
 
-	public void testRetrieveRevObject() throws Exception {
-		Repository repository = GitBlitSuite.getHelloworldRepository();
-		RevCommit commit = JGitUtils.getCommit(repository, Constants.HEAD);
-		RevTree tree = commit.getTree();
-		RevObject object = JGitUtils.getRevObject(repository, tree, "java.java");
-		repository.close();
-		assertTrue("Object is null!", object != null);
+	public void testCommitNotes() throws Exception {
+//		Repository repository = new FileRepository(new File("c:/projects/git/jgit.git/.git"));
+//		RevCommit commit = JGitUtils.getCommit(repository,
+//				"ada903085d1b4ef8c79e3e2d91f49fee7e188f53");
+//		List<GitNote> list = JGitUtils.getNotesOnCommit(repository, commit);
+//		repository.close();
+//		assertTrue(list.size() > 0);
 	}
 
-	public void testRetrieveStringContent() throws Exception {
+	public void testStringContent() throws Exception {
 		Repository repository = GitBlitSuite.getHelloworldRepository();
+		String contentA = JGitUtils.getRawContentAsString(repository, null, "java.java");
 		RevCommit commit = JGitUtils.getCommit(repository, Constants.HEAD);
-		RevTree tree = commit.getTree();
-		RevBlob blob = (RevBlob) JGitUtils.getRevObject(repository, tree, "java.java");
-		String content = JGitUtils.getRawContentAsString(repository, blob);
+		String contentB = JGitUtils.getRawContentAsString(repository, commit, "java.java");
+		String contentC = JGitUtils.getRawContentAsString(repository, commit, "missing.txt");
 		repository.close();
-		assertTrue("Content is null!", content != null && content.length() > 0);
+		assertTrue("ContentA is null!", contentA != null && contentA.length() > 0);
+		assertTrue("ContentB is null!", contentB != null && contentB.length() > 0);
+		assertTrue(contentA.equals(contentB));
+		assertTrue(contentC == null);
 	}
 
 	public void testFilesInCommit() throws Exception {
@@ -157,6 +186,15 @@
 		RevCommit commit = JGitUtils.getCommit(repository,
 				"1d0c2933a4ae69c362f76797d42d6bd182d05176");
 		List<PathChangeModel> paths = JGitUtils.getFilesInCommit(repository, commit);
+
+		commit = JGitUtils.getCommit(repository, "af0e9b2891fda85afc119f04a69acf7348922830");
+		List<PathChangeModel> deletions = JGitUtils.getFilesInCommit(repository, commit);
+
+		commit = JGitUtils.getFirstCommit(repository, null);
+		List<PathChangeModel> additions = JGitUtils.getFilesInCommit(repository, commit);
+
+		List<PathChangeModel> latestChanges = JGitUtils.getFilesInCommit(repository, null);
+
 		repository.close();
 		assertTrue("No changed paths found!", paths.size() == 1);
 		for (PathChangeModel path : paths) {
@@ -165,17 +203,119 @@
 			assertTrue("PathChangeModel equals itself failed!", path.equals(path));
 			assertFalse("PathChangeModel equals string failed!", path.equals(""));
 		}
+		assertTrue(deletions.get(0).changeType.equals(ChangeType.DELETE));
+		assertTrue(additions.get(0).changeType.equals(ChangeType.ADD));
+		assertTrue(latestChanges.size() > 0);
+	}
+
+	public void testFilesInPath() throws Exception {
+		assertTrue(JGitUtils.getFilesInPath(null, null, null).size() == 0);
+		Repository repository = GitBlitSuite.getHelloworldRepository();
+		List<PathModel> files = JGitUtils.getFilesInPath(repository, null, null);
+		repository.close();
+		assertTrue(files.size() > 10);
+	}
+
+	public void testDocuments() throws Exception {
+		Repository repository = GitBlitSuite.getTicgitRepository();
+		List<String> extensions = GitBlit.getStrings(Keys.web.markdownExtensions);
+		List<PathModel> markdownDocs = JGitUtils.getDocuments(repository, extensions);
+		List<PathModel> markdownDocs2 = JGitUtils.getDocuments(repository,
+				Arrays.asList(new String[] { ".mkd", ".md" }));
+		List<PathModel> allFiles = JGitUtils.getDocuments(repository, null);
+		repository.close();
+		assertTrue(markdownDocs.size() > 0);
+		assertTrue(markdownDocs2.size() > 0);
+		assertTrue(allFiles.size() > markdownDocs.size());
+	}
+
+	public void testFileModes() throws Exception {
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.TREE.getBits()).equals("drwxr-xr-x"));
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.REGULAR_FILE.getBits()).equals(
+				"-rw-r--r--"));
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.EXECUTABLE_FILE.getBits()).equals(
+				"-rwxr-xr-x"));
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.SYMLINK.getBits()).equals("symlink"));
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.GITLINK.getBits()).equals("gitlink"));
+		assertTrue(JGitUtils.getPermissionsFromMode(FileMode.MISSING.getBits()).equals("missing"));
+	}
+
+	public void testRevlog() throws Exception {
+		List<RevCommit> commits = JGitUtils.getRevLog(null, 10);
+		assertTrue(commits.size() == 0);
+
+		Repository repository = GitBlitSuite.getHelloworldRepository();
+		// get most recent 10 commits
+		commits = JGitUtils.getRevLog(repository, 10);
+		assertTrue(commits.size() == 10);
+
+		// test paging and offset by getting the 10th most recent commit
+		RevCommit lastCommit = JGitUtils.getRevLog(repository, null, 9, 1).get(0);
+		assertTrue(commits.get(9).equals(lastCommit));
+
+		// grab the two most recent commits to java.java
+		commits = JGitUtils.getRevLog(repository, null, "java.java", 0, 2);
+		assertTrue(commits.size() == 2);
+		repository.close();
+	}
+
+	public void testSearchTypes() throws Exception {
+		assertTrue(SearchType.forName("commit").equals(SearchType.COMMIT));
+		assertTrue(SearchType.forName("committer").equals(SearchType.COMMITTER));
+		assertTrue(SearchType.forName("author").equals(SearchType.AUTHOR));
+		assertTrue(SearchType.forName("unknown").equals(SearchType.COMMIT));
+
+		assertTrue(SearchType.COMMIT.toString().equals("commit"));
+		assertTrue(SearchType.COMMITTER.toString().equals("committer"));
+		assertTrue(SearchType.AUTHOR.toString().equals("author"));
+	}
+
+	public void testSearchRevlogs() throws Exception {
+		List<RevCommit> results = JGitUtils.searchRevlogs(null, null, "java", SearchType.COMMIT, 0,
+				3);
+		assertTrue(results.size() == 0);
+
+		// test commit message search
+		Repository repository = GitBlitSuite.getHelloworldRepository();
+		results = JGitUtils.searchRevlogs(repository, null, "java", SearchType.COMMIT, 0, 3);
+		assertTrue(results.size() == 3);
+
+		// test author search
+		results = JGitUtils.searchRevlogs(repository, null, "timothy", SearchType.AUTHOR, 0, -1);
+		assertTrue(results.size() == 1);
+
+		// test committer search
+		results = JGitUtils.searchRevlogs(repository, null, "mike", SearchType.COMMITTER, 0, 10);
+		assertTrue(results.size() == 10);
+
+		// test paging and offset
+		RevCommit commit = JGitUtils.searchRevlogs(repository, null, "mike", SearchType.COMMITTER,
+				9, 1).get(0);
+		assertTrue(results.get(9).equals(commit));
+
+		repository.close();
 	}
 
 	public void testZip() throws Exception {
+		assertFalse(JGitUtils.zip(null, null, null, null));
 		Repository repository = GitBlitSuite.getHelloworldRepository();
-		File zipFile = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
-		FileOutputStream fos = new FileOutputStream(zipFile);
-		boolean success = JGitUtils.zip(repository, null, Constants.HEAD, fos);
-		assertTrue("Failed to generate zip file!", success);
-		assertTrue(zipFile.length() > 0);
-		fos.close();
-		zipFile.delete();
+		File zipFileA = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
+		FileOutputStream fosA = new FileOutputStream(zipFileA);
+		boolean successA = JGitUtils.zip(repository, null, Constants.HEAD, fosA);
+		fosA.close();
+
+		File zipFileB = new File(GitBlitSuite.REPOSITORIES, "helloworld-java.zip");
+		FileOutputStream fosB = new FileOutputStream(zipFileB);
+		boolean successB = JGitUtils.zip(repository, "java.java", Constants.HEAD, fosB);
+		fosB.close();
+
 		repository.close();
+		assertTrue("Failed to generate zip file!", successA);
+		assertTrue(zipFileA.length() > 0);
+		zipFileA.delete();
+
+		assertTrue("Failed to generate zip file!", successB);
+		assertTrue(zipFileB.length() > 0);
+		zipFileB.delete();
 	}
 }
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/MarkdownUtilsTest.java b/tests/com/gitblit/tests/MarkdownUtilsTest.java
new file mode 100644
index 0000000..2034bda
--- /dev/null
+++ b/tests/com/gitblit/tests/MarkdownUtilsTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.tests;
+
+import java.text.ParseException;
+
+import junit.framework.TestCase;
+
+import com.gitblit.utils.MarkdownUtils;
+
+public class MarkdownUtilsTest extends TestCase {
+
+	public void testMarkdown() throws Exception {
+		assertTrue(MarkdownUtils.transformMarkdown("# H1").equals("<h1> H1</h1>"));
+		assertTrue(MarkdownUtils.transformMarkdown("## H2").equals("<h2> H2</h2>"));
+		try {
+			MarkdownUtils.transformMarkdown((String) null);
+			assertTrue(false);
+		} catch (ParseException p) {
+			assertTrue(p != null);
+		}
+	}
+}
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/MetricUtilsTest.java b/tests/com/gitblit/tests/MetricUtilsTest.java
index 77f43a1..07cd606 100644
--- a/tests/com/gitblit/tests/MetricUtilsTest.java
+++ b/tests/com/gitblit/tests/MetricUtilsTest.java
@@ -28,8 +28,17 @@
 
 	public void testMetrics() throws Exception {
 		Repository repository = GitBlitSuite.getHelloworldRepository();
-		List<Metric> metrics = MetricUtils.getDateMetrics(repository, true);
+		List<Metric> metrics = MetricUtils.getDateMetrics(repository, true, null);
 		repository.close();
-		assertTrue("No metrics found!", metrics.size() > 0);
+		assertTrue("No date metrics found!", metrics.size() > 0);
+	}
+	
+	public void testAuthorMetrics() throws Exception {
+		Repository repository = GitBlitSuite.getHelloworldRepository();
+		List<Metric> byEmail = MetricUtils.getAuthorMetrics(repository, true);
+		List<Metric> byName = MetricUtils.getAuthorMetrics(repository, false);
+		repository.close();
+		assertTrue("No author metrics found!", byEmail.size() == 9);
+		assertTrue("No author metrics found!", byName.size() == 8);
 	}
 }
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/TicgitUtilsTest.java b/tests/com/gitblit/tests/TicgitUtilsTest.java
index 25dba2c..792c426 100644
--- a/tests/com/gitblit/tests/TicgitUtilsTest.java
+++ b/tests/com/gitblit/tests/TicgitUtilsTest.java
@@ -28,10 +28,20 @@
 
 public class TicgitUtilsTest extends TestCase {
 
-	public void testTicGit() throws Exception {
+	public void testTicgitBranch() throws Exception {
 		Repository repository = GitBlitSuite.getTicgitRepository();
 		RefModel branch = TicgitUtils.getTicketsBranch(repository);
+		repository.close();
 		assertTrue("Ticgit branch does not exist!", branch != null);
+		
+		repository = GitBlitSuite.getHelloworldRepository();
+		branch = TicgitUtils.getTicketsBranch(repository);
+		repository.close();
+		assertTrue("Ticgit branch exists!", branch == null);
+	}
+
+	public void testRetrieveTickets() throws Exception {
+		Repository repository = GitBlitSuite.getTicgitRepository();
 		List<TicketModel> ticketsA = TicgitUtils.getTickets(repository);
 		List<TicketModel> ticketsB = TicgitUtils.getTickets(repository);
 		repository.close();
@@ -50,5 +60,21 @@
 				assertTrue(commentA.hashCode() == commentA.text.hashCode());
 			}
 		}
+		
+		repository = GitBlitSuite.getHelloworldRepository();
+		List<TicketModel> ticketsC = TicgitUtils.getTickets(repository);
+		repository.close();
+		assertTrue(ticketsC == null);
+	}
+
+	public void testReadTicket() throws Exception {
+		Repository repository = GitBlitSuite.getTicgitRepository();
+		List<TicketModel> tickets = TicgitUtils.getTickets(repository);
+		TicketModel ticket = TicgitUtils
+				.getTicket(repository, tickets.get(tickets.size() - 1).name);
+		repository.close();
+		assertTrue(ticket != null);
+		assertTrue(ticket.name
+				.equals("1254123752_comments-on-ticgits-longer-than-5-lines-can-t-be-viewed-entirely_266"));
 	}
 }
\ No newline at end of file

--
Gitblit v1.9.1