From f339f5de2ee6d354f55e14e9340bebc4611535b3 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 09 Jun 2011 19:04:24 -0400
Subject: [PATCH] Unit testing. Documentation. Simplified settings classes.

---
 src/com/gitblit/utils/StringUtils.java           |   22 ++
 tests/com/gitblit/tests/GitBlitTest.java         |   78 +++++++
 src/com/gitblit/FileSettings.java                |  126 +-----------
 tests/com/gitblit/tests/DiffUtilsTest.java       |   18 +
 src/com/gitblit/WebXmlSettings.java              |   64 +----
 tests/com/gitblit/tests/StringUtilsTest.java     |   10 +
 src/com/gitblit/IStoredSettings.java             |   81 +++++++
 src/com/gitblit/wicket/pages/RepositoryPage.java |   66 +++--
 src/com/gitblit/wicket/resources/gitblit.css     |    4 
 src/com/gitblit/GitBlit.java                     |   14 -
 src/com/gitblit/wicket/pages/CommitPage.java     |    3 
 src/com/gitblit/utils/DiffUtils.java             |   35 +-
 tests/com/gitblit/tests/JGitUtilsTest.java       |    1 
 docs/00_index.mkd                                |   23 +-
 14 files changed, 301 insertions(+), 244 deletions(-)

diff --git a/docs/00_index.mkd b/docs/00_index.mkd
index 2f15b38..fbd736a 100644
--- a/docs/00_index.mkd
+++ b/docs/00_index.mkd
@@ -31,18 +31,17 @@
 - Repository Owners may edit repositories through the web UI
 - Automatically generates a self-signed certificate for https communications
 - Git-notes support
-- Branch metrics
+- Branch metrics (uses Google Charts)
 - Blame annotations view
 - Dates can optionally be displayed using the browser's reported timezone
-- Author and Committer email address display can be controlled
-- Search commit messages, authors, and committers
+- Display of Author and Committer email addresses can be disabled
+- Case-insensitive searching of commit messages, authors, or committers
 - Dynamic zip downloads feature
-- Markdown view support
-- Syntax highlighting
-- Customizable regular expression handling for commit messages
+- Markdown file view support
+- Syntax highlighting for popular source code types
+- Customizable regular expression substitution for commit messages (i.e. bug or code review link integration)
 - Single text file for server configuration
 - Single text file for users configuration
-- Simple repository stats and activity graph (uses Google Charts)
 - Optional utility pages
     <ul class='noBullets'>
     <li>![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository</li>
@@ -50,24 +49,23 @@
     </ul>
 
 ### Limitations
-- [%JGIT%][jgit] does not [garbage collect or repack](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)
+- [%JGIT%][jgit] does not currently [garbage collect or repack](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)
 - HTTP/HTTPS are the only supported protocols
 - Access controls are not path-based, they are repository-based
 - Only Administrators can create, rename or delete repositories
 - Gitblit is an integrated, full-stack solution.  There is no WAR build at this time.
 
 ### Caveats
-- I don't know everything there is to know about [Git][git] nor [JGit][jgit].
 - Gitblit may eat your data.  Use at your own risk.
 - Gitblit may have security holes.  Patches welcome.  :)
 
 ### Todo List
 - Code documentation
 - Unit testing
-- Finish Blame (waiting for JGit 1.0.0 release)
-- Clone remote repository
+- Update Build.java to JGit 1.0.0, when its released
 
 ### Idea List
