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 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   ### 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. 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 { 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."); } } 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; String getString(String name, String defaultValue); public IStoredSettings(Class<? extends IStoredSettings> clazz) { logger = LoggerFactory.getLogger(clazz); } List<String> getStrings(String name); protected abstract Properties read(); List<String> getStringsFromValue(String value); 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, String separator); 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, String separator); 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; } 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; } 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; } } 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 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)); 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; } } 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)); } }; src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -227,8 +227,17 @@ } protected void addFullText(String wicketId, String text, boolean substituteRegex) { String html = StringUtils.breakLinesForHtml(text); String html; if (substituteRegex) { html = substituteText(text); } else { html = StringUtils.breakLinesForHtml(text); } add(new Label(wicketId, html).setEscapeModelStrings(false)); } 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)) { @@ -259,8 +268,7 @@ + definition); } } } add(new Label(wicketId, html).setEscapeModelStrings(false)); return html; } protected abstract String getPageName(); 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; tests/com/gitblit/tests/DiffUtilsTest.java
@@ -15,16 +15,26 @@ */ 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(); @@ -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")); } } 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); } } 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")); 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")); } }