+- Consider clone remote repository feature
 - Consider [Apache Shiro](http://shiro.apache.org) for authentication
 - Stronger Ticgit read-only integration
     - activity/timeline
@@ -88,7 +86,7 @@
 
 ## Architecture
 
-![block diagram](architecture.png "Git Blit Architecture")
+![block diagram](architecture.png "Gitblit Architecture")
 
 ### Bundled Dependencies
 The following dependencies are bundled with the Gitblit zip distribution file.
@@ -116,6 +114,7 @@
 ### Other Build Dependencies
 - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
 - [JUnit](http://junit.org) (Common Public License)
+- [commons-net](http://commons.apache.org/net) (Apache 2.0)
 
 ## Building from Source
 [Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured.
diff --git a/src/com/gitblit/FileSettings.java b/src/com/gitblit/FileSettings.java
index 01176c0..b70daa0 100644
--- a/src/com/gitblit/FileSettings.java
+++ b/src/com/gitblit/FileSettings.java
@@ -18,144 +18,42 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Properties;
-import java.util.regex.PatternSyntaxException;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Reads GitBlit settings file.
  * 
  */
-public class FileSettings implements IStoredSettings {
-
-	private final Logger logger = LoggerFactory.getLogger(FileSettings.class);
+public class FileSettings extends IStoredSettings {
 
 	private final File propertiesFile;
 
-	private Properties properties = new Properties();
+	private final Properties properties = new Properties();
 
-	private long lastread;
+	private volatile long lastread;
 
 	public FileSettings(String file) {
+		super(FileSettings.class);
 		this.propertiesFile = new File(file);
 	}
 
 	@Override
-	public List<String> getAllKeys(String startingWith) {
-		startingWith = startingWith.toLowerCase();
-		List<String> keys = new ArrayList<String>();
-		Properties props = read();
-		for (Object o : props.keySet()) {
-			String key = o.toString().toLowerCase();
-			if (key.startsWith(startingWith)) {
-				keys.add(key);
-			}
-		}
-		return keys;
-	}
-
-	@Override
-	public boolean getBoolean(String name, boolean defaultValue) {
-		Properties props = read();
-		if (props.containsKey(name)) {
-			try {
-				String value = props.getProperty(name);
-				if (value != null && value.trim().length() > 0) {
-					return Boolean.parseBoolean(value);
-				}
-			} catch (Exception e) {
-				logger.warn("No override setting for " + name + " using default of " + defaultValue);
-			}
-		}
-		return defaultValue;
-	}
-
-	@Override
-	public int getInteger(String name, int defaultValue) {
-		Properties props = read();
-		if (props.containsKey(name)) {
-			try {
-				String value = props.getProperty(name);
-				if (value != null && value.trim().length() > 0) {
-					return Integer.parseInt(value);
-				}
-			} catch (Exception e) {
-				logger.warn("No override setting for " + name + " using default of " + defaultValue);
-			}
-		}
-		return defaultValue;
-	}
-
-	@Override
-	public String getString(String name, String defaultValue) {
-		Properties props = read();
-		if (props.containsKey(name)) {
-			try {
-				String value = props.getProperty(name);
-				if (value != null) {
-					return value;
-				}
-			} catch (Exception e) {
-				logger.warn("No override setting for " + name + " using default of " + defaultValue);
-			}
-		}
-		return defaultValue;
-	}
-
-	@Override
-	public List<String> getStrings(String name) {
-		return getStrings(name, " ");
-	}
-
-	@Override
-	public List<String> getStringsFromValue(String value) {
-		return getStringsFromValue(value, " ");
-	}
-
-	@Override
-	public List<String> getStrings(String name, String separator) {
-		List<String> strings = new ArrayList<String>();
-		Properties props = read();
-		if (props.containsKey(name)) {
-			String value = props.getProperty(name);
-			strings = getStringsFromValue(value, separator);
-		}
-		return strings;
-	}
-
-	@Override
-	public List<String> getStringsFromValue(String value, String separator) {
-		List<String> strings = new ArrayList<String>();
-		try {
-			String[] chunks = value.split(separator);
-			for (String chunk : chunks) {
-				chunk = chunk.trim();
-				if (chunk.length() > 0) {
-					strings.add(chunk);
-				}
-			}
-		} catch (PatternSyntaxException e) {
-			logger.error("Failed to parse " + value, e);
-		}
-		return strings;
-	}
-
-	private synchronized Properties read() {
+	protected synchronized Properties read() {
 		if (propertiesFile.exists() && (propertiesFile.lastModified() > lastread)) {
 			FileInputStream is = null;
 			try {
-				properties = new Properties();
+				Properties props = new Properties();
 				is = new FileInputStream(propertiesFile);
-				properties.load(is);
+				props.load(is);
+				
+				// load properties after we have successfully read file
+				properties.clear();
+				properties.putAll(props);
 				lastread = propertiesFile.lastModified();
 			} catch (FileNotFoundException f) {
 				// IGNORE - won't happen because file.exists() check above
 			} catch (Throwable t) {
-				t.printStackTrace();
+				logger.error("Failed to read " + propertiesFile.getName(), t);
 			} finally {
 				if (is != null) {
 					try {
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index dcf5a6b..0132623 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -312,17 +312,6 @@
 		return false;
 	}
 
-	public boolean renameRepository(RepositoryModel model, String newName) {
-		File folder = new File(repositoriesFolder, model.name);
-		if (folder.exists() && folder.isDirectory()) {
-			File newFolder = new File(repositoriesFolder, newName);
-			if (folder.renameTo(newFolder)) {
-				return loginService.renameRole(model.name, newName);
-			}
-		}
-		return false;
-	}
-
 	public void configureContext(IStoredSettings settings) {
 		logger.info("Reading configuration from " + settings.toString());
 		this.storedSettings = settings;
@@ -334,6 +323,7 @@
 	@Override
 	public void contextInitialized(ServletContextEvent contextEvent) {
 		if (storedSettings == null) {
+			// for running gitblit as a traditional webapp in a servlet container
 			WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
 			configureContext(webxmlSettings);
 		}
@@ -341,6 +331,6 @@
 
 	@Override
 	public void contextDestroyed(ServletContextEvent contextEvent) {
-		logger.info("GitBlit context destroyed by servlet container.");
+		logger.info("Gitblit context destroyed by servlet container.");
 	}
 }
diff --git a/src/com/gitblit/IStoredSettings.java b/src/com/gitblit/IStoredSettings.java
index 07d21e2..7108c06 100644
--- a/src/com/gitblit/IStoredSettings.java
+++ b/src/com/gitblit/IStoredSettings.java
@@ -15,24 +15,87 @@
  */
 package com.gitblit;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Properties;
 
-public interface IStoredSettings {
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-	List<String> getAllKeys(String startingWith);
+import com.gitblit.utils.StringUtils;
 
-	boolean getBoolean(String name, boolean defaultValue);
+public abstract class IStoredSettings {
 
-	int getInteger(String name, int defaultValue);
+	protected final Logger logger;
+	
+	public IStoredSettings(Class<? extends IStoredSettings> clazz) {
+		logger = LoggerFactory.getLogger(clazz);
+	}
+		
+	protected abstract Properties read();
 
-	String getString(String name, String defaultValue);
+	public List<String> getAllKeys(String startingWith) {
+		startingWith = startingWith.toLowerCase();
+		List<String> keys = new ArrayList<String>();
+		Properties props = read();
+		for (Object o : props.keySet()) {
+			String key = o.toString();
+			if (key.toLowerCase().startsWith(startingWith)) {
+				keys.add(key);
+			}
+		}
+		return keys;
+	}
 
-	List<String> getStrings(String name);
+	public boolean getBoolean(String name, boolean defaultValue) {
+		Properties props = read();
+		if (props.containsKey(name)) {
+			String value = props.getProperty(name);
+			if (!StringUtils.isEmpty(value)) {
+				return Boolean.parseBoolean(value);
+			}
+		}
+		return defaultValue;
+	}
 
-	List<String> getStringsFromValue(String value);
+	public int getInteger(String name, int defaultValue) {
+		Properties props = read();
+		if (props.containsKey(name)) {
+			try {
+				String value = props.getProperty(name);
+				if (!StringUtils.isEmpty(value)) {
+					return Integer.parseInt(value);
+				}
+			} catch (NumberFormatException e) {
+				logger.warn("Failed to parse integer for " + name + " using default of "
+						+ defaultValue);
+			}
+		}
+		return defaultValue;
+	}
 
-	List<String> getStrings(String name, String separator);
+	public String getString(String name, String defaultValue) {
+		Properties props = read();
+		if (props.containsKey(name)) {
+			String value = props.getProperty(name);
+			if (value != null) {
+				return value;
+			}
+		}
+		return defaultValue;
+	}
 
-	List<String> getStringsFromValue(String value, String separator);
+	public List<String> getStrings(String name) {
+		return getStrings(name, " ");
+	}
 
+	public List<String> getStrings(String name, String separator) {
+		List<String> strings = new ArrayList<String>();
+		Properties props = read();
+		if (props.containsKey(name)) {
+			String value = props.getProperty(name);
+			strings = StringUtils.getStringsFromValue(value, separator);
+		}
+		return strings;
+	}
 }
\ No newline at end of file
diff --git a/src/com/gitblit/WebXmlSettings.java b/src/com/gitblit/WebXmlSettings.java
index 6ca38f7..0ff2a3e 100644
--- a/src/com/gitblit/WebXmlSettings.java
+++ b/src/com/gitblit/WebXmlSettings.java
@@ -15,62 +15,28 @@
  */
 package com.gitblit;
 
-import java.util.List;
+import java.util.Enumeration;
+import java.util.Properties;
 
 import javax.servlet.ServletContext;
 
-public class WebXmlSettings implements IStoredSettings {
+public class WebXmlSettings extends IStoredSettings {
 
+	private final Properties properties = new Properties();
+	
 	public WebXmlSettings(ServletContext context) {
-
+		super(WebXmlSettings.class);
+		Enumeration<?> keys = context.getInitParameterNames();
+		while (keys.hasMoreElements()) {
+			String key = keys.nextElement().toString();
+			String value = context.getInitParameter(key);
+			properties.put(key, value);
+		}
 	}
-
+	
 	@Override
-	public List<String> getAllKeys(String startingWith) {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public boolean getBoolean(String name, boolean defaultValue) {
-		// TODO Auto-generated method stub
-		return false;
-	}
-
-	@Override
-	public int getInteger(String name, int defaultValue) {
-		// TODO Auto-generated method stub
-		return 0;
-	}
-
-	@Override
-	public String getString(String name, String defaultValue) {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public List<String> getStrings(String name) {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public List<String> getStringsFromValue(String value) {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public List<String> getStrings(String name, String separator) {
-		// TODO Auto-generated method stub
-		return null;
-	}
-
-	@Override
-	public List<String> getStringsFromValue(String value, String separator) {
-		// TODO Auto-generated method stub
-		return null;
+	protected Properties read() {
+		return properties;
 	}
 
 	@Override
diff --git a/src/com/gitblit/utils/DiffUtils.java b/src/com/gitblit/utils/DiffUtils.java
index 0f56907..c1401f9 100644
--- a/src/com/gitblit/utils/DiffUtils.java
+++ b/src/com/gitblit/utils/DiffUtils.java
@@ -25,6 +25,7 @@
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -69,6 +70,7 @@
 
 	public static String getDiff(Repository r, RevCommit baseCommit, RevCommit commit, String path,
 			DiffOutputType outputType) {
+		String diff = null;
 		try {
 			RevTree baseTree;
 			if (baseCommit == null) {
@@ -107,18 +109,17 @@
 			df.setRepository(r);
 			df.setDiffComparator(cmp);
 			df.setDetectRenames(true);
-			List<DiffEntry> diffs = df.scan(baseTree, commitTree);
+			List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
 			if (path != null && path.length() > 0) {
-				for (DiffEntry diff : diffs) {
-					if (diff.getNewPath().equalsIgnoreCase(path)) {
-						df.format(diff);
+				for (DiffEntry diffEntry : diffEntries) {
+					if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+						df.format(diffEntry);
 						break;
 					}
 				}
 			} else {
-				df.format(diffs);
+				df.format(diffEntries);
 			}
-			String diff;
 			if (df instanceof GitWebDiffFormatter) {
 				// workaround for complex private methods in DiffFormatter
 				diff = ((GitWebDiffFormatter) df).getHtml();
@@ -126,15 +127,15 @@
 				diff = os.toString();
 			}
 			df.flush();
-			return diff;
 		} catch (Throwable t) {
 			LOGGER.error("failed to generate commit diff!", t);
 		}
-		return null;
+		return diff;
 	}
 
 	public static String getCommitPatch(Repository r, RevCommit baseCommit, RevCommit commit,
 			String path) {
+		String diff = null;
 		try {
 			RevTree baseTree;
 			if (baseCommit == null) {
@@ -159,29 +160,31 @@
 			df.setRepository(r);
 			df.setDiffComparator(cmp);
 			df.setDetectRenames(true);
-			List<DiffEntry> diffs = df.scan(baseTree, commitTree);
+			List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
 			if (path != null && path.length() > 0) {
-				for (DiffEntry diff : diffs) {
-					if (diff.getNewPath().equalsIgnoreCase(path)) {
-						df.format(diff);
+				for (DiffEntry diffEntry : diffEntries) {
+					if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+						df.format(diffEntry);
 						break;
 					}
 				}
 			} else {
-				df.format(diffs);
+				df.format(diffEntries);
 			}
-			String diff = df.getPatch(commit);
+			diff = df.getPatch(commit);
 			df.flush();
-			return diff;
 		} catch (Throwable t) {
 			LOGGER.error("failed to generate commit diff!", t);
 		}
-		return null;
+		return diff;
 	}
 
 	public static List<AnnotatedLine> blame(Repository r, String blobPath, String objectId) {
 		List<AnnotatedLine> lines = new ArrayList<AnnotatedLine>();
 		try {
+			if (StringUtils.isEmpty(objectId)) {
+				objectId = Constants.HEAD;
+			}
 			BlameCommand blameCommand = new BlameCommand(r);
 			blameCommand.setFilePath(blobPath);
 			blameCommand.setStartCommit(r.resolve(objectId));
diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java
index a881b58..fa84fe8 100644
--- a/src/com/gitblit/utils/StringUtils.java
+++ b/src/com/gitblit/utils/StringUtils.java
@@ -18,7 +18,9 @@
 import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.PatternSyntaxException;
 
 public class StringUtils {
 
@@ -142,4 +144,24 @@
 		}
 		return relativePath;
 	}
+	
+	public static List<String> getStringsFromValue(String value) {
+		return getStringsFromValue(value, " ");
+	}
+		
+	public static List<String> getStringsFromValue(String value, String separator) {
+		List<String> strings = new ArrayList<String>();
+		try {
+			String[] chunks = value.split(separator);
+			for (String chunk : chunks) {
+				chunk = chunk.trim();
+				if (chunk.length() > 0) {
+					strings.add(chunk);
+				}
+			}
+		} catch (PatternSyntaxException e) {
+			throw new RuntimeException(e);
+		}
+		return strings;
+	}
 }
diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java
index 0295600..6da962e 100644
--- a/src/com/gitblit/wicket/pages/CommitPage.java
+++ b/src/com/gitblit/wicket/pages/CommitPage.java
@@ -38,7 +38,6 @@
 import com.gitblit.models.PathModel.PathChangeModel;
 import com.gitblit.utils.JGitUtils;
 import com.gitblit.utils.JGitUtils.SearchType;
-import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.WicketUtils;
 import com.gitblit.wicket.panels.CommitHeaderPanel;
 import com.gitblit.wicket.panels.CommitLegendPanel;
@@ -129,7 +128,7 @@
 						SearchType.AUTHOR));
 				item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
 						.getAuthorIdent().getWhen(), getTimeZone()));
-				item.add(new Label("noteContent", StringUtils.breakLinesForHtml(entry.content))
+				item.add(new Label("noteContent", substituteText(entry.content))
 						.setEscapeModelStrings(false));
 			}
 		};
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java
index e2120f7..cff59f2 100644
--- a/src/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -227,40 +227,48 @@
 	}
 
 	protected void addFullText(String wicketId, String text, boolean substituteRegex) {
-		String html = StringUtils.breakLinesForHtml(text);
+		String html;
 		if (substituteRegex) {
-			Map<String, String> map = new HashMap<String, String>();
-			// global regex keys
-			if (GitBlit.getBoolean(Keys.regex.global, false)) {
-				for (String key : GitBlit.getAllKeys(Keys.regex.global)) {
-					if (!key.equals(Keys.regex.global)) {
-						String subKey = key.substring(key.lastIndexOf('.') + 1);
-						map.put(subKey, GitBlit.getString(key, ""));
-					}
-				}
-			}
+			html = substituteText(text);
+		} else {
+			html = StringUtils.breakLinesForHtml(text);
+		}
+		add(new Label(wicketId, html).setEscapeModelStrings(false));
+	}
 
-			// repository-specific regex keys
-			List<String> keys = GitBlit.getAllKeys(Keys.regex._ROOT + "."
-					+ repositoryName.toLowerCase());
-			for (String key : keys) {
-				String subKey = key.substring(key.lastIndexOf('.') + 1);
-				map.put(subKey, GitBlit.getString(key, ""));
-			}
-
-			for (Entry<String, String> entry : map.entrySet()) {
-				String definition = entry.getValue().trim();
-				String[] chunks = definition.split("!!!");
-				if (chunks.length == 2) {
-					html = html.replaceAll(chunks[0], chunks[1]);
-				} else {
-					logger.warn(entry.getKey()
-							+ " improperly formatted.  Use !!! to separate match from replacement: "
-							+ definition);
+	protected String substituteText(String text) {
+		String html = StringUtils.breakLinesForHtml(text);
+		Map<String, String> map = new HashMap<String, String>();
+		// global regex keys
+		if (GitBlit.getBoolean(Keys.regex.global, false)) {
+			for (String key : GitBlit.getAllKeys(Keys.regex.global)) {
+				if (!key.equals(Keys.regex.global)) {
+					String subKey = key.substring(key.lastIndexOf('.') + 1);
+					map.put(subKey, GitBlit.getString(key, ""));
 				}
 			}
 		}
-		add(new Label(wicketId, html).setEscapeModelStrings(false));
+
+		// repository-specific regex keys
+		List<String> keys = GitBlit.getAllKeys(Keys.regex._ROOT + "."
+				+ repositoryName.toLowerCase());
+		for (String key : keys) {
+			String subKey = key.substring(key.lastIndexOf('.') + 1);
+			map.put(subKey, GitBlit.getString(key, ""));
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			String definition = entry.getValue().trim();
+			String[] chunks = definition.split("!!!");
+			if (chunks.length == 2) {
+				html = html.replaceAll(chunks[0], chunks[1]);
+			} else {
+				logger.warn(entry.getKey()
+						+ " improperly formatted.  Use !!! to separate match from replacement: "
+						+ definition);
+			}
+		}
+		return html;
 	}
 
 	protected abstract String getPageName();
diff --git a/src/com/gitblit/wicket/resources/gitblit.css b/src/com/gitblit/wicket/resources/gitblit.css
index 4309458..7143f85 100644
--- a/src/com/gitblit/wicket/resources/gitblit.css
+++ b/src/com/gitblit/wicket/resources/gitblit.css
@@ -249,6 +249,10 @@
 	border-width: 1px 0px 0px;
 }
 
+div.commit_message a {
+	font-family: monospace;
+}
+
 div.bug_open, span.bug_open {
 	padding: 2px;
 	background-color: #803333;
diff --git a/tests/com/gitblit/tests/DiffUtilsTest.java b/tests/com/gitblit/tests/DiffUtilsTest.java
index e60a2a4..84353c1 100644
--- a/tests/com/gitblit/tests/DiffUtilsTest.java
+++ b/tests/com/gitblit/tests/DiffUtilsTest.java
@@ -15,17 +15,27 @@
  */
 package com.gitblit.tests;
 
+import java.util.List;
+
 import junit.framework.TestCase;
 
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 
+import com.gitblit.models.AnnotatedLine;
 import com.gitblit.utils.DiffUtils;
 import com.gitblit.utils.DiffUtils.DiffOutputType;
 import com.gitblit.utils.JGitUtils;
 
 public class DiffUtilsTest extends TestCase {
 
+	public void testDiffOutputTypes() throws Exception {
+		assertTrue(DiffOutputType.forName("plain").equals(DiffOutputType.PLAIN));
+		assertTrue(DiffOutputType.forName("gitweb").equals(DiffOutputType.GITWEB));
+		assertTrue(DiffOutputType.forName("gitblit").equals(DiffOutputType.GITBLIT));
+		assertTrue(DiffOutputType.forName(null) == null);
+	}
+	
 	public void testParentCommitDiff() throws Exception {
 		Repository repository = GitBlitSuite.getHelloworldRepository();
 		RevCommit commit = JGitUtils.getCommit(repository,
@@ -97,4 +107,12 @@
 		String expected = "-		system.out.println(\"Hello World\");\n+		System.out.println(\"Hello World\"";
 		assertTrue(patch.indexOf(expected) > -1);
 	}
+	
+	public void testBlame() throws Exception {
+		Repository repository = GitBlitSuite.getHelloworldRepository();
+		List<AnnotatedLine> lines = DiffUtils.blame(repository, "java.java", "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+		repository.close();
+		assertTrue(lines.size() > 0);
+		assertTrue(lines.get(0).commitId.equals("c6d31dccf5cc75e8e46299fc62d38f60ec6d41e0"));
+	}
 }
diff --git a/tests/com/gitblit/tests/GitBlitTest.java b/tests/com/gitblit/tests/GitBlitTest.java
index 853ca39..13705f1 100644
--- a/tests/com/gitblit/tests/GitBlitTest.java
+++ b/tests/com/gitblit/tests/GitBlitTest.java
@@ -19,6 +19,8 @@
 
 import junit.framework.TestCase;
 
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.FileSettings;
 import com.gitblit.GitBlit;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
@@ -47,10 +49,84 @@
 		assertTrue(model.toString().equals("admin"));
 		assertTrue("Admin missing #admin role!", model.canAdmin);
 		model.canAdmin = false;
-		assertFalse("Admin should not hae #admin!", model.canAdmin);
+		assertFalse("Admin should not have #admin!", model.canAdmin);
 		String repository = GitBlitSuite.getHelloworldRepository().getDirectory().getName();
 		assertFalse("Admin can still access repository!", model.canAccessRepository(repository));
 		model.addRepository(repository);
 		assertTrue("Admin can't access repository!", model.canAccessRepository(repository));
 	}
+	
+	public void testAccessRestrictionTypes() throws Exception {
+		assertTrue(AccessRestrictionType.PUSH.exceeds(AccessRestrictionType.NONE));
+		assertTrue(AccessRestrictionType.CLONE.exceeds(AccessRestrictionType.PUSH));
+		assertTrue(AccessRestrictionType.VIEW.exceeds(AccessRestrictionType.CLONE));
+
+		assertFalse(AccessRestrictionType.NONE.exceeds(AccessRestrictionType.PUSH));
+		assertFalse(AccessRestrictionType.PUSH.exceeds(AccessRestrictionType.CLONE));
+		assertFalse(AccessRestrictionType.CLONE.exceeds(AccessRestrictionType.VIEW));
+
+		assertTrue(AccessRestrictionType.PUSH.atLeast(AccessRestrictionType.NONE));
+		assertTrue(AccessRestrictionType.CLONE.atLeast(AccessRestrictionType.PUSH));
+		assertTrue(AccessRestrictionType.VIEW.atLeast(AccessRestrictionType.CLONE));
+
+		assertFalse(AccessRestrictionType.NONE.atLeast(AccessRestrictionType.PUSH));
+		assertFalse(AccessRestrictionType.PUSH.atLeast(AccessRestrictionType.CLONE));
+		assertFalse(AccessRestrictionType.CLONE.atLeast(AccessRestrictionType.VIEW));
+		
+		assertTrue(AccessRestrictionType.PUSH.toString().equals("PUSH"));
+		assertTrue(AccessRestrictionType.CLONE.toString().equals("CLONE"));
+		assertTrue(AccessRestrictionType.VIEW.toString().equals("VIEW"));
+
+		assertTrue(AccessRestrictionType.fromName("none").equals(AccessRestrictionType.NONE));
+		assertTrue(AccessRestrictionType.fromName("push").equals(AccessRestrictionType.PUSH));
+		assertTrue(AccessRestrictionType.fromName("clone").equals(AccessRestrictionType.CLONE));
+		assertTrue(AccessRestrictionType.fromName("view").equals(AccessRestrictionType.VIEW));
+	}
+	
+	public void testFileSettings() throws Exception {
+		FileSettings settings = new FileSettings("distrib/gitblit.properties");
+		assertTrue(settings.getBoolean("missing", true) == true);
+		assertTrue(settings.getString("missing", "default").equals("default"));
+		assertTrue(settings.getInteger("missing", 10) == 10);
+		assertTrue(settings.getInteger("realm.realmFile", 5) == 5);
+		
+		assertTrue(settings.getBoolean("git.enableGitServlet", false) == true);
+		assertTrue(settings.getString("realm.realmFile", null).equals("users.properties"));
+		assertTrue(settings.getInteger("realm.minPasswordLength", 0) == 5);
+		List<String> mdExtensions = settings.getStrings("web.markdownExtensions");
+		assertTrue(mdExtensions.size() > 0);
+		assertTrue(mdExtensions.contains("md"));
+		
+		List<String> keys = settings.getAllKeys("server");
+		assertTrue(keys.size() > 0);
+		assertTrue(keys.contains("server.httpsPort"));
+	}
+	
+	public void testGitblitSettings() throws Exception {
+		// These are already tested by above test method.
+		assertTrue(GitBlit.getBoolean("missing", true) == true);
+		assertTrue(GitBlit.getString("missing", "default").equals("default"));
+		assertTrue(GitBlit.getInteger("missing", 10) == 10);
+		assertTrue(GitBlit.getInteger("realm.realmFile", 5) == 5);
+		
+		assertTrue(GitBlit.getBoolean("git.enableGitServlet", false) == true);
+		assertTrue(GitBlit.getString("realm.realmFile", null).equals("users.properties"));
+		assertTrue(GitBlit.getInteger("realm.minPasswordLength", 0) == 5);
+		List<String> mdExtensions = GitBlit.getStrings("web.markdownExtensions");
+		assertTrue(mdExtensions.size() > 0);
+		assertTrue(mdExtensions.contains("md"));
+		
+		List<String> keys = GitBlit.getAllKeys("server");
+		assertTrue(keys.size() > 0);
+		assertTrue(keys.contains("server.httpsPort"));
+	}
+	
+	public void testAuthentication() throws Exception  {
+		assertTrue(GitBlit.self().authenticate("admin", "admin".toCharArray()) != null);
+	}
+	
+	public void testRepositories() throws Exception  {
+		assertTrue(GitBlit.self().getRepository("missing") == null);
+		assertTrue(GitBlit.self().getRepositoryModel("missing") == null);
+	}
 }
diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java
index 1b57585..19a4847 100644
--- a/tests/com/gitblit/tests/JGitUtilsTest.java
+++ b/tests/com/gitblit/tests/JGitUtilsTest.java
@@ -122,6 +122,7 @@
 			List<RefModel> list = entry.getValue();
 			for (RefModel ref : list) {
 				if (ref.displayName.equals("refs/tags/spearce-gpg-pub")) {
+					assertTrue(ref.toString().equals("refs/tags/spearce-gpg-pub"));
 					assertTrue(ref.getObjectId().getName()
 							.equals("8bbde7aacf771a9afb6992434f1ae413e010c6d8"));
 					assertTrue(ref.getAuthorIdent().getEmailAddress().equals("spearce@spearce.org"));
diff --git a/tests/com/gitblit/tests/StringUtilsTest.java b/tests/com/gitblit/tests/StringUtilsTest.java
index df51a4b..b0d9a1f 100644
--- a/tests/com/gitblit/tests/StringUtilsTest.java
+++ b/tests/com/gitblit/tests/StringUtilsTest.java
@@ -16,6 +16,7 @@
 package com.gitblit.tests;
 
 import java.util.Arrays;
+import java.util.List;
 
 import junit.framework.TestCase;
 
@@ -76,4 +77,13 @@
 		assertTrue(StringUtils.getRootPath(input).equals(output));
 		assertTrue(StringUtils.getRootPath("repository").equals(""));
 	}
+	
+	public void testStringsFromValue() throws Exception {
+		List<String> strings = StringUtils.getStringsFromValue("A B C D");
+		assertTrue(strings.size() == 4);
+		assertTrue(strings.get(0).equals("A"));
+		assertTrue(strings.get(1).equals("B"));
+		assertTrue(strings.get(2).equals("C"));
+		assertTrue(strings.get(3).equals("D"));
+	}
 }

--
Gitblit v1.9.1