James Moger
2011-06-25 85c2e6eb34215e2242e388a8f8b7173a14b96ad3
Big push for first release.

* Build script overhaul including building & publishing GO, WAR, Docs,
and Site.
* Restored JGit 0.12.1 dependency and backported Blame. Got tired of
waiting for JGit 1.0.0 Maven artifacts.
* Changed Summary Page layout
* Optional cookie authentication
* Added icons for log, tags, and branches panels.
* Show last commit author and short message on branches panel.
* Unit testing.
* Documentation.
12 files added
6 files renamed
42 files modified
1 files deleted
3788 ■■■■ changed files
.classpath 43 ●●●●● patch | view | raw | blame | history
NOTICE 8 ●●●●● patch | view | raw | blame | history
README.MKD 10 ●●●●● patch | view | raw | blame | history
build.xml 501 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties 32 ●●●●● patch | view | raw | blame | history
distrib/installService.cmd 2 ●●● patch | view | raw | blame | history
distrib/installService64.cmd 2 ●●● patch | view | raw | blame | history
docs/00_index.mkd 158 ●●●● patch | view | raw | blame | history
docs/01_features.mkd 64 ●●●●● patch | view | raw | blame | history
docs/01_releases.mkd 1 ●●●● patch | view | raw | blame | history
docs/01_setup.mkd 80 ●●●●● patch | view | raw | blame | history
docs/02_faq.mkd 27 ●●●● patch | view | raw | blame | history
docs/03_properties.mkd patch | view | raw | blame | history
docs/04_design.mkd 69 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 9 ●●●●● patch | view | raw | blame | history
resources/commit_changes_16x16.png patch | view | raw | blame | history
resources/gitblit.css 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/Build.java 21 ●●●● patch | view | raw | blame | history
src/com/gitblit/BuildThumbnails.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/BuildWebXml.java 40 ●●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/FileSettings.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/FileUserService.java 65 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 82 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServer.java 18 ●●●● patch | view | raw | blame | history
src/com/gitblit/IStoredSettings.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/IUserService.java 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/SyndicationServlet.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/WebXmlSettings.java 12 ●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 15 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 7 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 46 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/DocsPage.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LogPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LoginPage.java 31 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LogoutPage.java 7 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 9 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.html 12 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 17 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/BranchesPanel.html 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/BranchesPanel.java 32 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/LogPanel.html 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/SearchPanel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/TagsPanel.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/TagsPanel.java 11 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/api/BlameCommand.java 227 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/blame/BlameGenerator.java 961 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/blame/BlameResult.java 356 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/blame/Candidate.java 386 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/blame/Region.java 133 ●●●●● patch | view | raw | blame | history
src/org/eclipse/jgit/blame/ReverseWalk.java 113 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/DiffUtilsTest.java 7 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitBlitSuite.java 7 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitBlitTest.java 30 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/JGitUtilsTest.java 4 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/StringUtilsTest.java 19 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/SyndicationUtilsTest.java 42 ●●●●● patch | view | raw | blame | history
tools/ant-googlecode-0.0.3.jar patch | view | raw | blame | history
.classpath
@@ -2,6 +2,7 @@
<classpath>
    <classpathentry kind="src" path="src"/>
    <classpathentry kind="src" path="tests"/>
    <classpathentry kind="src" path="resources"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
    <classpathentry kind="lib" path="ext/log4j-1.2.16.jar" sourcepath="ext/log4j-1.2.16-sources.jar">
        <attributes>
@@ -49,17 +50,6 @@
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/googlecharts-1.4.17-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/junit-3.8.2.jar"/>
    <classpathentry kind="lib" path="ext/org.eclipse.jgit-0.12.1.jar" sourcepath="ext/org.eclipse.jgit-0.12.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/org.eclipse.jgit-0.12.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/org.eclipse.jgit.http.server-0.12.1.jar" sourcepath="ext/org.eclipse.jgit.http.server-0.12.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/org.eclipse.jgit.http.server-0.12.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/bcprov-jdk16-1.46.jar" sourcepath="ext/bcprov-jdk16-1.46-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/bcprov-jdk16-1.46-javadoc.jar!/"/>
@@ -70,15 +60,36 @@
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/bcmail-jdk16-1.46-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/jetty-all-7.4.1.v20110513.jar" sourcepath="ext/jetty-all-7.4.1.v20110513-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/jetty-all-7.2.2.v20101205-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/jsch-0.1.44-1.jar" sourcepath="ext/jsch-0.1.44-1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/jsch-0.1.44-1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/rome-0.9.jar" sourcepath="ext/rome-0.9-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/rome-0.9-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/jdom-1.1.jar" sourcepath="ext/jdom-1.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/jdom-1.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/jetty-webapp-7.4.2.v20110526.jar" sourcepath="ext/jetty-webapp-7.4.2.v20110526-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/jetty-webapp-7.4.2.v20110526-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/junit-4.8.2.jar"/>
    <classpathentry kind="lib" path="ext/org.eclipse.jgit-0.12.1.jar" sourcepath="ext/org.eclipse.jgit-0.12.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/org.eclipse.jgit-0.12.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/org.eclipse.jgit.http.server-0.12.1.jar" sourcepath="ext/org.eclipse.jgit.http.server-0.12.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/org.eclipse.jgit.http.server-0.12.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="output" path="bin"/>
</classpath>
NOTICE
@@ -120,6 +120,14 @@
   http://junit.org
---------------------------------------------------------------------------
ant-googlecode
---------------------------------------------------------------------------
   ant-googlecode, released under the
   New BSD License
   http://code.google.com/p/ant-googlecode
---------------------------------------------------------------------------
Fancybox image viewer
---------------------------------------------------------------------------
   Fancybox image viewer, released under the
README.MKD
@@ -1,20 +1,18 @@
Gitblit
=================
Gitblit is an open source, pure Java Git solution for creating, viewing, and serving [Git](http://git-scm.com) repositories.<br/>
Gitblit is an open source, pure Java Git solution for managing, viewing, and serving [Git](http://git-scm.com) repositories.<br/>
More information about Gitblit can be found [here](http://gitblit.com).
License
-------
Gitblit is distributed under the terms of the Apache Software Foundation
license, version 2.0. The text of the license is included in the file LICENSE in the root
of the project.
Gitblit is distributed under the terms of the Apache Software Foundation license, version 2.0. The text of the license is included in the file LICENSE in the root of the project.
Java/Application server requirements
Java Runtime Requirement
------------------------------------
Gitblit requires at least Java 1.6.
Gitblit requires at Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK).
Getting help
------------
build.xml
@@ -1,14 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="gitblit" default="main" basedir=".">
<project name="gitblit" default="compile" basedir=".">
    <!-- Google Code upload task -->
    <taskdef classname="net.bluecow.googlecode.ant.GoogleCodeUploadTask" classpath="${basedir}/tools/ant-googlecode-0.0.3.jar" name="gcupload"/>
    <!-- Project Properties -->
    <property name="project.jar" value="gitblit.jar" />
    <property name="project.mainclass" value="com.gitblit.Launcher" />
    <property name="project.build.dir" value="${basedir}/build" />
    <property name="project.resources.dir" value="${basedir}/resources" />
    <property name="project.deploy.dir" value="${basedir}/deploy" />
    <property name="project.war.dir" value="${basedir}/war" />
    <property name="project.site.dir" value="${basedir}/site" />
    <property name="project.resources.dir" value="${basedir}/resources" />
    <!-- Load publication servers, paths, and credentials -->
    <loadproperties srcfile="${basedir}/build.properties" />
    
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Scrape the version info from code and setup the build properties
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildinfo">
        <!-- build date -->
        <tstamp>
@@ -43,15 +56,18 @@
                    <trim />
                </tokenfilter>
            </filterchain>
        </loadfile>
        <echo>Building Gitblit ${gb.version}</echo>
        </loadfile>
        <property name="distribution.zipfile" value="gitblit-${gb.version}.zip" />
        <property name="distribution.warfile" value="gitblit-${gb.version}.war" />
    </target>
    
    <!-- Build Gitblit GO -->
    <target name="main" description="Compiles Gitblit from source to website" depends="buildinfo">
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Compile
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="compile" depends="buildinfo" description="Retrieves dependencies and compiles Gitblit from source">
        <!-- copy required distribution files to project folder -->
        <copy todir="${basedir}" overwrite="false">
@@ -67,8 +83,7 @@
        <delete dir="${project.build.dir}" />
        <mkdir dir="${project.build.dir}" />
        <javac srcdir="${basedir}/src" destdir="${project.build.dir}">
            <include name="com/gitblit/Build.java" />
            <include name="com/gitblit/BuildWebXml.java" />
            <include name="com/gitblit/Build.java" />
            <include name="com/gitblit/Constants.java" />
            <include name="com/gitblit/utils/StringUtils.java" />            
        </javac>
@@ -87,10 +102,31 @@
        <copy todir="${project.build.dir}">
            <fileset dir="${basedir}/src" excludes="**/*.java,**/thumbs.db" />
        </copy>
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build Gitblit GO
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildGO" depends="compile" description="Build Gitblit GO distribution">
        <echo>Building Gitblit GO ${gb.version}</echo>
        <!-- Delete the deploy folder -->
        <delete dir="${project.deploy.dir}" />
        <!-- Create deployment folder structure -->
        <mkdir dir="${project.deploy.dir}" />
        <copy todir="${project.deploy.dir}">
            <fileset dir="${basedir}/distrib">
                <include name="**/*" />
            </fileset>
        </copy>
        <!-- Build jar -->
        <delete file="${project.jar}" />
        <jar jarfile="${project.jar}">
        <jar jarfile="${project.deploy.dir}/${project.jar}">
            <fileset dir="${project.build.dir}">
                <include name="**/*" />
            </fileset>
@@ -102,10 +138,200 @@
            </manifest>
        </jar>
        <!-- Build the docs for the deploy -->
        <antcall target="buildDocs" inheritall="true" inheritrefs="true">
            <param name="docs.output.dir" value="${project.deploy.dir}/docs" />
        </antcall>
        <!-- Create Zip deployment -->
        <zip destfile="${distribution.zipfile}">
            <fileset dir="${project.deploy.dir}">
                <include name="**/*" />
            </fileset>
        </zip>
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build Gitblit Docs which are bundled with GO and WAR downloads
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildDocs">
    <!-- Build Docs -->
            <mkdir dir="${docs.output.dir}" />
            <copy todir="${docs.output.dir}">
                <!-- Copy selected Gitblit resources -->
                <fileset dir="${project.resources.dir}">
                    <include name="background.png" />
                    <include name="gitblit.css" />
                    <include name="markdown.css" />
                    <include name="gitblt_25.png" />
                    <include name="gitblt-favicon.png" />
                    <include name="lock_go_16x16.png" />
                    <include name="lock_pull_16x16.png" />
                    <include name="shield_16x16.png" />
                    <include name="cold_16x16.png" />
                    <include name="bug_16x16.png" />
                    <include name="book_16x16.png" />
                    <include name="blank.png" />
                </fileset>
                <!-- Copy Doc images -->
                <fileset dir="${basedir}/docs">
                    <include name="*.png" />
                </fileset>
            </copy>
            <!-- Copy google-code-prettify -->
            <mkdir dir="${docs.output.dir}/prettify" />
            <copy todir="${docs.output.dir}/prettify">
                <fileset dir="${basedir}/src/com/gitblit/wicket/pages/prettify">
                    <exclude name="thumbs.db" />
                </fileset>
            </copy>
            <!-- Build deployment doc pages -->
            <java classpath="${project.build.dir}" classname="com.gitblit.BuildSite">
                <classpath refid="master-classpath" />
                <arg value="--sourceFolder" />
                <arg value="${basedir}/docs" />
                <arg value="--outputFolder" />
                <arg value="${docs.output.dir}" />
                <arg value="--pageHeader" />
                <arg value="${basedir}/docs/doc_header.html" />
                <arg value="--pageFooter" />
                <arg value="${basedir}/docs/doc_footer.html" />
                <arg value="--skip" />
                <arg value="screenshots" />
                <arg value="--skip" />
                <arg value="releases" />
                <arg value="--alias" />
                <arg value="index=overview" />
                <arg value="--alias" />
                <arg value="properties=gitblit.properties" />
                <arg value="--substitute" />
                <arg value="%VERSION%=${gb.version}" />
                <arg value="--substitute" />
                <arg value="%GO%=${distribution.zipfile}" />
                <arg value="--substitute" />
                <arg value="%WAR%=${distribution.warfile}" />
                <arg value="--substitute" />
                <arg value="%BUILDDATE%=${gb.buildDate}" />
                <arg value="--substitute" />
                <arg value="%JGIT%=${jgit.version}" />
                <arg value="--load" />
                <arg value="%PROPERTIES%=${basedir}/distrib/gitblit.properties" />
            </java>
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build Gitblit WAR
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildWAR" depends="compile" description="Build Gitblit WAR">
        <echo>Building Gitblit WAR ${gb.version}</echo>
        <delete dir="${project.war.dir}" />
        <!-- Copy web.xml and users.properties to WEB-INF -->
        <copy todir="${project.war.dir}/WEB-INF">
            <fileset dir="${basedir}/distrib">
                 <include name="users.properties" />
            </fileset>
            <fileset dir="${basedir}/src/WEB-INF">
                 <include name="web.xml" />
            </fileset>
        </copy>
        <!-- Build the docs for the WAR build -->
        <antcall target="buildDocs" inheritall="true" inheritrefs="true">
            <param name="docs.output.dir" value="${project.war.dir}/WEB-INF/docs" />
        </antcall>
        <!-- Build the WAR web.xml from the prototype web.xml and gitblit.properties -->
        <java classpath="${project.build.dir}" classname="com.gitblit.BuildWebXml">
            <classpath refid="master-classpath" />
            <arg value="--sourceFile" />
            <arg value="${basedir}/src/WEB-INF/web.xml" />
            <arg value="--destinationFile" />
            <arg value="${project.war.dir}/WEB-INF/web.xml" />
            <arg value="--propertiesFile" />
            <arg value="${basedir}/distrib/gitblit.properties" />
        </java>
        <!-- Gitblit resources -->
        <copy todir="${project.war.dir}">
            <fileset dir="${project.resources.dir}">
                <exclude name="thumbs.db" />
            </fileset>
        </copy>
        <!-- Gitblit library dependencies -->
        <mkdir dir="${project.war.dir}/WEB-INF/lib"/>
        <copy todir="${project.war.dir}/WEB-INF/lib">
            <fileset dir="${basedir}/ext">
                <exclude name="*-sources.jar" />
                <exclude name="*-javadoc.jar" />
                <exclude name="jcommander*.jar" />
                <exclude name="jetty*.jar" />
                <exclude name="junit*.jar" />
                <exclude name="servlet*.jar" />
            </fileset>
        </copy>
        <!-- Gitblit classes -->
        <mkdir dir="${project.war.dir}/WEB-INF/classes"/>
        <copy todir="${project.war.dir}/WEB-INF/classes">
            <fileset dir="${project.build.dir}">
                <exclude name="WEB-INF/web.xml" />
                <exclude name="com/gitblit/tests/" />
                <exclude name="com/gitblit/Build*.class" />
                <exclude name="com/gitblit/GitBlitServer*.class" />
                <exclude name="com/gitblit/Launcher*.class" />
                <exclude name="com/gitblit/MakeCertificate*.class" />
            </fileset>
        </copy>
        <!-- Build the WAR file -->
        <jar basedir="${project.war.dir}" destfile="${distribution.warfile}" compress="true" />
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build the Gitblit Website
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildSite" depends="compile" description="Build the Gitblit website">
        <echo>Building Gitblit Website ${gb.version}</echo>
        <!-- Build Site -->
        <delete dir="${basedir}/site" />
        <mkdir dir="${basedir}/site" />
        <copy todir="${basedir}/site">
        <delete dir="${project.site.dir}" />
        <mkdir dir="${project.site.dir}" />
        <copy todir="${project.site.dir}">
            <!-- Copy selected Gitblit resources -->
            <fileset dir="${project.resources.dir}">
                <include name="background.png" />
@@ -130,8 +356,8 @@
        </copy>
        <!-- Copy Fancybox -->
        <mkdir dir="${basedir}/site/fancybox" />
        <copy todir="${basedir}/site/fancybox">
        <mkdir dir="${project.site.dir}/fancybox" />
        <copy todir="${project.site.dir}/fancybox">
            <fileset dir="${basedir}/docs/fancybox">
                <exclude name="thumbs.db" />
            </fileset>
@@ -139,29 +365,29 @@
        <!-- Copy google-code-prettify -->
        <mkdir dir="${basedir}/src/com/gitblit/wicket/pages/prettify" />
        <copy todir="${basedir}/site/prettify">
        <copy todir="${project.site.dir}/prettify">
            <fileset dir="${basedir}/src/com/gitblit/wicket/pages/prettify">
                <exclude name="thumbs.db" />
            </fileset>
        </copy>
        <!-- Generate thumbnails of screenshots -->
        <java classpath="${project.build.dir}" classname="com.gitblit.Thumbnailer">
        <java classpath="${project.build.dir}" classname="com.gitblit.BuildThumbnails">
            <classpath refid="master-classpath" />
                
            <arg value="--sourceFolder" />
            <arg value="${basedir}/docs/screenshots" />
        
            <arg value="--destinationFolder" />
            <arg value="${basedir}/site/thumbs" />
            <arg value="${project.site.dir}/thumbs" />
            
            <arg value="--maximumDimension" />
            <arg value="250" />
        </java>
        <!-- Copy screenshots -->
        <mkdir dir="${basedir}/site/screenshots" />
        <copy todir="${basedir}/site/screenshots">
        <mkdir dir="${project.site.dir}/screenshots" />
        <copy todir="${project.site.dir}/screenshots">
            <fileset dir="${basedir}/docs/screenshots">
                <include name="*.png" />
            </fileset>
@@ -174,7 +400,7 @@
            <arg value="${basedir}/docs" />
            <arg value="--outputFolder" />
            <arg value="${basedir}/site" />
            <arg value="${project.site.dir}" />
            <arg value="--pageHeader" />
            <arg value="${basedir}/docs/site_header.html" />
@@ -192,7 +418,10 @@
            <arg value="%VERSION%=${gb.version}" />
            <arg value="--substitute" />
            <arg value="%DISTRIBUTION%=${distribution.zipfile}" />
            <arg value="%GO%=${distribution.zipfile}" />
            <arg value="--substitute" />
            <arg value="%WAR%=${distribution.warfile}" />
            <arg value="--substitute" />
            <arg value="%BUILDDATE%=${gb.buildDate}" />
@@ -204,189 +433,83 @@
            <arg value="%PROPERTIES%=${basedir}/distrib/gitblit.properties" />
        </java>
        <!-- Delete the deploy folder -->
        <delete dir="${basedir}/deploy" />
        <!-- Create deployment folder structure -->
        <mkdir dir="${basedir}/deploy" />
        <copy todir="${basedir}/deploy" file="${project.jar}" />
        <copy todir="${basedir}/deploy">
            <fileset dir="${basedir}/distrib">
                <include name="**/*" />
            </fileset>
        </copy>
        <!-- Build Deployment Docs -->
        <mkdir dir="${basedir}/deploy/docs" />
        <copy todir="${basedir}/deploy/docs">
            <!-- Copy selected Gitblit resources -->
            <fileset dir="${project.resources.dir}">
                <include name="background.png" />
                <include name="gitblit.css" />
                <include name="markdown.css" />
                <include name="gitblt_25.png" />
                <include name="gitblt-favicon.png" />
                <include name="lock_go_16x16.png" />
                <include name="lock_pull_16x16.png" />
                <include name="shield_16x16.png" />
                <include name="cold_16x16.png" />
                <include name="bug_16x16.png" />
                <include name="book_16x16.png" />
                <include name="blank.png" />
            </fileset>
            <!-- Copy Doc images -->
            <fileset dir="${basedir}/docs">
                <include name="*.png" />
            </fileset>
        </copy>
        <!-- Copy google-code-prettify -->
        <mkdir dir="${basedir}/src/com/gitblit/wicket/pages/prettify" />
        <copy todir="${basedir}/deploy/docs/prettify">
            <fileset dir="${basedir}/src/com/gitblit/wicket/pages/prettify">
                <exclude name="thumbs.db" />
            </fileset>
        </copy>
        <!-- Build deployment doc pages -->
        <java classpath="${project.build.dir}" classname="com.gitblit.BuildSite">
            <classpath refid="master-classpath" />
            <arg value="--sourceFolder" />
            <arg value="${basedir}/docs" />
            <arg value="--outputFolder" />
            <arg value="${basedir}/deploy/docs" />
            <arg value="--pageHeader" />
            <arg value="${basedir}/docs/page_header.html" />
            <arg value="--pageFooter" />
            <arg value="${basedir}/docs/page_footer.html" />
            <arg value="--skip" />
            <arg value="screenshots" />
            <arg value="--skip" />
            <arg value="releases" />
            <arg value="--alias" />
            <arg value="index=overview" />
            <arg value="--alias" />
            <arg value="properties=gitblit.properties" />
            <arg value="--substitute" />
            <arg value="%VERSION%=${gb.version}" />
            <arg value="--substitute" />
            <arg value="%DISTRIBUTION%=${distribution.zipfile}" />
            <arg value="--substitute" />
            <arg value="%BUILDDATE%=${gb.buildDate}" />
            <arg value="--substitute" />
            <arg value="%JGIT%=${jgit.version}" />
            <arg value="--load" />
            <arg value="%PROPERTIES%=${basedir}/distrib/gitblit.properties" />
        </java>
    </target>
        
        <!-- Create Zip deployment -->
        <zip destfile="${distribution.zipfile}">
            <fileset dir="${basedir}/deploy">
                <include name="**/*" />
            </fileset>
        </zip>
        <!-- Delete the deploy folder -->
        <delete dir="${basedir}/deploy" />
        <!-- Cleanup builds -->
        <delete>
            <fileset dir="${basedir}">
                <include name="${project.jar}" />
            </fileset>
        </delete>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Compile from source, publish binaries, and build & deploy site
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildAll" depends="buildGO,buildWAR,buildSite">
        <!-- Cleanup -->
        <delete dir="${project.build.dir}" />
        <delete dir="${project.war.dir}" />
        <delete dir="${project.deploy.dir}" />
    </target>
    
    <!-- Build Gitblit WAR -->
    <target name="buildWAR" description="Build the Gitblit WAR" depends="buildinfo">
        <path id="master-classpath">
            <fileset dir="${basedir}/ext">
                <include name="*.jar" />
            </fileset>
        </path>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Publish binaries to Google Code
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="publishBinaries" depends="buildGO,buildWAR" description="Publish the Gitblit binaries to Google Code">
        <echo>Uploading Gitblit ${gb.version} binaries</echo>
        <!-- Upload ZIP file -->
        <gcupload
             username="${googlecode.user}"
             password="${googlecode.password}"
             projectname="gitblit"
             filename="${distribution.zipfile}"
             targetfilename="gitblit-${gb.version}.zip"
             summary="Standalone, integrated Gitblit server v${gb.version}"
             labels="Featured, Type-Package, OpSys-All" />
            
        <delete dir="${basedir}/war" />
        <mkdir dir="${basedir}/war/WEB-INF/lib"/>
        <!-- Gitblit web.xml -->
        <java classpath="${project.build.dir}" classname="com.gitblit.BuildWebXml">
            <classpath refid="master-classpath" />
        </java>
        <!-- Gitblit resources -->
        <copy todir="${basedir}/war">
            <fileset dir="${project.resources.dir}">
                <exclude name="thumbs.db" />
            </fileset>
        </copy>
        <!-- Gitblit library dependencies -->
        <copy todir="${basedir}/war/WEB-INF/lib">
            <fileset dir="${basedir}/ext">
                <exclude name="*-sources.jar" />
                <exclude name="*-javadoc.jar" />
                <exclude name="jcommander*.jar" />
                <exclude name="jetty*.jar" />
                <exclude name="junit*.jar" />
                <exclude name="servlet*.jar" />
            </fileset>
        </copy>
        <!-- Gitblit classes -->
        <mkdir dir="${basedir}/war/WEB-INF/classes"/>
        <copy todir="${basedir}/war/WEB-INF/classes">
            <fileset dir="${basedir}/bin">
                <exclude name="WEB-INF/web.xml" />
                <exclude name="com/gitblit/tests/" />
                <exclude name="com/gitblit/Build*.class" />
                <exclude name="com/gitblit/GitBlitServer*.class" />
                <exclude name="com/gitblit/Launcher*.class" />
                <exclude name="com/gitblit/MakeCertificate*.class" />
                <exclude name="com/gitblit/Thumbnailer*.class" />
            </fileset>
        </copy>
        <!-- Build the WAR file -->
        <jar basedir="${basedir}/war" destfile="${distribution.warfile}" compress="true" />
        <!-- Upload WAR file -->
        <gcupload
             username="${googlecode.user}"
             password="${googlecode.password}"
             projectname="gitblit"
             filename="${distribution.warfile}"
             targetfilename="gitblit-${gb.version}.war"
             summary="Gitblit WAR v${gb.version} for your servlet container"
             labels="Featured, Type-Package, OpSys-All" />
    </target>
    
    <!-- Publish binaries to github -->
    <target name="publishBinaries" description="Publish the Gitblit distribution to Github">
        <!-- TODO -->
        <!-- https://github.com/oyvindkinsey/GitHubUploadTask -->
    </target>
    <!-- Publish site to hosting service -->
    <!-- You must add ext/commons-net-1.4.0.jar to your    ANT classpath. -->
    <target name="publishSite" description="Publish the Gitblit site to a webserver (requires ext/commons-net-1.4.0.jar)">
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Publish site to hosting service
        You must add ext/commons-net-1.4.0.jar to your ANT classpath.
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="publishSite" depends="buildSite" description="Publish the Gitblit site to a webserver (requires ext/commons-net-1.4.0.jar)" >
        <echo>Uploading Gitblit ${gb.version} website</echo>
        <ftp server="${ftp.server}"
            userid="${ftp.user}"
            password="${ftp.password}"
            remotedir="${ftp.dir}"
            passive="true"
            verbose="yes">
        <fileset dir="${basedir}/site" />
        <fileset dir="${project.site.dir}" />
        </ftp>
    </target>
    
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Compile from source, publish binaries, and build & deploy site
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="publishAll" depends="publishBinaries,publishSite">
        <!-- Cleanup -->
        <delete dir="${project.build.dir}" />
        <delete dir="${project.war.dir}" />
        <delete dir="${project.deploy.dir}" />
    </target>
</project>
distrib/gitblit.properties
@@ -2,9 +2,6 @@
# Git Servlet Settings
#
# Allow push/pull over http/https with JGit servlet.
git.enableGitServlet = true
# Base folder for repositories
# Changing this value requires a server restart.
# Use forward slashes even on Windows!!
@@ -18,6 +15,9 @@
#      c:/gitrepos/libraries/myotherlibrary.git
git.searchRepositoriesSubfolders = true
# Allow push/pull over http/https with JGit servlet.
git.enableGitServlet = true
#
# Authentication Settings
#
@@ -30,11 +30,15 @@
# Changing this value requires a server restart.
web.authenticateAdminPages = true
# Either a simple user realm file to authenticate users
# OR a fully qualified class name that implements the ILoginService interface.
# Allow Gitblit to store a cookie in the user's browser for automatic
# authentication.  The cookie is generated by the user service.
web.allowCookieAuthentication = true
# Either the path to a simple user properties file
# OR a fully qualified class name that implements the IUserService interface.
# Any custom implementation must have a public default constructor.
# Changing this value requires a server restart.
realm.realmFile = users.properties
realm.userService = users.properties
# How to store passwords.
# Valid values are plain or md5.  Default is md5. 
@@ -60,7 +64,7 @@
# Allow dyanamic zip downloads.   
web.allowZipDownloads = true
# Default number of entries to include in RSS/Atom Syndication links
# Default number of entries to include in RSS Syndication links
web.syndicationEntries = 25
# This is the message display above the repositories table.
@@ -69,7 +73,9 @@
web.repositoriesMessage = gitblit
# Use the client timezone when formatting dates.
# This uses AJAX to determine the browser's timezone.
# This uses AJAX to determine the browser's timezone and may require more
# server overhead because a Wicket session is created.  All Gitblit pages
# attempt to be stateless, if possible.
# Changing this value requires a server restart.
web.useClientTimezone = false
@@ -118,8 +124,10 @@
# Value must exceed 0 else default of 20 is used
web.summaryCommitCount = 16
# The number of tags/heads to display on the summary page
# Value must exceed 0 else default of 5 is used
# The number of tags/branches to display on the summary page.
# -1 = all tags/branches
# 0 = hide tags/branches
# N = N tags/branches
web.summaryRefsCount = 5
# The number of items to show on a page before showing the first, prev, next
@@ -144,7 +152,7 @@
# Aggressive heap management will run the garbage collector on every generated
# page.  This slows down page generation a little but improves heap consumption. 
web.aggressiveHeapManagement = true
web.aggressiveHeapManagement = false
# Run the webapp in debug mode
# Changing this value requires a server restart.
@@ -171,7 +179,7 @@
server.tempFolder = temp
#
# Jetty Settings
# Jetty Http/Https Server Settings
#
# Use Jetty NIO connectors.  If false, Jetty Socket connectors will be used.
distrib/installService.cmd
@@ -1,2 +1,2 @@
set JVM=C:\Program Files\Java\jdk1.6.0_26
JavaService.exe -install gitblit "%JVM%\jre\bin\server\jvm.dll" -Xmx1024M -Djava.class.path=%CD%\gitblit.jar;"%JVM%\lib\tools.jar" -start com.gitblit.Launcher -params --storePassword dosomegit -stop com.gitblit.Launcher -params --stop -out %CD%\logs\stdout.log -err %CD%\logs\stderr.log -current %CD%
JavaService.exe -install gitblit "%JVM%\jre\bin\server\jvm.dll" -Xmx1024M -Djava.class.path=%CD%\gitblit.jar;"%JVM%\lib\tools.jar" -start com.gitblit.Launcher -params --storePassword gitblit -stop com.gitblit.Launcher -params --stop -out %CD%\logs\stdout.log -err %CD%\logs\stderr.log -current %CD%
distrib/installService64.cmd
@@ -1,2 +1,2 @@
set JVM=C:\Program Files\Java\jdk1.6.0_26
JavaService64.exe -install gitblit "%JVM%\jre\bin\server\jvm.dll" -Djava.class.path=%CD%\gitblit.jar;"%JVM%\lib\tools.jar" -start com.gitblit.Launcher -params --storePassword dosomegit -stop com.gitblit.Launcher -params --stop -out %CD%\logs\stdout.log -err %CD%\logs\stderr.log -current %CD%
JavaService64.exe -install gitblit "%JVM%\jre\bin\server\jvm.dll" -Djava.class.path=%CD%\gitblit.jar;"%JVM%\lib\tools.jar" -start com.gitblit.Launcher -params --storePassword gitblit -stop com.gitblit.Launcher -params --stop -out %CD%\logs\stdout.log -err %CD%\logs\stderr.log -current %CD%
docs/00_index.mkd
@@ -1,148 +1,36 @@
## Overview
Gitblit is an open-source, integrated pure Java stack for managing, viewing, and serving [Git][git] repositories.
Its designed primarily as a tool for small workgroups who want to host [Git][git] repositories on a Windows machine.  Having said that, it works equally well on any standard Linux distribution.
<a href="screenshots.html" title="Screenshots"><img class="overview" src="thumbs/00.png" alt="Screenshots" /></a>
Gitblit is an open-source, pure Java stack for managing, viewing, and serving [Git][git] repositories.<br/>
Its designed primarily as a tool for small workgroups who want to host centralized repositories.
Gitblit is available in two variations:
<ul class='noBullets'>
<li>*Gitblit GO* - a complete & integrated pure Java stack<p>
    This is what you should download if you want to go from zero to Git in less than 5 mins.<br/>
    Gitblit GO is like a mashup of Apache httpd, [Git][git], and Gitweb with simplified configuration and maintenance.<br/>
    All dependencies are downloaded on first execution.<p>
<li>*Gitblit WAR* - a traditional WAR distribution<p>
    This is what you should download if you want to deploy Gitblit into your own servlet container (e.g. Tomcat, Jetty, etc).<br/>
    All dependencies are bundled.
</ul>
### Java Runtime Requirement
Gitblit requires a Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK).
 
### Current Release
%VERSION% ([go](http://gitblit.com/%GO%)|[war](http://gitblit.com/%WAR%)) based on [%JGIT%][jgit] &nbsp; (*%BUILDDATE%*)
%VERSION% ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] with Blame backport &nbsp; (*%BUILDDATE%*)
issues & binaries @ [Google Code][googlecode]<br/>
sources @ [Github][gitbltsrc]
### Design Principles
1. [Keep It Simple, Stupid](http://en.wikipedia.org/wiki/KISS_principle)
2. Offer useful features for serving Git repositories.  If feature is complex, refer to #1.
3. All dependencies must be retrievable from a publicly accessible [Maven](http://maven.apache.org) repository.<br/>This is to ensure authenticity of dependencies and to keep the Gitblit distribution svelte.
### Gitblit Features
- JGit SmartHTTP servlet
- Browser and git client authentication
- Four repository access control configurations and a Read-Only flag
    <ul class='noBullets'>
    <li>![anonymous](blank.png) *Anonymous View, Clone & Push*</li>
    <li>![push](lock_go_16x16.png) *Authenticated Push*</li>
    <li>![clone](lock_pull_16x16.png) *Authenticated Clone & Push*</li>
    <li>![view](shield_16x16.png) *Authenticated View, Clone & Push*</li>
    <li>![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only)
    </ul>
- Gitweb inspired UI
- Administrators may create, edit, rename, or delete repositories through the web UI
- Administrators may create, edit, rename, or delete users through the web UI
- Repository Owners may edit repositories through the web UI
- Git-notes support
- Branch metrics (uses Google Charts)
- HEAD and branch RSS feeds
- Blame annotations view
- Dates can optionally be displayed using the browser's reported timezone
- Display of Author and Committer email addresses can be disabled
- Case-insensitive searching of commit messages, authors, or committers
- Dynamic zip downloads feature
- 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 users configuration
- Optional utility pages
    <ul class='noBullets'>
    <li>![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository</li>
    <li>![tickets](bug_16x16.png) Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)*</li>
    </ul>
### Gitblit-Go Features
- Out-of-the-box integrated stack requiring minimal configuration
- Automatically generates a self-signed certificate for https communications
- Single text file for server configuration
### Limitations
- 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
### Caveats
- Gitblit may eat your data.  Use at your own risk.
- Gitblit may have security holes.  Patches welcome.  :)
### Todo List
- Code documentation
- Unit testing
- Update Build.java to JGit 1.0.0, when its released
### Idea List
- Consider clone remote repository feature
- Stronger Ticgit read-only integration
    - activity/timeline
    - query feature with paging support
    - change history
- Ticgit write integration
- Blob page improvements
    - view images
    - view other binary files (pdf, doc, etc)
- Markdown editing feature
### License
Gitblit is distributed under the terms of the [Apache Software Foundation license, version 2.0][apachelicense]
### Inspirations
- [Gitweb](http://www.git-scm.com)
- [Fossil](http://www.fossil-scm.org)
## Architecture
![block diagram](architecture.png "Gitblit Architecture")
### Bundled Dependencies
The following dependencies are bundled with Gitblit.
- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0)
- [JavaService](http://forge.ow2.org/projects/javaservice) (BSD and LGPL)
- magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY)
- modified Git logo originally designed by [Henrik Nyh](http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon)
- other icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons) (Creative Commons CC-BY)
### Downloaded Dependencies
The following dependencies are automatically downloaded by Gitblit-Go (or already bundled with the WAR) from the Apache Maven repository and from the Eclipse Maven repository when Gitblit is launched for the first time.
- [JGit][jgit] (EDL 1.0)
- [Wicket](http://wicket.apache.org) (Apache 2.0)
- [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts) (Apache 2.0)
- [MarkdownPapers](http://markdown.tautua.org) (Apache 2.0)
- [Jetty](http://eclipse.org/jetty) (Apache 2.0, EPL 1.0)
- [SLF4J](http://www.slf4j.org) (MIT/X11)
- [Log4j](http://logging.apache.org/log4j) (Apache 2.0)
- [JCommander](http://jcommander.org) (Apache 2.0)
- [BouncyCastle](http://www.bouncycastle.org) (MIT/X11)
- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD)
- [Rome](http://rome.dev.java.net) (Apache 1.1)
- [jdom](http://www.jdom.org) (Apache-style JDOM license)
### 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.
Additionally, [Google CodePro AnalytiX](http://code.google.com/javadevtools), [eclipse-cs](http://eclipse-cs.sourceforge.net), [FindBugs](http://findbugs.sourceforge.net), and [EclEmma](http://www.eclemma.org) are recommended development tools.
1. Clone the git repository from [Github][gitbltsrc].
2. Import the gitblit project into your Eclipse workspace.<br/>
*There will be lots of build errors.*
3. Using Ant, execute the `build.xml` script in the project root.<br/>
*This will download all necessary build dependencies and will also generate the Keys class for accessing settings.*
4. Select your gitblit project root and **Refresh** the project, this should correct all build problems.
5. Using JUnit, execute the `com.gitblit.tests.GitBlitSuite` test suite.<br/>
*This will clone some repositories from the web and run through the unit tests.*
5. Review the settings in `gitblit.properties` in your project root.
    - By default, the *git.repositoriesFolder* points to the repositories cloned by the test suite.<br/>
    - If running on Linux you may have to change the served port(s) to > 1024 unless you are developing as the root user.
6. Execute the *com.gitblit.Launcher* class to start Gitblit.
## Contributing
Patches welcome in any form.
Contributions must be your own original work and must licensed under the [Apache License, Version 2.0][apachelicense], the same license used by Gitblit.
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
[git]: http://git-scm.com "Official Git Site"
[gitbltsrc]: http://somewhere.com "gitblit git repository"
[gitbltsrc]: http://github.com/gitblit "gitblit git repository"
[googlecode]: http://code.google.com/p/gitblit "gitblit project management"
[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0"
docs/01_features.mkd
New file
@@ -0,0 +1,64 @@
## Gitblit Features
- JGit SmartHTTP servlet
- Browser and git client authentication
- Four repository access control configurations with a Read-Only control flag
    <ul class='noBullets'>
    <li>![anonymous](blank.png) *Anonymous View, Clone & Push*</li>
    <li>![push](lock_go_16x16.png) *Authenticated Push*</li>
    <li>![clone](lock_pull_16x16.png) *Authenticated Clone & Push*</li>
    <li>![view](shield_16x16.png) *Authenticated View, Clone & Push*</li>
    <li>![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only)
    </ul>
- Gitweb inspired web UI
- Administrators may create, edit, rename, or delete repositories through the web UI
- Administrators may create, edit, rename, or delete users through the web UI
- Repository Owners may edit repositories through the web UI
- Git-notes support
- Branch metrics (uses Google Charts)
- HEAD and Branch RSS feeds
- Blame annotations view
- Dates can optionally be displayed using the browser's reported timezone
- Display of Author and Committer email addresses can be disabled
- Case-insensitive searching of commit messages, authors, or committers
- Dynamic zip downloads feature
- 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 users configuration
- Optional utility pages
    <ul class='noBullets'>
    <li>![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository</li>
    <li>![tickets](bug_16x16.png) Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)*</li>
    </ul>
## Gitblit GO Features
- Out-of-the-box integrated stack requiring minimal configuration
- Automatically generates a self-signed certificate for https communications
- Single text file for configuring server and gitblit
## Limitations
- 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
- Only Administrators can create, modify or delete users
### Caveats
- Gitblit may eat your data.  Use at your own risk.
- Gitblit may have security holes.  Patches welcome.  :)
## Todo List
- Code documentation
- Unit testing
- Update to JGit 1.0.0 when JGit team provides Maven artifacts
### Under Consideration
- Clone remote repository feature
- Blob page improvements
    - view images
    - view other binary files (pdf, doc, etc)
- Markdown editing feature
- Stronger Ticgit read-only integration
    - activity/timeline
    - query feature with paging support
    - change history
- Ticgit write integration
docs/01_releases.mkd
File was deleted
docs/01_setup.mkd
File was renamed from docs/00_setup.mkd
@@ -1,11 +1,25 @@
## Gitblit-Go Setup and Configuration
## Gitblit WAR Setup
1. Download and unzip [Gitblit-Go %VERSION%](http://gitblit.com/%GO%).<br/>
*Its best to eliminate spaces in the path name as that can cause troubleshooting headaches.*
1. Download [Gitblit WAR %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) to the webapps folder of your servlet container.<br/>
2. You may have to manually extract the WAR (zip file) to a folder within your webapps folder.  Manual extraction depends on if your servlet container is configured to automatically deploy WAR files.
3. Copy the `WEB-INF/users.properties` file to a location outside the webapps folder but accessible by your servlet container.
4. The Gitblit webapp is configured through its `web.xml` file.<br/>
Open `web.xml` in your favorite text editor and make sure to review and set:
    - &lt;context-parameter&gt; *git.repositoryFolder* (set the full path to your repositories folder)
    - &lt;context-parameter&gt; *realm.userService* (set the full path to `users.properties`)
5. You may have to restart your servlet container.
6. Open your browser to <http://localhost/gitblit> or whatever the url should be.
7. Click the *Login* link and enter the default administrator credentials: **admin / admin**<br/>
**NOTE:** Make sure to change the administrator username and/or password!!
## Gitblit GO Setup
1. Download and unzip [Gitblit GO %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%GO%).<br/>
*Its best to eliminate spaces in the path name.*
2. The server itself is configured through a simple text file.<br/>
Open `gitblit.properties` in your favorite text editor and make sure to review and set:
    - *git.repositoryFolder*
    - *server.tempFolder*
    - *git.repositoryFolder* (path my be relative or absolute)
    - *server.tempFolder* (path my be relative or absolute)
    - *server.httpBindInterface* and *server.httpsBindInterface*<br/>
**NOTE:** Consider using **https** exclusively because passwords for authentication are transmitted as clear text!     
    - *server.storePassword*<br/>
@@ -15,6 +29,36 @@
5. Open your browser to <http://localhost> or <https://localhost> depending on your chosen configuration.
6. Click the *Login* link and enter the default administrator credentials: **admin / admin**<br/>
**NOTE:** Make sure to change the administrator username and/or password!! 
### Creating your own Self-Signed Certificate
Gitblit GO automatically generates an ssl certificate for you that contains generic, non-personalized information.
Should you want to include more personal or server-specific information in your self-signed certificate you will have to generate a new one.
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd` script and execute it.<br/>
**NOTE:** If you manually generate an ssl certificate, the certificate password AND the keystore password must match!
### Running as a Windows Service
Review the contents of the `installService.cmd` or `installService64.cmd`, as appropriate for your installed Java Virtual Machine.<br/>
Set the *JVM* variable in the script to the location of your Java Virtual Machine, add any necessary start parameters, and execute the script.
#### Command-Line Parameters
Command-Line parameters override the values in `gitblit.properties` at runtime.
    --repositoriesFolder   Git Repositories Folder
    --userService          Authentication and Authorization Service (filename or fully qualified classname)
    --useNio               Use NIO Connector else use Socket Connector.
    --httpPort             HTTP port for to serve. (port <= 0 will disable this connector)
    --httpsPort            HTTPS port to serve.  (port <= 0 will disable this connector)
    --storePassword        Password for SSL (https) keystore.
    --shutdownPort         Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)
    --tempFolder           Folder for server to extract built-in webapp
**Example**
    java -jar gitblit.jar --userService c:\myrealm.properties --storePassword something
## Gitblit Configuration
### Administering Repositories
Repositories can be created, edited, renamed, and deleted through the web UI.  They may also be created, edited, and deleted from the command-line using real [Git](http://git-scm.com) or your favorite file manager and text editor.
@@ -37,7 +81,7 @@
Repositories can be grouped within subfolders.  e.g. *libraries/mycoollib.git* and *libraries/myotherlib.git*
All created repositories are *bare* and will automatically have *.git* appended to the name at creation time, if not already specified.
All repositories created with Gitblit are *bare* and will automatically have *.git* appended to the name at creation time, if not already specified.
#### Repository Owner
The *Repository Owner* has the special permission of being able to edit a repository through the web UI.  The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user.
@@ -58,29 +102,13 @@
#### User Roles
There is only one actual *role* in Gitblit and that is *#admin* which grants administrative powers to that user.  Administrators automatically have access to all repositories.  All other *roles* are repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
### Creating your own Self-Signed Certificate
## Authentication and Authorization Customization
Instead of maintaining a `users.properties` file, you may want to integrate Gitblit into an existing environment.
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd` script and execute it.<br/>
**NOTE:** If you manually generate an ssl certificate, the certificate password AND the keystore password must match!
You may use your own custom *com.gitblit.IUserService* implementation by specifying its fully qualified classname in the *realm.userService* setting.<br/>
### Running as a Service
Review the contents of the `installService.cmd` or `installService64.cmd`, as appropriate for your installed Java Virtual Machine.<br/>
Set the *JVM* variable in the script to the location of your Java Virtual Machine, add any necessary start parameters, and execute the script.
Your user service class must be on Gitblit's classpath and must have a public default constructor.
#### Command-Line Parameters
    --tempFolder           Server temp folder
    --repositoriesFolder   Git Repositories Folder
    --realmFile            Users Realm Hash File
    --useNio               Use NIO Connector else use Socket Connector.
    --httpPort             HTTP port for to serve. (port <= 0 will disable this connector)
    --httpsPort            HTTPS port to serve.  (port <= 0 will disable this connector)
    --storePassword        Password for SSL (https) keystore.
    --shutdownPort         Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)
**Example**
    java -jar gitblit.jar --realmFile c:\myrealm.txt --storePassword something
## Client Setup and Configuration
### Https with Self-Signed Certificates
You must tell Git not to verify the self-signed certificate in order to perform any remote Git operations.
docs/02_faq.mkd
File was renamed from docs/01_faq.mkd
@@ -12,12 +12,23 @@
3. The repository is clone-restricted and your password changed.
4. A regression in Gitblit.  :(
### Why can't I access Gitblit-Go from another machine?
Please check *server.httpBindInterface* and *server.httpsBindInterface* in `gitblit.properties`.
### Why can't I access Gitblit GO from another machine?
Please check *server.httpBindInterface* and *server.httpsBindInterface* in `gitblit.properties`, you may be binding only to localhost.
### How do I run Gitblit-Go on port 80 or 443 in Linux?
And of course ensure that any firewall you may have running either has an exception for your ports or for the running process.
### How do I run Gitblit GO on port 80 or 443 in Linux?
Linux requires root permissions to serve on ports < 1024.<br/>
Run the server as *root* (security concern) or change the ports you are serving to 8080 (http) and/or 8443 (https). 
### Gitblit GO does not list my repositories?!
Confirm that the value *git.repositoriesFolder* in `gitblit.properties` actually points to your repositories folder.
### Gitblit WAR does not list my repositories?!
Confirm that the &lt;context-param&gt; *git.repositoriesFolder* value in your `web.xml` file actually points to your repositories folder.
### Gitblit WAR will not authenticate any users?!
Confirm that the &lt;context-param&gt; *realm.userService* value in your `web.xml` file actually points to a `users.properties` file.
## General Interest Questions
@@ -32,7 +43,7 @@
Gitblit is not meant to be a social coding resource like [Github](http://github.com) or [Bitbucket](http://bitbucket.com) with 100s or 1000s of users.  Gitblit is designed to fulfill the same function as your centralized Subversion or CVS server.
### Why does Gitblit exist?
### Why does Gitblit exist when there is Git and Gitweb?
As a Java developer I prefer that as much of my tooling as possible is Java.<br/>
Originally, I was going to use [Mercurial](http://mercurial.selenic.com) but...
@@ -43,9 +54,11 @@
Gitblit eliminates all that complication with its 100% Java stack and simple single configuration file.
Additionally, Git and Gitweb do not offer repository creation or user management.
### Do I need real Git?
No.  Gitblit is based on [JGit][jgit] which is a pure Java implementation of the [Git version control system][git].<br/>
Everything you need for Gitblit is either in the zip distribution file or automatically downloaded on execution.
Everything you need for Gitblit (except Java) is either bundled in the distribution file or automatically downloaded on execution.
### Can I run Gitblit in conjunction with my existing Git tooling?
Yes.
@@ -59,7 +72,7 @@
### Can I manually edit users.properties, gitblit.properties, or .git/config?
Yes.  You can manually manipulate all of them and (most) changes will be immediately available to Gitblit.<br/>Exceptions to this are noted in `gitblit.properties`.
*NOTE:* Care must be taken to preserve the relationship between user roles and repository names.<br/>Please see the [setup](/setup.html) page for details.
*NOTE:* Care must be taken to preserve the relationship between user roles and repository names.<br/>Please see the *User Roles* section of the [setup](/setup.html) page for details.
### Can I restrict access to paths within a repository?
No.  Access restrictions apply to the repository as a whole.
@@ -67,7 +80,7 @@
Gitblit's simple authentication and authorization mechanism can be used to facilitate one or more of the [workflows outlined here](http://progit.org/book/ch5-1.html).  Should you require more fine-grained access controls you might consider using [gitolite](https://github.com/sitaramc/gitolite).
### Can I authenticate users against XYZ?
Yes.  The login service is pluggable.  You may write your own authentication module by implementing the *ILoginService* interface.  Set the fully qualified classname as the *realm.realmFile* property.
Yes.  The user service is pluggable.  You may write your own user service by implementing the *com.gitblit.IUserService* interface.  Set the fully qualified classname as the *realm.userService* property.
### Why doesn't Gitblit support SSH?
Gitblit could integrate [Apache Mina][mina] to provide SSH access.  However, doing so violates Gitblit's first design principle: [KISS](http://en.wikipedia.org/wiki/KISS_principle).<br/>
docs/03_properties.mkd
docs/04_design.mkd
New file
@@ -0,0 +1,69 @@
## Design Principles
1. [Keep It Simple, Stupid](http://en.wikipedia.org/wiki/KISS_principle)
2. Offer useful features for serving Git repositories.  If feature is complex, refer to #1.
3. All dependencies must be retrievable from a publicly accessible [Maven](http://maven.apache.org) repository.<br/>This is to ensure authenticity of dependencies, to keep the Gitblit GO distribution svelte, and to automate the setup of developer environments.
## Architecture
![block diagram](architecture.png "Gitblit Architecture")
### Bundled Dependencies
The following dependencies are bundled with Gitblit.
- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0)
- [JavaService](http://forge.ow2.org/projects/javaservice) (BSD and LGPL)
- magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY)
- modified Git logo originally designed by [Henrik Nyh](http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon)
- other icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons) (Creative Commons CC-BY)
### Downloaded Dependencies
The following dependencies are automatically downloaded by Gitblit GO (or already bundled with the WAR) from the Apache Maven repository and from the Eclipse Maven repository when Gitblit is launched for the first time.
- [JGit][jgit] (EDL 1.0)
- [Wicket](http://wicket.apache.org) (Apache 2.0)
- [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts) (Apache 2.0)
- [MarkdownPapers](http://markdown.tautua.org) (Apache 2.0)
- [Jetty](http://eclipse.org/jetty) (Apache 2.0, EPL 1.0)
- [SLF4J](http://www.slf4j.org) (MIT/X11)
- [Log4j](http://logging.apache.org/log4j) (Apache 2.0)
- [JCommander](http://jcommander.org) (Apache 2.0)
- [BouncyCastle](http://www.bouncycastle.org) (MIT/X11)
- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD)
- [Rome](http://rome.dev.java.net) (Apache 1.1)
- [jdom](http://www.jdom.org) (Apache-style JDOM license)
### 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)
- [ant-googlecode](http://code.google.com/p/ant-googlecode) (New BSD)
## Building from Source
[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured.
Additionally, [Google CodePro AnalytiX](http://code.google.com/javadevtools), [eclipse-cs](http://eclipse-cs.sourceforge.net), [FindBugs](http://findbugs.sourceforge.net), and [EclEmma](http://www.eclemma.org) are recommended development tools.
1. Clone the git repository from [Github][gitbltsrc].
2. Import the gitblit project into your Eclipse workspace.<br/>
*There will be lots of build errors.*
3. Using Ant, execute the `build.xml` script in the project root.<br/>
*This will download all necessary build dependencies and will also generate the Keys class for accessing settings.*
4. Select your gitblit project root and **Refresh** the project, this should correct all build problems.
5. Using JUnit, execute the `com.gitblit.tests.GitBlitSuite` test suite.<br/>
*This will clone some repositories from the web and run through the unit tests.*
5. Review the settings in `gitblit.properties` in your project root.
    - By default, the *git.repositoriesFolder* points to the repositories cloned by the test suite.<br/>
    - If running on Linux you may have to change the served port(s) to > 1024 unless you are developing as the root user.
6. Execute the *com.gitblit.Launcher* class to start Gitblit.
## Contributing
Patches welcome in any form.
Contributions must be your own original work and must licensed under the [Apache License, Version 2.0][apachelicense], the same license used by Gitblit.
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
[git]: http://git-scm.com "Official Git Site"
[gitbltsrc]: http://github.com/gitblit "gitblit git repository"
[googlecode]: http://code.google.com/p/gitblit "gitblit project management"
[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0"
docs/04_releases.mkd
New file
@@ -0,0 +1,9 @@
## Release History
### Current Release
%VERSION% ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] with Blame backport &nbsp; (*%BUILDDATE%*)
### Older Releases
none
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
resources/commit_changes_16x16.png
resources/gitblit.css
@@ -56,6 +56,11 @@
    padding-right: 1px;
}
img.overview {
    float:right;
    border:1px solid #CCCCCC;
}
a {
    color: #0000cc;
}
src/com/gitblit/Build.java
@@ -86,10 +86,10 @@
        downloadFromApache(MavenObject.JSCH, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.ROME, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.JDOM, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
        // needed for site publishing
        downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME);
    }
@@ -401,18 +401,17 @@
                "e528f593b19b04d500992606f58b87fcfded8883",
                "d0ffadd0a4ab909d94a577b5aad43c13b617ddcb");
        public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net", "commons-net",
                "1.4.0", 181000, 0, 0, "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7",
                "",
                "");
        public static final MavenObject ROME = new MavenObject("rome", "rome", "rome",
                "0.9", 208000, 196000, 407000, "dee2705dd01e79a5a96a17225f5a1ae30470bb18",
        public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net",
                "commons-net", "1.4.0", 181000, 0, 0, "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7",
                "", "");
        public static final MavenObject ROME = new MavenObject("rome", "rome", "rome", "0.9",
                208000, 196000, 407000, "dee2705dd01e79a5a96a17225f5a1ae30470bb18",
                "226f851dc44fd94fe70b9c471881b71f88949cbf",
                "8d7d867b97eeb3a9196c3926da550ad042941c1b");
        public static final MavenObject JDOM = new MavenObject("jdom", "org/jdom", "jdom",
                "1.1", 153000, 235000, 445000, "1d04c0f321ea337f3661cf7ede8f4c6f653a8fdd",
        public static final MavenObject JDOM = new MavenObject("jdom", "org/jdom", "jdom", "1.1",
                153000, 235000, 445000, "1d04c0f321ea337f3661cf7ede8f4c6f653a8fdd",
                "a7ed425c4c46605b8f2bf2ee118c1609682f4f2c",
                "f3df91edccba2f07a0fced70887c2f7b7836cb75");
src/com/gitblit/BuildThumbnails.java
File was renamed from src/com/gitblit/Thumbnailer.java
@@ -33,7 +33,7 @@
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
public class Thumbnailer {
public class BuildThumbnails {
    public static void main(String[] args) {
        Params params = new Params();
@@ -127,7 +127,7 @@
        @Parameter(names = { "--destinationFolder" }, description = "Destination folder for thumbnails", required = true)
        public String destinationFolder;
        @Parameter(names = { "--maxDimension" }, description = "Maximum width or height for thumbnail", required = true)
        @Parameter(names = { "--maximumDimension" }, description = "Maximum width or height for thumbnail", required = true)
        public int maximumDimension;
    }
src/com/gitblit/BuildWebXml.java
@@ -24,6 +24,12 @@
import java.util.List;
import java.util.Vector;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.gitblit.utils.StringUtils;
public class BuildWebXml {
    private static final String PARAMS = "<!-- PARAMS -->";
@@ -34,9 +40,21 @@
    private static final String PARAM_PATTERN = "\n\t<context-param>\n\t\t<param-name>{0}</param-name>\n\t\t<param-value>{1}</param-value>\n\t</context-param>\n";
    public static void main(String[] args) throws Exception {
        Params params = new Params();
        JCommander jc = new JCommander(params);
        try {
            jc.parse(args);
        } catch (ParameterException t) {
            System.err.println(t.getMessage());
            jc.usage();
        }
        generateWebXml(params);
    }
    private static void generateWebXml(Params params) throws Exception {
        // Read the current Gitblit properties
        BufferedReader propertiesReader = new BufferedReader(new FileReader(new File(
                "distrib/gitblit.properties")));
                params.propertiesFile)));
        Vector<Setting> settings = new Vector<Setting>();
        List<String> comments = new ArrayList<String>();
@@ -68,11 +86,11 @@
            for (String comment : setting.comments) {
                parameters.append(MessageFormat.format(COMMENT_PATTERN, comment));
            }
            parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name, setting.value));
            parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name, StringUtils.escapeForHtml(setting.value, false)));
        }
        // Read the prototype web.xml file
        File webxml = new File("src/WEB-INF/web.xml");
        File webxml = new File(params.sourceFile);
        char[] buffer = new char[(int) webxml.length()];
        FileReader webxmlReader = new FileReader(webxml);
        webxmlReader.read(buffer);
@@ -90,7 +108,7 @@
        sb.append(webXmlContent.substring(idx + PARAMS.length()));
        // Save the merged web.xml to the war build folder
        FileOutputStream os = new FileOutputStream(new File("war/WEB-INF/web.xml"), false);
        FileOutputStream os = new FileOutputStream(new File(params.destinationFile), false);
        os.write(sb.toString().getBytes());
        os.close();
    }
@@ -110,4 +128,18 @@
            this.comments = new ArrayList<String>(comments);
        }
    }
    @Parameters(separators = " ")
    private static class Params {
        @Parameter(names = { "--sourceFile" }, description = "Source web.xml file", required = true)
        public String sourceFile;
        @Parameter(names = { "--propertiesFile" }, description = "Properties settings file", required = true)
        public String propertiesFile;
        @Parameter(names = { "--destinationFile" }, description = "Destination web.xml file", required = true)
        public String destinationFile;
    }
}
src/com/gitblit/Constants.java
@@ -36,9 +36,9 @@
    public static final String GIT_PATH = "/git/";
    public static final String ZIP_PATH = "/zip/";
    public static final String SYNDICATION_PATH = "/feed/";
    public static final String BORDER = "***********************************************************";
    public static enum AccessRestrictionType {
src/com/gitblit/FileSettings.java
@@ -45,7 +45,7 @@
                Properties props = new Properties();
                is = new FileInputStream(propertiesFile);
                props.load(is);
                // load properties after we have successfully read file
                properties.clear();
                properties.putAll(props);
@@ -67,6 +67,10 @@
        return properties;
    }
    protected long lastRead() {
        return lastread;
    }
    @Override
    public String toString() {
        return propertiesFile.getAbsolutePath();
src/com/gitblit/FileUserService.java
File was renamed from src/com/gitblit/FileLoginService.java
@@ -22,8 +22,10 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,12 +33,44 @@
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
public class FileLoginService extends FileSettings implements ILoginService {
public class FileUserService extends FileSettings implements IUserService {
    private final Logger logger = LoggerFactory.getLogger(FileLoginService.class);
    private final Logger logger = LoggerFactory.getLogger(FileUserService.class);
    public FileLoginService(File realmFile) {
    private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
    public FileUserService(File realmFile) {
        super(realmFile.getAbsolutePath());
    }
    @Override
    public boolean supportsCookies() {
        return true;
    }
    @Override
    public char[] getCookie(UserModel model) {
        Properties allUsers = super.read();
        String value = allUsers.getProperty(model.username);
        String[] roles = value.split(",");
        String password = roles[0];
        String cookie = StringUtils.getSHA1(model.username + password);
        return cookie.toCharArray();
    }
    @Override
    public UserModel authenticate(char[] cookie) {
        String hash = new String(cookie);
        if (StringUtils.isEmpty(hash)) {
            return null;
        }
        read();
        UserModel model = null;
        if (cookies.containsKey(hash)) {
            String username = cookies.get(hash);
            model = getUserModel(username);
        }
        return model;
    }
    @Override
@@ -149,7 +183,7 @@
    }
    @Override
    public List<String> getUsernamesForRole(String role) {
    public List<String> getUsernamesForRepository(String role) {
        List<String> list = new ArrayList<String>();
        try {
            Properties allUsers = read();
@@ -172,7 +206,7 @@
    }
    @Override
    public boolean setUsernamesForRole(String role, List<String> usernames) {
    public boolean setUsernamesForRepository(String role, List<String> usernames) {
        try {
            Set<String> specifiedUsers = new HashSet<String>(usernames);
            Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
@@ -239,7 +273,7 @@
    }
    @Override
    public boolean renameRole(String oldRole, String newRole) {
    public boolean renameRepositoryRole(String oldRole, String newRole) {
        try {
            Properties allUsers = read();
            Set<String> needsRenameRole = new HashSet<String>();
@@ -294,7 +328,7 @@
    }
    @Override
    public boolean deleteRole(String role) {
    public boolean deleteRepositoryRole(String role) {
        try {
            Properties allUsers = read();
            Set<String> needsDeleteRole = new HashSet<String>();
@@ -369,4 +403,21 @@
                    realmFileCopy.getAbsolutePath()));
        }
    }
    @Override
    protected synchronized Properties read() {
        long lastRead = lastRead();
        Properties allUsers = super.read();
        if (lastRead != lastRead()) {
            // reload hash cache
            cookies.clear();
            for (String username : allUsers.stringPropertyNames()) {
                String value = allUsers.getProperty(username);
                String[] roles = value.split(",");
                String password = roles[0];
                cookies.put(StringUtils.getSHA1(username + password), username);
            }
        }
        return allUsers;
    }
}
src/com/gitblit/GitBlit.java
@@ -27,7 +27,9 @@
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.Cookie;
import org.apache.wicket.protocol.http.WebResponse;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
@@ -55,7 +57,7 @@
    private boolean exportAll = true;
    private ILoginService loginService;
    private IUserService userService;
    private IStoredSettings storedSettings;
@@ -105,44 +107,81 @@
        return cloneUrls;
    }
    public void setLoginService(ILoginService loginService) {
        logger.info("Setting up login service " + loginService.toString());
        this.loginService = loginService;
    public void setUserService(IUserService userService) {
        logger.info("Setting up user service " + userService.toString());
        this.userService = userService;
    }
    public UserModel authenticate(String username, char[] password) {
        if (loginService == null) {
        if (userService == null) {
            return null;
        }
        return loginService.authenticate(username, password);
        return userService.authenticate(username, password);
    }
    public UserModel authenticate(Cookie[] cookies) {
        if (userService == null) {
            return null;
        }
        if (userService.supportsCookies()) {
            if (cookies != null && cookies.length > 0) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(Constants.NAME)) {
                        String value = cookie.getValue();
                        return userService.authenticate(value.toCharArray());
                    }
                }
            }
        }
        return null;
    }
    public void setCookie(WebResponse response, UserModel user) {
        if (userService == null) {
            return;
        }
        if (userService.supportsCookies()) {
            Cookie userCookie;
            if (user == null) {
                // clear cookie for logout
                userCookie = new Cookie(Constants.NAME, "");
            } else {
                // set cookie for login
                char[] cookie = userService.getCookie(user);
                userCookie = new Cookie(Constants.NAME, new String(cookie));
                userCookie.setMaxAge(Integer.MAX_VALUE);
            }
            userCookie.setPath("/");
            response.addCookie(userCookie);
        }
    }
    public List<String> getAllUsernames() {
        List<String> names = new ArrayList<String>(loginService.getAllUsernames());
        List<String> names = new ArrayList<String>(userService.getAllUsernames());
        Collections.sort(names);
        return names;
    }
    public boolean deleteUser(String username) {
        return loginService.deleteUser(username);
        return userService.deleteUser(username);
    }
    public UserModel getUserModel(String username) {
        UserModel user = loginService.getUserModel(username);
        UserModel user = userService.getUserModel(username);
        return user;
    }
    public List<String> getRepositoryUsers(RepositoryModel repository) {
        return loginService.getUsernamesForRole(repository.name);
        return userService.getUsernamesForRepository(repository.name);
    }
    public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
        return loginService.setUsernamesForRole(repository.name, repositoryUsers);
        return userService.setUsernamesForRepository(repository.name, repositoryUsers);
    }
    public void editUserModel(String username, UserModel user, boolean isCreate)
            throws GitBlitException {
        if (!loginService.updateUserModel(username, user)) {
        if (!userService.updateUserModel(username, user)) {
            throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
        }
    }
@@ -181,6 +220,9 @@
    public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
        RepositoryModel model = getRepositoryModel(repositoryName);
        if (model == null) {
            return null;
        }
        if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
            if (user != null && user.canAccessRepository(model.name)) {
                return model;
@@ -261,7 +303,7 @@
                            repository.name));
                }
                // rename the roles
                if (!loginService.renameRole(repositoryName, repository.name)) {
                if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
                    throw new GitBlitException(MessageFormat.format(
                            "Failed to rename repository permissions ''{0}'' to ''{1}''.",
                            repositoryName, repository.name));
@@ -309,7 +351,7 @@
            File folder = new File(repositoriesFolder, repositoryName);
            if (folder.exists() && folder.isDirectory()) {
                FileUtils.delete(folder, FileUtils.RECURSIVE);
                if (loginService.deleteRole(repositoryName)) {
                if (userService.deleteRepositoryRole(repositoryName)) {
                    return true;
                }
            }
@@ -360,13 +402,13 @@
        repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git"));
        logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
        repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
        String realm = settings.getString(Keys.realm.realmFile, "users.properties");
        ILoginService loginService = null;
        String realm = settings.getString(Keys.realm.userService, "users.properties");
        IUserService loginService = null;
        try {
            // Check to see if this "file" is a login service class
            Class<?> realmClass = Class.forName(realm);
            if (ILoginService.class.isAssignableFrom(realmClass)) {
                loginService = (ILoginService) realmClass.newInstance();
            if (IUserService.class.isAssignableFrom(realmClass)) {
                loginService = (IUserService) realmClass.newInstance();
            }
        } catch (Throwable t) {
            // Not a login service class OR other issue
@@ -380,9 +422,9 @@
                            MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
                }
            }
            loginService = new FileLoginService(realmFile);
            loginService = new FileUserService(realmFile);
        }
        setLoginService(loginService);
        setUserService(loginService);
    }
    @Override
src/com/gitblit/GitBlitServer.java
@@ -111,7 +111,7 @@
     * Start Server.
     */
    private static void start(Params params) {
        FileSettings settings = params.FILESETTINGS;
        FileSettings settings = Params.FILESETTINGS;
        logger = LoggerFactory.getLogger(GitBlitServer.class);
        logger.info(Constants.BORDER);
@@ -194,15 +194,15 @@
        sessionManager.setSecureCookies(params.port <= 0 && params.securePort > 0);
        rootContext.getSessionHandler().setSessionManager(sessionManager);
        // Ensure there is a defined Login Service
        String realmUsers = params.realmFile;
        // Ensure there is a defined User Service
        String realmUsers = params.userService;
        if (StringUtils.isEmpty(realmUsers)) {
            logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.realmFile));
            logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService));
            return;
        }
        // Override settings
        settings.overrideSetting(Keys.realm.realmFile, params.realmFile);
        settings.overrideSetting(Keys.realm.userService, params.userService);
        settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder);
        // Set the server's contexts
@@ -342,7 +342,7 @@
        @Parameter(names = { "--stop" }, description = "Stop Server")
        public Boolean stop = false;
        @Parameter(names = { "--tempFolder" }, description = "Server temp folder")
        @Parameter(names = { "--tempFolder" }, description = "Folder for server to extract built-in webapp")
        public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp");
        /*
@@ -355,8 +355,8 @@
        /*
         * Authentication Parameters
         */
        @Parameter(names = { "--realmFile" }, description = "Users Realm Hash File")
        public String realmFile = FILESETTINGS.getString(Keys.realm.realmFile, "users.properties");
        @Parameter(names = { "--userService" }, description = "Authentication and Authorization Service (filename or fully qualified classname)")
        public String userService = FILESETTINGS.getString(Keys.realm.userService, "users.properties");
        /*
         * JETTY Parameters
src/com/gitblit/IStoredSettings.java
@@ -27,7 +27,7 @@
public abstract class IStoredSettings {
    protected final Logger logger;
    protected final Properties overrides = new Properties();
    public IStoredSettings(Class<? extends IStoredSettings> clazz) {
@@ -35,7 +35,7 @@
    }
    protected abstract Properties read();
    private Properties getSettings() {
        Properties props = read();
        props.putAll(overrides);
@@ -110,7 +110,7 @@
        }
        return strings;
    }
    public void overrideSetting(String key, String value) {
        overrides.put(key, value);
    }
src/com/gitblit/IUserService.java
File was renamed from src/com/gitblit/ILoginService.java
@@ -19,7 +19,13 @@
import com.gitblit.models.UserModel;
public interface ILoginService {
public interface IUserService {
    boolean supportsCookies();
    char[] getCookie(UserModel model);
    UserModel authenticate(char[] cookie);
    UserModel authenticate(String username, char[] password);
@@ -35,13 +41,13 @@
    List<String> getAllUsernames();
    List<String> getUsernamesForRole(String role);
    List<String> getUsernamesForRepository(String role);
    boolean setUsernamesForRole(String role, List<String> usernames);
    boolean setUsernamesForRepository(String role, List<String> usernames);
    boolean renameRole(String oldRole, String newRole);
    boolean renameRepositoryRole(String oldRole, String newRole);
    boolean deleteRole(String role);
    boolean deleteRepositoryRole(String role);
    String toString();
}
src/com/gitblit/SyndicationServlet.java
@@ -62,7 +62,7 @@
        }
        return url.toString();
    }
    public static String getTitle(String repository, String objectId) {
        String id = objectId;
        if (!StringUtils.isEmpty(id)) {
src/com/gitblit/WebXmlSettings.java
@@ -20,20 +20,28 @@
import javax.servlet.ServletContext;
import com.gitblit.utils.StringUtils;
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);
            properties.put(key, decodeValue(value));
            logger.debug(key + "=" + properties.getProperty(key));
        }
    }
    
    private String decodeValue(String value) {
        // Decode escaped backslashes and HTML entities
        return StringUtils.decodeFromHtml(value).replace("\\\\", "\\");
    }
    @Override
    protected Properties read() {
        return properties;
src/com/gitblit/models/UserModel.java
@@ -43,7 +43,7 @@
    }
    @Override
    public String getName() {
    public String getName() {
        return username;
    }
src/com/gitblit/utils/JGitUtils.java
@@ -402,12 +402,12 @@
    public static List<PathChangeModel> getFilesInCommit(Repository r, RevCommit commit) {
        List<PathChangeModel> list = new ArrayList<PathChangeModel>();
        RevWalk rw = new RevWalk(r);
        RevWalk rw = new RevWalk(r);
        try {
            if (commit == null) {
                ObjectId object = r.resolve(Constants.HEAD);
                commit = rw.parseCommit(object);
            }
            }
            if (commit.getParentCount() == 0) {
                TreeWalk tw = new TreeWalk(r);
@@ -441,7 +441,7 @@
        } catch (Throwable t) {
            LOGGER.error("failed to determine files in commit!", t);
        } finally {
            rw.dispose();
            rw.dispose();
        }
        return list;
    }
@@ -526,6 +526,9 @@
    public static List<RevCommit> getRevLog(Repository r, String objectId, String path, int offset,
            int maxCount) {
        List<RevCommit> list = new ArrayList<RevCommit>();
        if (maxCount == 0) {
            return list;
        }
        if (!hasCommits(r)) {
            return list;
        }
@@ -591,6 +594,9 @@
            final SearchType type, int offset, int maxCount) {
        final String lcValue = value.toLowerCase();
        List<RevCommit> list = new ArrayList<RevCommit>();
        if (maxCount == 0) {
            return list;
        }
        if (!hasCommits(r)) {
            return list;
        }
@@ -677,6 +683,9 @@
    private static List<RefModel> getRefs(Repository r, String refs, boolean fullName, int maxCount) {
        List<RefModel> list = new ArrayList<RefModel>();
        if (maxCount == 0) {
            return list;
        }
        try {
            Map<String, Ref> map = r.getRefDatabase().getRefs(refs);
            RevWalk rw = new RevWalk(r);
src/com/gitblit/utils/StringUtils.java
@@ -58,6 +58,11 @@
        return retStr.toString();
    }
    public static String decodeFromHtml(String inStr) {
        return inStr.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
                .replace("&quot;", "\"").replace("&nbsp;", " ");
    }
    public static String encodeURL(String inStr) {
        StringBuffer retStr = new StringBuffer();
        int i = 0;
@@ -165,7 +170,7 @@
        }
        return sb.toString();
    }
    public static String getRootPath(String path) {
        if (path.indexOf('/') > -1) {
            return path.substring(0, path.lastIndexOf('/'));
src/com/gitblit/wicket/pages/BasePage.java
@@ -19,13 +19,17 @@
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,6 +38,7 @@
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.UserModel;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.LinkPanel;
@@ -45,14 +50,40 @@
    public BasePage() {
        super();
        logger = LoggerFactory.getLogger(getClass());
        loginByCookie();
    }
    public BasePage(PageParameters params) {
        super(params);
        logger = LoggerFactory.getLogger(getClass());
        loginByCookie();
    }
    private void loginByCookie() {
        if (!GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) {
            return;
        }
        UserModel user = null;
        // Grab cookie from Browser Session
        Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies();
        if (cookies != null && cookies.length > 0) {
            user = GitBlit.self().authenticate(cookies);
        }
        // Login the user
        if (user != null) {
            // Set the user into the session
            GitBlitWebSession.get().setUser(user);
            // Set Cookie
            WebResponse response = (WebResponse) getRequestCycle().getResponse();
            GitBlit.self().setCookie(response, user);
        }
    }
    protected void setupPage(String repositoryName, String pageName) {
        if (repositoryName != null && repositoryName.trim().length() > 0) {
            add(new Label("title", getServerName() + " - " + repositoryName));
        } else {
@@ -122,7 +153,7 @@
        HttpServletRequest req = servletWebRequest.getHttpServletRequest();
        return req.getServerName();
    }
    public void warn(String message, Throwable t) {
        logger.warn(message, t);
    }
@@ -131,7 +162,7 @@
        logger.error(message);
        if (redirect) {
            GitBlitWebSession.get().cacheErrorMessage(message);
            throw new RestartResponseAtInterceptPageException(getApplication().getHomePage());
            throw new RestartResponseException(getApplication().getHomePage());
        } else {
            super.error(message);
        }
@@ -141,9 +172,18 @@
        logger.error(message, t);
        if (redirect) {
            GitBlitWebSession.get().cacheErrorMessage(message);
            throw new RestartResponseAtInterceptPageException(getApplication().getHomePage());
            throw new RestartResponseException(getApplication().getHomePage());
        } else {
            super.error(message);
        }
    }
    public void authenticationError(String message) {
        logger.error(message);
        if (GitBlitWebSession.get().isLoggedIn()) {
            error(message, true);
        } else {
            throw new RestartResponseAtInterceptPageException(LoginPage.class);
        }
    }
}
src/com/gitblit/wicket/pages/CommitPage.java
@@ -128,8 +128,8 @@
                        SearchType.AUTHOR));
                item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
                        .getAuthorIdent().getWhen(), getTimeZone()));
                item.add(new Label("noteContent", GitBlit.self().processCommitMessage(repositoryName, entry.content))
                        .setEscapeModelStrings(false));
                item.add(new Label("noteContent", GitBlit.self().processCommitMessage(
                        repositoryName, entry.content)).setEscapeModelStrings(false));
            }
        };
        add(notesView.setVisible(notes.size() > 0));
src/com/gitblit/wicket/pages/DocsPage.java
@@ -64,8 +64,8 @@
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class,
                        WicketUtils.newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                        .newPathParameter(repositoryName, entry.commitId, entry.path)));
                WicketUtils.setAlternatingBackground(item, counter);
src/com/gitblit/wicket/pages/EditUserPage.java
@@ -130,7 +130,8 @@
                    String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
                    if (type.equalsIgnoreCase("md5")) {
                        // store MD5 digest of password
                        userModel.password = StringUtils.MD5_TYPE + StringUtils.getMD5(userModel.password);
                        userModel.password = StringUtils.MD5_TYPE
                                + StringUtils.getMD5(userModel.password);
                    }
                }
src/com/gitblit/wicket/pages/LogPage.java
@@ -27,7 +27,7 @@
        super(params);
        addSyndicationDiscoveryLink();
        int pageNumber = WicketUtils.getPage(params);
        int prevPage = Math.max(0, pageNumber - 1);
        int nextPage = pageNumber + 1;
src/com/gitblit/wicket/pages/LoginPage.java
@@ -15,7 +15,10 @@
 */
package com.gitblit.wicket.pages;
import javax.servlet.http.Cookie;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.PasswordTextField;
@@ -24,6 +27,8 @@
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.Constants;
import com.gitblit.GitBlit;
@@ -42,8 +47,11 @@
        // If we are already logged in because user directly accessed
        // the login url, redirect to the home page
        if (GitBlitWebSession.get().isLoggedIn()) {
            setRedirect(true);
            setResponsePage(getApplication().getHomePage());
            throw new RestartResponseException(getApplication().getHomePage());
        }
        if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) {
            loginByCookie();
        }
        add(new Label("title", GitBlit.getString(Keys.web.siteName, Constants.NAME)));
@@ -72,11 +80,30 @@
        add(loginForm);
    }
    private void loginByCookie() {
        UserModel user = null;
        // Grab cookie from Browser Session
        Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies();
        if (cookies != null && cookies.length > 0) {
            user = GitBlit.self().authenticate(cookies);
        }
        // Login the user
        loginUser(user);
    }
    private void loginUser(UserModel user) {
        if (user != null) {
            // Set the user into the session
            GitBlitWebSession.get().setUser(user);
            // Set Cookie
            if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) {
                WebResponse response = (WebResponse) getRequestCycle().getResponse();
                GitBlit.self().setCookie(response, user);
            }
            if (!continueToOriginalDestination()) {
                // Redirect to home page
                setResponsePage(getApplication().getHomePage());
src/com/gitblit/wicket/pages/LogoutPage.java
@@ -16,11 +16,16 @@
package com.gitblit.wicket.pages;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.GitBlit;
import com.gitblit.wicket.GitBlitWebSession;
public class LogoutPage extends WebPage {
    public LogoutPage() {
        getSession().invalidate();
        GitBlitWebSession.get().invalidate();
        GitBlit.self().setCookie(((WebResponse) getResponse()), null);
        setRedirect(true);
        setResponsePage(getApplication().getHomePage());
    }
src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -15,6 +15,7 @@
 */
package com.gitblit.wicket.pages;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -205,7 +206,7 @@
            RepositoryModel model = GitBlit.self().getRepositoryModel(
                    GitBlitWebSession.get().getUser(), repositoryName);
            if (model == null) {
                error("Unauthorized access for repository " + repositoryName, true);
                authenticationError("Unauthorized access for repository " + repositoryName);
                return null;
            }
            m = model;
@@ -333,7 +334,9 @@
        return WicketUtils.newPathParameter(repositoryName, objectId, path);
    }
    private static class PageRegistration {
    private static class PageRegistration implements Serializable {
        private static final long serialVersionUID = 1L;
        final String translationKey;
        final Class<? extends BasePage> pageClass;
@@ -343,7 +346,7 @@
        }
    }
    private static class SearchForm extends StatelessForm<Void> {
    private static class SearchForm extends StatelessForm<Void> implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String repositoryName;
src/com/gitblit/wicket/pages/SummaryPage.html
@@ -32,15 +32,11 @@
    <!-- commits -->
    <div style="padding-bottom:10px;" wicket:id="commitsPanel">[commits panel]</div>    
    <!-- branches -->
    <div style="padding-bottom:10px;width:400px; float:left;">
        <div wicket:id="branchesPanel">[branches panel]</div>
    </div>
    <!-- tags -->
    <div style="padding-bottom:10px;margin-left:405px;">
        <div wicket:id="tagsPanel">[tags panel]</div>
    </div>
    <div style="padding-bottom:10px;" wicket:id="tagsPanel">[tags panel]</div>
    <!-- branches -->
    <div style="padding-bottom:10px;" wicket:id="branchesPanel">[branches panel]</div>
    
    <!-- markdown readme -->
    <div wicket:id="readme" class="markdown" style="clear:both;padding-bottom:5px;"></div>
src/com/gitblit/wicket/pages/SummaryPage.java
@@ -58,18 +58,11 @@
    public SummaryPage(PageParameters params) {
        super(params);
        int numCommitsDef = 20;
        int numRefsDef = 5;
        int numberCommits = GitBlit.getInteger(Keys.web.summaryCommitCount, numCommitsDef);
        int numberCommits = GitBlit.getInteger(Keys.web.summaryCommitCount, 20);
        if (numberCommits <= 0) {
            numberCommits = numCommitsDef;
            numberCommits = 20;
        }
        int numberRefs = GitBlit.getInteger(Keys.web.summaryRefsCount, numRefsDef);
        if (numberRefs <= 0) {
            numberRefs = numRefsDef;
        }
        int numberRefs = GitBlit.getInteger(Keys.web.summaryRefsCount, 5);
        Repository r = getRepository();
        List<Metric> metrics = null;
@@ -78,7 +71,7 @@
            metrics = MetricUtils.getDateMetrics(r, null, true, null);
            metricsTotal = metrics.remove(0);
        }
        addSyndicationDiscoveryLink();
        // repository description
@@ -121,7 +114,7 @@
                add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
            }
            StringBuilder sb = new StringBuilder();
            sb.append(WicketUtils.getHostURL(getRequestCycle().getRequest()));
            sb.append(WicketUtils.getHostURL(getRequestCycle().getRequest()));
            sb.append(Constants.GIT_PATH);
            sb.append(repositoryName);
            repositoryUrls.add(sb.toString());
src/com/gitblit/wicket/panels/BranchesPanel.html
@@ -8,14 +8,15 @@
<wicket:panel>
    <!-- header -->
    <div class="header" wicket:id="branches">[branches header]</div>
    <div class="header"><img style="vertical-align: top;" src="commit_branch_16x16.png"></img><span wicket:id="branches">[branches header]</span></div>
    
    <table class="pretty">
        <tbody>
               <tr wicket:id="branch">
                 <td class="date"><span wicket:id="branchDate">[branch date]</span></td>
                 <td><span wicket:id="branchName">[branch name]</span></td>
                 <td><span wicket:id="branchType">[branch type]</span></td>
                 <td class="author"><span wicket:id="branchAuthor">[branch author]</span></td>
                 <td><span wicket:id="branchLog">[branch log]</span></td>
                 <td class="rightAlign">
                     <span wicket:id="branchLinks"></span>
                </td>
src/com/gitblit/wicket/panels/BranchesPanel.java
@@ -27,18 +27,20 @@
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.StringResourceModel;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import com.gitblit.SyndicationServlet;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
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.pages.BranchesPage;
import com.gitblit.wicket.pages.CommitPage;
import com.gitblit.wicket.pages.LogPage;
import com.gitblit.wicket.pages.MetricsPage;
import com.gitblit.wicket.pages.SearchPage;
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.TreePage;
@@ -90,11 +92,24 @@
                        entry.displayName, 28), LogPage.class, WicketUtils.newObjectParameter(
                        model.name, entry.getName())));
                // only show branch type on the branches page
                boolean remote = entry.getName().startsWith(Constants.R_REMOTES);
                item.add(new Label("branchType", remote ? getString("gb.remote")
                        : getString("gb.local")).setVisible(maxCount <= 0));
                String author = entry.getAuthorIdent().getName();
                LinkPanel authorLink = new LinkPanel("branchAuthor", "list", author,
                        SearchPage.class, WicketUtils.newSearchParameter(model.name, entry.getName(),
                                author, SearchType.AUTHOR));
                setPersonSearchTooltip(authorLink, author, SearchType.AUTHOR);
                item.add(authorLink);
                // short message
                String shortMessage = entry.getShortMessage();
                String trimmedMessage = StringUtils.trimShortLog(shortMessage);
                LinkPanel shortlog = new LinkPanel("branchLog", "list subject",
                        trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
                                model.name, entry.getName()));
                if (!shortMessage.equals(trimmedMessage)) {
                    WicketUtils.setHtmlTooltip(shortlog, shortMessage);
                }
                item.add(shortlog);
                if (maxCount <= 0) {
                    Fragment fragment = new Fragment("branchLinks", "branchPageLinks", this);
                    fragment.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
@@ -103,8 +118,9 @@
                            .newObjectParameter(model.name, entry.getName())));
                    fragment.add(new BookmarkablePageLink<Void>("metrics", MetricsPage.class,
                            WicketUtils.newObjectParameter(model.name, entry.getName())));
                    fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
                            .getRelativePathPrefixToContextRoot(), model.name, entry.getName(), 0)));
                    fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink(
                            getRequest().getRelativePathPrefixToContextRoot(), model.name,
                            entry.getName(), 0)));
                    item.add(fragment);
                } else {
                    Fragment fragment = new Fragment("branchLinks", "branchPanelLinks", this);
src/com/gitblit/wicket/panels/LogPanel.html
@@ -8,8 +8,7 @@
<wicket:panel>
    <!-- header -->    
    <div class="header" wicket:id="header">[log header]</div>
    <div class="header"><img style="vertical-align: top;" src="commit_changes_16x16.png"></img><span wicket:id="header">[log header]</span></div>
    <table class="pretty">
        <tbody>
               <tr wicket:id="commit">
src/com/gitblit/wicket/panels/SearchPanel.java
@@ -74,7 +74,7 @@
        // header
        add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
        add(new Label("searchString", value));
        add(new Label("searchType", searchType.toString()));
src/com/gitblit/wicket/panels/TagsPanel.html
@@ -8,7 +8,7 @@
<wicket:panel>
    <!-- tags -->
    <div class="header" wicket:id="header">[tags header]</div>
    <div class="header"><img style="vertical-align: top;" src="tag_16x16.png"></img><span wicket:id="header">[tags header]</span></div>
    <table class="pretty">
        <tbody>
            <tr wicket:id="tag">
src/com/gitblit/wicket/panels/TagsPanel.java
@@ -90,13 +90,10 @@
                item.add(new LinkPanel("tagName", "list name", entry.displayName, linkClass,
                        WicketUtils.newObjectParameter(repositoryName, entry
                                .getReferencedObjectId().getName())));
                String message;
                if (maxCount > 0) {
                    message = StringUtils.trimString(entry.getShortMessage(), 40);
                } else {
                    // workaround for RevTag returning a lengthy shortlog. :(
                    message = StringUtils.trimShortLog(entry.getShortMessage());
                }
                // workaround for RevTag returning a lengthy shortlog. :(
                String message = StringUtils.trimShortLog(entry.getShortMessage());
                if (linkClass.equals(BlobPage.class)) {
                    // Blob Tag Object
                    item.add(WicketUtils.newImage("tagIcon", "file_16x16.png"));
src/org/eclipse/jgit/api/BlameCommand.java
New file
@@ -0,0 +1,227 @@
/*
 * Copyright (C) 2011, GitHub Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.api;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.blame.BlameGenerator;
import org.eclipse.jgit.blame.BlameResult;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
/**
 * Blame command for building a {@link BlameResult} for a file path.
 */
public class BlameCommand extends GitCommand<BlameResult> {
    private String path;
    private DiffAlgorithm diffAlgorithm;
    private RawTextComparator textComparator;
    private ObjectId startCommit;
    private Collection<ObjectId> reverseEndCommits;
    private Boolean followFileRenames;
    /**
     * @param repo
     */
    public BlameCommand(Repository repo) {
        super(repo);
    }
    /**
     * Set file path
     *
     * @param filePath
     * @return this command
     */
    public BlameCommand setFilePath(String filePath) {
        this.path = filePath;
        return this;
    }
    /**
     * Set diff algorithm
     *
     * @param diffAlgorithm
     * @return this command
     */
    public BlameCommand setDiffAlgorithm(DiffAlgorithm diffAlgorithm) {
        this.diffAlgorithm = diffAlgorithm;
        return this;
    }
    /**
     * Set raw text comparator
     *
     * @param textComparator
     * @return this command
     */
    public BlameCommand setTextComparator(RawTextComparator textComparator) {
        this.textComparator = textComparator;
        return this;
    }
    /**
     * Set start commit id
     *
     * @param commit
     * @return this command
     */
    public BlameCommand setStartCommit(AnyObjectId commit) {
        this.startCommit = commit.toObjectId();
        return this;
    }
    /**
     * Enable (or disable) following file renames.
     * <p>
     * If true renames are followed using the standard FollowFilter behavior
     * used by RevWalk (which matches {@code git log --follow} in the C
     * implementation). This is not the same as copy/move detection as
     * implemented by the C implementation's of {@code git blame -M -C}.
     *
     * @param follow
     *            enable following.
     * @return {@code this}
     */
    public BlameCommand setFollowFileRenames(boolean follow) {
        followFileRenames = Boolean.valueOf(follow);
        return this;
    }
    /**
     * Configure the command to compute reverse blame (history of deletes).
     *
     * @param start
     *            oldest commit to traverse from. The result file will be loaded
     *            from this commit's tree.
     * @param end
     *            most recent commit to stop traversal at. Usually an active
     *            branch tip, tag, or HEAD.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameCommand reverse(AnyObjectId start, AnyObjectId end)
            throws IOException {
        return reverse(start, Collections.singleton(end.toObjectId()));
    }
    /**
     * Configure the generator to compute reverse blame (history of deletes).
     *
     * @param start
     *            oldest commit to traverse from. The result file will be loaded
     *            from this commit's tree.
     * @param end
     *            most recent commits to stop traversal at. Usually an active
     *            branch tip, tag, or HEAD.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameCommand reverse(AnyObjectId start, Collection<ObjectId> end)
            throws IOException {
        startCommit = start.toObjectId();
        reverseEndCommits = new ArrayList<ObjectId>(end);
        return this;
    }
    /**
     * Generate a list of lines with information about when the lines were
     * introduced into the file path.
     *
     * @return list of lines
     */
    public BlameResult call() throws JGitInternalException {
        checkCallable();
        BlameGenerator gen = new BlameGenerator(repo, path);
        try {
            if (diffAlgorithm != null)
                gen.setDiffAlgorithm(diffAlgorithm);
            if (textComparator != null)
                gen.setTextComparator(textComparator);
            if (followFileRenames != null)
                gen.setFollowFileRenames(followFileRenames.booleanValue());
            if (reverseEndCommits != null)
                gen.reverse(startCommit, reverseEndCommits);
            else if (startCommit != null)
                gen.push(null, startCommit);
            else {
                gen.push(null, repo.resolve(Constants.HEAD));
                if (!repo.isBare()) {
                    DirCache dc = repo.readDirCache();
                    int entry = dc.findEntry(path);
                    if (0 <= entry)
                        gen.push(null, dc.getEntry(entry).getObjectId());
                    File inTree = new File(repo.getWorkTree(), path);
                    if (inTree.isFile())
                        gen.push(null, new RawText(inTree));
                }
            }
            return gen.computeBlameResult();
        } catch (IOException e) {
            throw new JGitInternalException(e.getMessage(), e);
        } finally {
            gen.release();
        }
    }
}
src/org/eclipse/jgit/blame/BlameGenerator.java
New file
@@ -0,0 +1,961 @@
/*
 * Copyright (C) 2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.blame;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jgit.blame.Candidate.BlobCandidate;
import org.eclipse.jgit.blame.Candidate.ReverseCandidate;
import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.diff.RenameDetector;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
/**
 * Generate author information for lines based on introduction to the file.
 * <p>
 * Applications that want a simple one-shot computation of blame for a file
 * should use {@link #computeBlameResult()} to prepare the entire result in one
 * method call. This may block for significant time as the history of the
 * repository must be traversed until information is gathered for every line.
 * <p>
 * Applications that want more incremental update behavior may use either the
 * raw {@link #next()} streaming approach supported by this class, or construct
 * a {@link BlameResult} using {@link BlameResult#create(BlameGenerator)} and
 * incrementally construct the result with {@link BlameResult#computeNext()}.
 * <p>
 * This class is not thread-safe.
 * <p>
 * An instance of BlameGenerator can only be used once. To blame multiple files
 * the application must create a new BlameGenerator.
 * <p>
 * During blame processing there are two files involved:
 * <ul>
 * <li>result - The file whose lines are being examined. This is the revision
 * the user is trying to view blame/annotation information alongside of.</li>
 * <li>source - The file that was blamed with supplying one or more lines of
 * data into result. The source may be a different file path (due to copy or
 * rename). Source line numbers may differ from result line numbers due to lines
 * being added/removed in intermediate revisions.</li>
 * </ul>
 * <p>
 * The blame algorithm is implemented by initially assigning responsibility for
 * all lines of the result to the starting commit. A difference against the
 * commit's ancestor is computed, and responsibility is passed to the ancestor
 * commit for any lines that are common. The starting commit is blamed only for
 * the lines that do not appear in the ancestor, if any. The loop repeats using
 * the ancestor, until there are no more lines to acquire information on, or the
 * file's creation point is discovered in history.
 */
public class BlameGenerator {
    private final Repository repository;
    private final PathFilter resultPath;
    private final MutableObjectId idBuf;
    /** Revision pool used to acquire commits from. */
    private RevWalk revPool;
    /** Indicates the commit has already been processed. */
    private RevFlag SEEN;
    private ObjectReader reader;
    private TreeWalk treeWalk;
    private DiffAlgorithm diffAlgorithm = new HistogramDiff();
    private RawTextComparator textComparator = RawTextComparator.DEFAULT;
    private RenameDetector renameDetector;
    /** Potential candidates, sorted by commit time descending. */
    private Candidate queue;
    /** Number of lines that still need to be discovered. */
    private int remaining;
    /** Blame is currently assigned to this source. */
    private Candidate currentSource;
    /**
     * Create a blame generator for the repository and path
     *
     * @param repository
     *            repository to access revision data from.
     * @param path
     *            initial path of the file to start scanning.
     */
    public BlameGenerator(Repository repository, String path) {
        this.repository = repository;
        this.resultPath = PathFilter.create(path);
        idBuf = new MutableObjectId();
        setFollowFileRenames(true);
        initRevPool(false);
        remaining = -1;
    }
    private void initRevPool(boolean reverse) {
        if (queue != null)
            throw new IllegalStateException();
        if (revPool != null)
            revPool.release();
        if (reverse)
            revPool = new ReverseWalk(getRepository());
        else
            revPool = new RevWalk(getRepository());
        revPool.setRetainBody(true);
        SEEN = revPool.newFlag("SEEN");
        reader = revPool.getObjectReader();
        treeWalk = new TreeWalk(reader);
    }
    /** @return repository being scanned for revision history. */
    public Repository getRepository() {
        return repository;
    }
    /** @return path file path being processed. */
    public String getResultPath() {
        return resultPath.getPath();
    }
    /**
     * Difference algorithm to use when comparing revisions.
     *
     * @param algorithm
     * @return {@code this}
     */
    public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) {
        diffAlgorithm = algorithm;
        return this;
    }
    /**
     * Text comparator to use when comparing revisions.
     *
     * @param comparator
     * @return {@code this}
     */
    public BlameGenerator setTextComparator(RawTextComparator comparator) {
        textComparator = comparator;
        return this;
    }
    /**
     * Enable (or disable) following file renames, on by default.
     * <p>
     * If true renames are followed using the standard FollowFilter behavior
     * used by RevWalk (which matches {@code git log --follow} in the C
     * implementation). This is not the same as copy/move detection as
     * implemented by the C implementation's of {@code git blame -M -C}.
     *
     * @param follow
     *            enable following.
     * @return {@code this}
     */
    public BlameGenerator setFollowFileRenames(boolean follow) {
        if (follow)
            renameDetector = new RenameDetector(getRepository());
        else
            renameDetector = null;
        return this;
    }
    /**
     * Obtain the RenameDetector if {@code setFollowFileRenames(true)}.
     *
     * @return the rename detector, allowing the application to configure its
     *         settings for rename score and breaking behavior.
     */
    public RenameDetector getRenameDetector() {
        return renameDetector;
    }
    /**
     * Push a candidate blob onto the generator's traversal stack.
     * <p>
     * Candidates should be pushed in history order from oldest-to-newest.
     * Applications should push the starting commit first, then the index
     * revision (if the index is interesting), and finally the working tree
     * copy (if the working tree is interesting).
     *
     * @param description
     *            description of the blob revision, such as "Working Tree".
     * @param contents
     *            contents of the file.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameGenerator push(String description, byte[] contents)
            throws IOException {
        return push(description, new RawText(contents));
    }
    /**
     * Push a candidate blob onto the generator's traversal stack.
     * <p>
     * Candidates should be pushed in history order from oldest-to-newest.
     * Applications should push the starting commit first, then the index
     * revision (if the index is interesting), and finally the working tree copy
     * (if the working tree is interesting).
     *
     * @param description
     *            description of the blob revision, such as "Working Tree".
     * @param contents
     *            contents of the file.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameGenerator push(String description, RawText contents)
            throws IOException {
        if (description == null)
            // XXX description = JGitText.get().blameNotCommittedYet;
            description = "blame not committed yet";
        BlobCandidate c = new BlobCandidate(description, resultPath);
        c.sourceText = contents;
        c.regionList = new Region(0, 0, contents.size());
        remaining = contents.size();
        push(c);
        return this;
    }
    /**
     * Push a candidate object onto the generator's traversal stack.
     * <p>
     * Candidates should be pushed in history order from oldest-to-newest.
     * Applications should push the starting commit first, then the index
     * revision (if the index is interesting), and finally the working tree copy
     * (if the working tree is interesting).
     *
     * @param description
     *            description of the blob revision, such as "Working Tree".
     * @param id
     *            may be a commit or a blob.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameGenerator push(String description, AnyObjectId id)
            throws IOException {
        ObjectLoader ldr = reader.open(id);
        if (ldr.getType() == OBJ_BLOB) {
            if (description == null)
                // XXX description = JGitText.get().blameNotCommittedYet;
                description = "blame not committed yet";
            BlobCandidate c = new BlobCandidate(description, resultPath);
            c.sourceBlob = id.toObjectId();
            c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
            c.regionList = new Region(0, 0, c.sourceText.size());
            remaining = c.sourceText.size();
            push(c);
            return this;
        }
        RevCommit commit = revPool.parseCommit(id);
        if (!find(commit, resultPath))
            return this;
        Candidate c = new Candidate(commit, resultPath);
        c.sourceBlob = idBuf.toObjectId();
        c.loadText(reader);
        c.regionList = new Region(0, 0, c.sourceText.size());
        remaining = c.sourceText.size();
        push(c);
        return this;
    }
    /**
     * Configure the generator to compute reverse blame (history of deletes).
     * <p>
     * This method is expensive as it immediately runs a RevWalk over the
     * history spanning the expression {@code start..end} (end being more recent
     * than start) and then performs the equivalent operation as
     * {@link #push(String, AnyObjectId)} to begin blame traversal from the
     * commit named by {@code start} walking forwards through history until
     * {@code end} blaming line deletions.
     * <p>
     * A reverse blame may produce multiple sources for the same result line,
     * each of these is a descendant commit that removed the line, typically
     * this occurs when the same deletion appears in multiple side branches such
     * as due to a cherry-pick. Applications relying on reverse should use
     * {@link BlameResult} as it filters these duplicate sources and only
     * remembers the first (oldest) deletion.
     *
     * @param start
     *            oldest commit to traverse from. The result file will be loaded
     *            from this commit's tree.
     * @param end
     *            most recent commit to stop traversal at. Usually an active
     *            branch tip, tag, or HEAD.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameGenerator reverse(AnyObjectId start, AnyObjectId end)
            throws IOException {
        return reverse(start, Collections.singleton(end.toObjectId()));
    }
    /**
     * Configure the generator to compute reverse blame (history of deletes).
     * <p>
     * This method is expensive as it immediately runs a RevWalk over the
     * history spanning the expression {@code start..end} (end being more recent
     * than start) and then performs the equivalent operation as
     * {@link #push(String, AnyObjectId)} to begin blame traversal from the
     * commit named by {@code start} walking forwards through history until
     * {@code end} blaming line deletions.
     * <p>
     * A reverse blame may produce multiple sources for the same result line,
     * each of these is a descendant commit that removed the line, typically
     * this occurs when the same deletion appears in multiple side branches such
     * as due to a cherry-pick. Applications relying on reverse should use
     * {@link BlameResult} as it filters these duplicate sources and only
     * remembers the first (oldest) deletion.
     *
     * @param start
     *            oldest commit to traverse from. The result file will be loaded
     *            from this commit's tree.
     * @param end
     *            most recent commits to stop traversal at. Usually an active
     *            branch tip, tag, or HEAD.
     * @return {@code this}
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameGenerator reverse(AnyObjectId start,
            Collection<? extends ObjectId> end) throws IOException {
        initRevPool(true);
        ReverseCommit result = (ReverseCommit) revPool.parseCommit(start);
        if (!find(result, resultPath))
            return this;
        revPool.markUninteresting(result);
        for (ObjectId id : end)
            revPool.markStart(revPool.parseCommit(id));
        while (revPool.next() != null) {
            // just pump the queue
        }
        ReverseCandidate c = new ReverseCandidate(result, resultPath);
        c.sourceBlob = idBuf.toObjectId();
        c.loadText(reader);
        c.regionList = new Region(0, 0, c.sourceText.size());
        remaining = c.sourceText.size();
        push(c);
        return this;
    }
    /**
     * Execute the generator in a blocking fashion until all data is ready.
     *
     * @return the complete result. Null if no file exists for the given path.
     * @throws IOException
     *             the repository cannot be read.
     */
    public BlameResult computeBlameResult() throws IOException {
        try {
            BlameResult r = BlameResult.create(this);
            if (r != null)
                r.computeAll();
            return r;
        } finally {
            release();
        }
    }
    /**
     * Step the blame algorithm one iteration.
     *
     * @return true if the generator has found a region's source. The getSource*
     *         and {@link #getResultStart()}, {@link #getResultEnd()} methods
     *         can be used to inspect the region found. False if there are no
     *         more regions to describe.
     * @throws IOException
     *             repository cannot be read.
     */
    public boolean next() throws IOException {
        // If there is a source still pending, produce the next region.
        if (currentSource != null) {
            Region r = currentSource.regionList;
            Region n = r.next;
            remaining -= r.length;
            if (n != null) {
                currentSource.regionList = n;
                return true;
            }
            if (currentSource.queueNext != null)
                return result(currentSource.queueNext);
            currentSource = null;
        }
        // If there are no lines remaining, the entire result is done,
        // even if there are revisions still available for the path.
        if (remaining == 0)
            return done();
        for (;;) {
            Candidate n = pop();
            if (n == null)
                return done();
            int pCnt = n.getParentCount();
            if (pCnt == 1) {
                if (processOne(n))
                    return true;
            } else if (1 < pCnt) {
                if (processMerge(n))
                    return true;
            } else if (n instanceof ReverseCandidate) {
                // Do not generate a tip of a reverse. The region
                // survives and should not appear to be deleted.
            } else /* if (pCnt == 0) */{
                // Root commit, with at least one surviving region.
                // Assign the remaining blame here.
                return result(n);
            }
        }
    }
    private boolean done() {
        release();
        return false;
    }
    private boolean result(Candidate n) throws IOException {
        if (n.sourceCommit != null)
            revPool.parseBody(n.sourceCommit);
        currentSource = n;
        return true;
    }
    private boolean reverseResult(Candidate parent, Candidate source)
            throws IOException {
        // On a reverse blame present the application the parent
        // (as this is what did the removals), however the region
        // list to enumerate is the source's surviving list.
        Candidate res = parent.copy(parent.sourceCommit);
        res.regionList = source.regionList;
        return result(res);
    }
    private Candidate pop() {
        Candidate n = queue;
        if (n != null) {
            queue = n.queueNext;
            n.queueNext = null;
        }
        return n;
    }
    private void push(BlobCandidate toInsert) {
        Candidate c = queue;
        if (c != null) {
            c.regionList = null;
            toInsert.parent = c;
        }
        queue = toInsert;
    }
    private void push(Candidate toInsert) {
        // Mark sources to ensure they get discarded (above) if
        // another path to the same commit.
        toInsert.add(SEEN);
        // Insert into the queue using descending commit time, so
        // the most recent commit will pop next.
        int time = toInsert.getTime();
        Candidate n = queue;
        if (n == null || time >= n.getTime()) {
            toInsert.queueNext = n;
            queue = toInsert;
            return;
        }
        for (Candidate p = n;; p = n) {
            n = p.queueNext;
            if (n == null || time >= n.getTime()) {
                toInsert.queueNext = n;
                p.queueNext = toInsert;
                return;
            }
        }
    }
    private boolean processOne(Candidate n) throws IOException {
        RevCommit parent = n.getParent(0);
        if (parent == null)
            return split(n.getNextCandidate(0), n);
        if (parent.has(SEEN))
            return false;
        revPool.parseHeaders(parent);
        if (find(parent, n.sourcePath)) {
            if (idBuf.equals(n.sourceBlob)) {
                // The common case of the file not being modified in
                // a simple string-of-pearls history. Blame parent.
                n.sourceCommit = parent;
                push(n);
                return false;
            }
            Candidate next = n.create(parent, n.sourcePath);
            next.sourceBlob = idBuf.toObjectId();
            next.loadText(reader);
            return split(next, n);
        }
        if (n.sourceCommit == null)
            return result(n);
        DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
        if (r == null)
            return result(n);
        if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
            // A 100% rename without any content change can also
            // skip directly to the parent.
            n.sourceCommit = parent;
            n.sourcePath = PathFilter.create(r.getOldPath());
            push(n);
            return false;
        }
        Candidate next = n.create(parent, PathFilter.create(r.getOldPath()));
        next.sourceBlob = r.getOldId().toObjectId();
        next.renameScore = r.getScore();
        next.loadText(reader);
        return split(next, n);
    }
    private boolean split(Candidate parent, Candidate source)
            throws IOException {
        EditList editList = diffAlgorithm.diff(textComparator,
                parent.sourceText, source.sourceText);
        if (editList.isEmpty()) {
            // Ignoring whitespace (or some other special comparator) can
            // cause non-identical blobs to have an empty edit list. In
            // a case like this push the parent alone.
            parent.regionList = source.regionList;
            push(parent);
            return false;
        }
        parent.takeBlame(editList, source);
        if (parent.regionList != null)
            push(parent);
        if (source.regionList != null) {
            if (source instanceof ReverseCandidate)
                return reverseResult(parent, source);
            return result(source);
        }
        return false;
    }
    private boolean processMerge(Candidate n) throws IOException {
        int pCnt = n.getParentCount();
        for (int pIdx = 0; pIdx < pCnt; pIdx++) {
            RevCommit parent = n.getParent(pIdx);
            if (parent.has(SEEN))
                continue;
            revPool.parseHeaders(parent);
        }
        // If any single parent exactly matches the merge, follow only
        // that one parent through history.
        ObjectId[] ids = null;
        for (int pIdx = 0; pIdx < pCnt; pIdx++) {
            RevCommit parent = n.getParent(pIdx);
            if (parent.has(SEEN))
                continue;
            if (!find(parent, n.sourcePath))
                continue;
            if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob)) {
                n.sourceCommit = parent;
                push(n);
                return false;
            }
            if (ids == null)
                ids = new ObjectId[pCnt];
            ids[pIdx] = idBuf.toObjectId();
        }
        // If rename detection is enabled, search for any relevant names.
        DiffEntry[] renames = null;
        if (renameDetector != null) {
            renames = new DiffEntry[pCnt];
            for (int pIdx = 0; pIdx < pCnt; pIdx++) {
                RevCommit parent = n.getParent(pIdx);
                if (parent.has(SEEN))
                    continue;
                if (ids != null && ids[pIdx] != null)
                    continue;
                DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
                if (r == null)
                    continue;
                if (n instanceof ReverseCandidate) {
                    if (ids == null)
                        ids = new ObjectId[pCnt];
                    ids[pCnt] = r.getOldId().toObjectId();
                } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
                    // A 100% rename without any content change can also
                    // skip directly to the parent. Note this bypasses an
                    // earlier parent that had the path (above) but did not
                    // have an exact content match. For performance reasons
                    // we choose to follow the one parent over trying to do
                    // possibly both parents.
                    n.sourceCommit = parent;
                    n.sourcePath = PathFilter.create(r.getOldPath());
                    push(n);
                    return false;
                }
                renames[pIdx] = r;
            }
        }
        // Construct the candidate for each parent.
        Candidate[] parents = new Candidate[pCnt];
        for (int pIdx = 0; pIdx < pCnt; pIdx++) {
            RevCommit parent = n.getParent(pIdx);
            if (parent.has(SEEN))
                continue;
            Candidate p;
            if (renames != null && renames[pIdx] != null) {
                p = n.create(parent,
                        PathFilter.create(renames[pIdx].getOldPath()));
                p.renameScore = renames[pIdx].getScore();
                p.sourceBlob = renames[pIdx].getOldId().toObjectId();
            } else if (ids != null && ids[pIdx] != null) {
                p = n.create(parent, n.sourcePath);
                p.sourceBlob = ids[pIdx];
            } else {
                continue;
            }
            EditList editList;
            if (n instanceof ReverseCandidate
                    && p.sourceBlob.equals(n.sourceBlob)) {
                // This special case happens on ReverseCandidate forks.
                p.sourceText = n.sourceText;
                editList = new EditList(0);
            } else {
                p.loadText(reader);
                editList = diffAlgorithm.diff(textComparator,
                        p.sourceText, n.sourceText);
            }
            if (editList.isEmpty()) {
                // Ignoring whitespace (or some other special comparator) can
                // cause non-identical blobs to have an empty edit list. In
                // a case like this push the parent alone.
                if (n instanceof ReverseCandidate) {
                    parents[pIdx] = p;
                    continue;
                }
                p.regionList = n.regionList;
                push(p);
                return false;
            }
            p.takeBlame(editList, n);
            // Only remember this parent candidate if there is at least
            // one region that was blamed on the parent.
            if (p.regionList != null) {
                // Reverse blame requires inverting the regions. This puts
                // the regions the parent deleted from us into the parent,
                // and retains the common regions to look at other parents
                // for deletions.
                if (n instanceof ReverseCandidate) {
                    Region r = p.regionList;
                    p.regionList = n.regionList;
                    n.regionList = r;
                }
                parents[pIdx] = p;
            }
        }
        if (n instanceof ReverseCandidate) {
            // On a reverse blame report all deletions found in the children,
            // and pass on to them a copy of our region list.
            Candidate resultHead = null;
            Candidate resultTail = null;
            for (int pIdx = 0; pIdx < pCnt; pIdx++) {
                Candidate p = parents[pIdx];
                if (p == null)
                    continue;
                if (p.regionList != null) {
                    Candidate r = p.copy(p.sourceCommit);
                    if (resultTail != null) {
                        resultTail.queueNext = r;
                        resultTail = r;
                    } else {
                        resultHead = r;
                        resultTail = r;
                    }
                }
                if (n.regionList != null) {
                    p.regionList = n.regionList.deepCopy();
                    push(p);
                }
            }
            if (resultHead != null)
                return result(resultHead);
            return false;
        }
        // Push any parents that are still candidates.
        for (int pIdx = 0; pIdx < pCnt; pIdx++) {
            if (parents[pIdx] != null)
                push(parents[pIdx]);
        }
        if (n.regionList != null)
            return result(n);
        return false;
    }
    /**
     * Get the revision blamed for the current region.
     * <p>
     * The source commit may be null if the line was blamed to an uncommitted
     * revision, such as the working tree copy, or during a reverse blame if the
     * line survives to the end revision (e.g. the branch tip).
     *
     * @return current revision being blamed.
     */
    public RevCommit getSourceCommit() {
        return currentSource.sourceCommit;
    }
    /** @return current author being blamed. */
    public PersonIdent getSourceAuthor() {
        return currentSource.getAuthor();
    }
    /** @return current committer being blamed. */
    public PersonIdent getSourceCommitter() {
        RevCommit c = getSourceCommit();
        return c != null ? c.getCommitterIdent() : null;
    }
    /** @return path of the file being blamed. */
    public String getSourcePath() {
        return currentSource.sourcePath.getPath();
    }
    /** @return rename score if a rename occurred in {@link #getSourceCommit}. */
    public int getRenameScore() {
        return currentSource.renameScore;
    }
    /**
     * @return first line of the source data that has been blamed for the
     *         current region. This is line number of where the region was added
     *         during {@link #getSourceCommit()} in file
     *         {@link #getSourcePath()}.
     */
    public int getSourceStart() {
        return currentSource.regionList.sourceStart;
    }
    /**
     * @return one past the range of the source data that has been blamed for
     *         the current region. This is line number of where the region was
     *         added during {@link #getSourceCommit()} in file
     *         {@link #getSourcePath()}.
     */
    public int getSourceEnd() {
        Region r = currentSource.regionList;
        return r.sourceStart + r.length;
    }
    /**
     * @return first line of the result that {@link #getSourceCommit()} has been
     *         blamed for providing. Line numbers use 0 based indexing.
     */
    public int getResultStart() {
        return currentSource.regionList.resultStart;
    }
    /**
     * @return one past the range of the result that {@link #getSourceCommit()}
     *         has been blamed for providing. Line numbers use 0 based indexing.
     *         Because a source cannot be blamed for an empty region of the
     *         result, {@link #getResultEnd()} is always at least one larger
     *         than {@link #getResultStart()}.
     */
    public int getResultEnd() {
        Region r = currentSource.regionList;
        return r.resultStart + r.length;
    }
    /**
     * @return number of lines in the current region being blamed to
     *         {@link #getSourceCommit()}. This is always the value of the
     *         expression {@code getResultEnd() - getResultStart()}, but also
     *         {@code getSourceEnd() - getSourceStart()}.
     */
    public int getRegionLength() {
        return currentSource.regionList.length;
    }
    /**
     * @return complete contents of the source file blamed for the current
     *         output region. This is the contents of {@link #getSourcePath()}
     *         within {@link #getSourceCommit()}. The source contents is
     *         temporarily available as an artifact of the blame algorithm. Most
     *         applications will want the result contents for display to users.
     */
    public RawText getSourceContents() {
        return currentSource.sourceText;
    }
    /**
     * @return complete file contents of the result file blame is annotating.
     *         This value is accessible only after being configured and only
     *         immediately before the first call to {@link #next()}. Returns
     *         null if the path does not exist.
     * @throws IOException
     *             repository cannot be read.
     * @throws IllegalStateException
     *             {@link #next()} has already been invoked.
     */
    public RawText getResultContents() throws IOException {
        return queue != null ? queue.sourceText : null;
    }
    /** Release the current blame session. */
    public void release() {
        revPool.release();
        queue = null;
        currentSource = null;
    }
    private boolean find(RevCommit commit, PathFilter path) throws IOException {
        treeWalk.setFilter(path);
        treeWalk.reset(commit.getTree());
        while (treeWalk.next()) {
            if (path.isDone(treeWalk)) {
                if (treeWalk.getFileMode(0).getObjectType() != OBJ_BLOB)
                    return false;
                treeWalk.getObjectId(idBuf, 0);
                return true;
            }
            if (treeWalk.isSubtree())
                treeWalk.enterSubtree();
        }
        return false;
    }
    private DiffEntry findRename(RevCommit parent, RevCommit commit,
            PathFilter path) throws IOException {
        if (renameDetector == null)
            return null;
        treeWalk.setFilter(TreeFilter.ANY_DIFF);
        treeWalk.reset(parent.getTree(), commit.getTree());
        renameDetector.addAll(DiffEntry.scan(treeWalk));
        for (DiffEntry ent : renameDetector.compute()) {
            if (isRename(ent) && ent.getNewPath().equals(path.getPath()))
                return ent;
        }
        return null;
    }
    private static boolean isRename(DiffEntry ent) {
        return ent.getChangeType() == ChangeType.RENAME
                || ent.getChangeType() == ChangeType.COPY;
    }
}
src/org/eclipse/jgit/blame/BlameResult.java
New file
@@ -0,0 +1,356 @@
/*
 * Copyright (C) 2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
/**
 * Collects line annotations for inspection by applications.
 * <p>
 * A result is usually updated incrementally as the BlameGenerator digs back
 * further through history. Applications that want to lay annotations down text
 * to the original source file in a viewer may find the BlameResult structure an
 * easy way to acquire the information, at the expense of keeping tables in
 * memory tracking every line of the result file.
 * <p>
 * This class is not thread-safe.
 * <p>
 * During blame processing there are two files involved:
 * <ul>
 * <li>result - The file whose lines are being examined. This is the revision
 * the user is trying to view blame/annotation information alongside of.</li>
 * <li>source - The file that was blamed with supplying one or more lines of
 * data into result. The source may be a different file path (due to copy or
 * rename). Source line numbers may differ from result line numbers due to lines
 * being added/removed in intermediate revisions.</li>
 * </ul>
 */
public class BlameResult {
    /**
     * Construct a new BlameResult for a generator.
     *
     * @param gen
     *            the generator the result will consume records from.
     * @return the new result object. null if the generator cannot find the path
     *         it starts from.
     * @throws IOException
     *             the repository cannot be read.
     */
    public static BlameResult create(BlameGenerator gen) throws IOException {
        String path = gen.getResultPath();
        RawText contents = gen.getResultContents();
        if (contents == null) {
            gen.release();
            return null;
        }
        return new BlameResult(gen, path, contents);
    }
    private final String resultPath;
    private final RevCommit[] sourceCommits;
    private final PersonIdent[] sourceAuthors;
    private final PersonIdent[] sourceCommitters;
    private final String[] sourcePaths;
    /** Warning: these are actually 1-based. */
    private final int[] sourceLines;
    private RawText resultContents;
    private BlameGenerator generator;
    private int lastLength;
    BlameResult(BlameGenerator bg, String path, RawText text) {
        generator = bg;
        resultPath = path;
        resultContents = text;
        int cnt = text.size();
        sourceCommits = new RevCommit[cnt];
        sourceAuthors = new PersonIdent[cnt];
        sourceCommitters = new PersonIdent[cnt];
        sourceLines = new int[cnt];
        sourcePaths = new String[cnt];
    }
    /** @return path of the file this result annotates. */
    public String getResultPath() {
        return resultPath;
    }
    /** @return contents of the result file, available for display. */
    public RawText getResultContents() {
        return resultContents;
    }
    /** Throw away the {@link #getResultContents()}. */
    public void discardResultContents() {
        resultContents = null;
    }
    /**
     * Check if the given result line has been annotated yet.
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return true if the data has been annotated, false otherwise.
     */
    public boolean hasSourceData(int idx) {
        return sourceLines[idx] != 0;
    }
    /**
     * Check if the given result line has been annotated yet.
     *
     * @param start
     *            first index to examine.
     * @param end
     *            last index to examine.
     * @return true if the data has been annotated, false otherwise.
     */
    public boolean hasSourceData(int start, int end) {
        for (; start < end; start++)
            if (sourceLines[start] == 0)
                return false;
        return true;
    }
    /**
     * Get the commit that provided the specified line of the result.
     * <p>
     * The source commit may be null if the line was blamed to an uncommitted
     * revision, such as the working tree copy, or during a reverse blame if the
     * line survives to the end revision (e.g. the branch tip).
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return commit that provided line {@code idx}. May be null.
     */
    public RevCommit getSourceCommit(int idx) {
        return sourceCommits[idx];
    }
    /**
     * Get the author that provided the specified line of the result.
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return author that provided line {@code idx}. May be null.
     */
    public PersonIdent getSourceAuthor(int idx) {
        return sourceAuthors[idx];
    }
    /**
     * Get the committer that provided the specified line of the result.
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return committer that provided line {@code idx}. May be null.
     */
    public PersonIdent getSourceCommitter(int idx) {
        return sourceCommitters[idx];
    }
    /**
     * Get the file path that provided the specified line of the result.
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return source file path that provided line {@code idx}.
     */
    public String getSourcePath(int idx) {
        return sourcePaths[idx];
    }
    /**
     * Get the corresponding line number in the source file.
     *
     * @param idx
     *            line to read data of, 0 based.
     * @return matching line number in the source file.
     */
    public int getSourceLine(int idx) {
        return sourceLines[idx] - 1;
    }
    /**
     * Compute all pending information.
     *
     * @throws IOException
     *             the repository cannot be read.
     */
    public void computeAll() throws IOException {
        BlameGenerator gen = generator;
        if (gen == null)
            return;
        try {
            while (gen.next())
                loadFrom(gen);
        } finally {
            gen.release();
            generator = null;
        }
    }
    /**
     * Compute the next available segment and return the first index.
     * <p>
     * Computes one segment and returns to the caller the first index that is
     * available. After return the caller can also inspect {@link #lastLength()}
     * to determine how many lines of the result were computed.
     *
     * @return index that is now available. -1 if no more are available.
     * @throws IOException
     *             the repository cannot be read.
     */
    public int computeNext() throws IOException {
        BlameGenerator gen = generator;
        if (gen == null)
            return -1;
        if (gen.next()) {
            loadFrom(gen);
            lastLength = gen.getRegionLength();
            return gen.getResultStart();
        } else {
            gen.release();
            generator = null;
            return -1;
        }
    }
    /** @return length of the last segment found by {@link #computeNext()}. */
    public int lastLength() {
        return lastLength;
    }
    /**
     * Compute until the entire range has been populated.
     *
     * @param start
     *            first index to examine.
     * @param end
     *            last index to examine.
     * @throws IOException
     *             the repository cannot be read.
     */
    public void computeRange(int start, int end) throws IOException {
        BlameGenerator gen = generator;
        if (gen == null)
            return;
        while (start < end) {
            if (hasSourceData(start, end))
                return;
            if (!gen.next()) {
                gen.release();
                generator = null;
                return;
            }
            loadFrom(gen);
            // If the result contains either end of our current range bounds,
            // update the bounds to avoid scanning that section during the
            // next loop iteration.
            int resLine = gen.getResultStart();
            int resEnd = gen.getResultEnd();
            if (resLine <= start && start < resEnd)
                start = resEnd;
            if (resLine <= end && end < resEnd)
                end = resLine;
        }
    }
    @Override
    public String toString() {
        StringBuilder r = new StringBuilder();
        r.append("BlameResult: ");
        r.append(getResultPath());
        return r.toString();
    }
    private void loadFrom(BlameGenerator gen) {
        RevCommit srcCommit = gen.getSourceCommit();
        PersonIdent srcAuthor = gen.getSourceAuthor();
        PersonIdent srcCommitter = gen.getSourceCommitter();
        String srcPath = gen.getSourcePath();
        int srcLine = gen.getSourceStart();
        int resLine = gen.getResultStart();
        int resEnd = gen.getResultEnd();
        for (; resLine < resEnd; resLine++) {
            // Reverse blame can generate multiple results for the same line.
            // Favor the first one selected, as this is the oldest and most
            // likely to be nearest to the inquiry made by the user.
            if (sourceLines[resLine] != 0)
                continue;
            sourceCommits[resLine] = srcCommit;
            sourceAuthors[resLine] = srcAuthor;
            sourceCommitters[resLine] = srcCommitter;
            sourcePaths[resLine] = srcPath;
            // Since sourceLines is 1-based to permit hasSourceData to use 0 to
            // mean the line has not been annotated yet, pre-increment instead
            // of the traditional post-increment when making the assignment.
            sourceLines[resLine] = ++srcLine;
        }
    }
}
src/org/eclipse/jgit/blame/Candidate.java
New file
@@ -0,0 +1,386 @@
/*
 * Copyright (C) 2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.treewalk.filter.PathFilter;
/**
 * A source that may have supplied some (or all) of the result file.
 * <p>
 * Candidates are kept in a queue by BlameGenerator, allowing the generator to
 * perform a parallel search down the parents of any merges that are discovered
 * during the history traversal. Each candidate retains a {@link #regionList}
 * describing sections of the result file the candidate has taken responsibility
 * for either directly or indirectly through its history. Actual blame from this
 * region list will be assigned to the candidate when its ancestor commit(s) are
 * themselves converted into Candidate objects and the ancestor's candidate uses
 * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections
 * of the result.
 */
class Candidate {
    /** Next candidate in the candidate queue. */
    Candidate queueNext;
    /** Commit being considered (or blamed, depending on state). */
    RevCommit sourceCommit;
    /** Path of the candidate file in {@link #sourceCommit}. */
    PathFilter sourcePath;
    /** Unique name of the candidate blob in {@link #sourceCommit}. */
    ObjectId sourceBlob;
    /** Complete contents of the file in {@link #sourceCommit}. */
    RawText sourceText;
    /**
     * Chain of regions this candidate may be blamed for.
     * <p>
     * This list is always kept sorted by resultStart order, making it simple to
     * merge-join with the sorted EditList during blame assignment.
     */
    Region regionList;
    /**
     * Score assigned to the rename to this candidate.
     * <p>
     * Consider the history "A<-B<-C". If the result file S in C was renamed to
     * R in B, the rename score for this rename will be held in this field by
     * the candidate object for B. By storing the score with B, the application
     * can see what the rename score was as it makes the transition from C/S to
     * B/R. This may seem backwards since it was C that performed the rename,
     * but the application doesn't learn about path R until B.
     */
    int renameScore;
    Candidate(RevCommit commit, PathFilter path) {
        sourceCommit = commit;
        sourcePath = path;
    }
    int getParentCount() {
        return sourceCommit.getParentCount();
    }
    RevCommit getParent(int idx) {
        return sourceCommit.getParent(idx);
    }
    Candidate getNextCandidate(@SuppressWarnings("unused") int idx) {
        return null;
    }
    void add(RevFlag flag) {
        sourceCommit.add(flag);
    }
    int getTime() {
        return sourceCommit.getCommitTime();
    }
    PersonIdent getAuthor() {
        return sourceCommit.getAuthorIdent();
    }
    Candidate create(RevCommit commit, PathFilter path) {
        return new Candidate(commit, path);
    }
    Candidate copy(RevCommit commit) {
        Candidate r = create(commit, sourcePath);
        r.sourceBlob = sourceBlob;
        r.sourceText = sourceText;
        r.regionList = regionList;
        r.renameScore = renameScore;
        return r;
    }
    void loadText(ObjectReader reader) throws IOException {
        ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB);
        sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
    }
    void takeBlame(EditList editList, Candidate child) {
        blame(editList, this, child);
    }
    private static void blame(EditList editList, Candidate a, Candidate b) {
        Region r = b.clearRegionList();
        Region aTail = null;
        Region bTail = null;
        for (int eIdx = 0; eIdx < editList.size();) {
            // If there are no more regions left, neither side has any
            // more responsibility for the result. Remaining edits can
            // be safely ignored.
            if (r == null)
                return;
            Edit e = editList.get(eIdx);
            // Edit ends before the next candidate region. Skip the edit.
            if (e.getEndB() <= r.sourceStart) {
                eIdx++;
                continue;
            }
            // Next candidate region starts before the edit. Assign some
            // of the blame onto A, but possibly split and also on B.
            if (r.sourceStart < e.getBeginB()) {
                int d = e.getBeginB() - r.sourceStart;
                if (r.length <= d) {
                    // Pass the blame for this region onto A.
                    Region next = r.next;
                    r.sourceStart = e.getBeginA() - d;
                    aTail = add(aTail, a, r);
                    r = next;
                    continue;
                }
                // Split the region and assign some to A, some to B.
                aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d));
                r.slideAndShrink(d);
            }
            // At this point e.getBeginB() <= r.sourceStart.
            // An empty edit on the B side isn't relevant to this split,
            // as it does not overlap any candidate region.
            if (e.getLengthB() == 0) {
                eIdx++;
                continue;
            }
            // If the region ends before the edit, blame on B.
            int rEnd = r.sourceStart + r.length;
            if (rEnd <= e.getEndB()) {
                Region next = r.next;
                bTail = add(bTail, b, r);
                r = next;
                if (rEnd == e.getEndB())
                    eIdx++;
                continue;
            }
            // This region extends beyond the edit. Blame the first
            // half of the region on B, and process the rest after.
            int len = e.getEndB() - r.sourceStart;
            bTail = add(bTail, b, r.splitFirst(r.sourceStart, len));
            r.slideAndShrink(len);
            eIdx++;
        }
        if (r == null)
            return;
        // For any remaining region, pass the blame onto A after shifting
        // the source start to account for the difference between the two.
        Edit e = editList.get(editList.size() - 1);
        int endB = e.getEndB();
        int d = endB - e.getEndA();
        if (aTail == null)
            a.regionList = r;
        else
            aTail.next = r;
        do {
            if (endB <= r.sourceStart)
                r.sourceStart -= d;
            r = r.next;
        } while (r != null);
    }
    private static Region add(Region aTail, Candidate a, Region n) {
        // If there is no region on the list, use only this one.
        if (aTail == null) {
            a.regionList = n;
            n.next = null;
            return n;
        }
        // If the prior region ends exactly where the new region begins
        // in both the result and the source, combine these together into
        // one contiguous region. This occurs when intermediate commits
        // have inserted and deleted lines in the middle of a region. Try
        // to report this region as a single region to the application,
        // rather than in fragments.
        if (aTail.resultStart + aTail.length == n.resultStart
                && aTail.sourceStart + aTail.length == n.sourceStart) {
            aTail.length += n.length;
            return aTail;
        }
        // Append the region onto the end of the list.
        aTail.next = n;
        n.next = null;
        return n;
    }
    private Region clearRegionList() {
        Region r = regionList;
        regionList = null;
        return r;
    }
    @Override
    public String toString() {
        StringBuilder r = new StringBuilder();
        r.append("Candidate[");
        r.append(sourcePath.getPath());
        if (sourceCommit != null)
            r.append(" @ ").append(sourceCommit.abbreviate(6).name());
        if (regionList != null)
            r.append(" regions:").append(regionList);
        r.append("]");
        return r.toString();
    }
    /**
     * Special candidate type used for reverse blame.
     * <p>
     * Reverse blame inverts the commit history graph to follow from a commit to
     * its descendant children, rather than the normal history direction of
     * child to parent. These types require a {@link ReverseCommit} which keeps
     * children pointers, allowing reverse navigation of history.
     */
    static final class ReverseCandidate extends Candidate {
        ReverseCandidate(ReverseCommit commit, PathFilter path) {
            super(commit, path);
        }
        @Override
        int getParentCount() {
            return ((ReverseCommit) sourceCommit).getChildCount();
        }
        @Override
        RevCommit getParent(int idx) {
            return ((ReverseCommit) sourceCommit).getChild(idx);
        }
        @Override
        int getTime() {
            // Invert the timestamp so newer dates sort older.
            return -sourceCommit.getCommitTime();
        }
        @Override
        Candidate create(RevCommit commit, PathFilter path) {
            return new ReverseCandidate((ReverseCommit) commit, path);
        }
        @Override
        public String toString() {
            return "Reverse" + super.toString();
        }
    }
    /**
     * Candidate loaded from a file source, and not a commit.
     * <p>
     * The {@link Candidate#sourceCommit} field is always null on this type of
     * candidate. Instead history traversal follows the single {@link #parent}
     * field to discover the next Candidate. Often this is a normal Candidate
     * type that has a valid sourceCommit.
     */
    static final class BlobCandidate extends Candidate {
        /**
         * Next candidate to pass blame onto.
         * <p>
         * When computing the differences that this candidate introduced to the
         * file content, the parent's sourceText is used as the base.
         */
        Candidate parent;
        /** Author name to refer to this blob with. */
        String description;
        BlobCandidate(String name, PathFilter path) {
            super(null, path);
            description = name;
        }
        @Override
        int getParentCount() {
            return parent != null ? 1 : 0;
        }
        @Override
        RevCommit getParent(int idx) {
            return null;
        }
        @Override
        Candidate getNextCandidate(int idx) {
            return parent;
        }
        @Override
        void add(RevFlag flag) {
            // Do nothing, sourceCommit is null.
        }
        @Override
        int getTime() {
            return Integer.MAX_VALUE;
        }
        @Override
        PersonIdent getAuthor() {
            return new PersonIdent(description, null);
        }
    }
}
src/org/eclipse/jgit/blame/Region.java
New file
@@ -0,0 +1,133 @@
/*
 * Copyright (C) 2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.blame;
/**
 * Region of the result that still needs to be computed.
 * <p>
 * Regions are held in a singly-linked-list by {@link Candidate} using the
 * {@link Candidate#regionList} field. The list is kept in sorted order by
 * {@link #resultStart}.
 */
class Region {
    /** Next entry in the region linked list. */
    Region next;
    /** First position of this region in the result file blame is computing. */
    int resultStart;
    /** First position in the {@link Candidate} that owns this Region. */
    int sourceStart;
    /** Length of the region, always >= 1. */
    int length;
    Region(int rs, int ss, int len) {
        resultStart = rs;
        sourceStart = ss;
        length = len;
    }
    /**
     * Copy the entire result region, but at a new source position.
     *
     * @param newSource
     *            the new source position.
     * @return the same result region, but offset for a new source.
     */
    Region copy(int newSource) {
        return new Region(resultStart, newSource, length);
    }
    /**
     * Split the region, assigning a new source position to the first half.
     *
     * @param newSource
     *            the new source position.
     * @param newLen
     *            length of the new region.
     * @return the first half of the region, at the new source.
     */
    Region splitFirst(int newSource, int newLen) {
        return new Region(resultStart, newSource, newLen);
    }
    /**
     * Edit this region to remove the first {@code d} elements.
     *
     * @param d
     *            number of elements to remove from the start of this region.
     */
    void slideAndShrink(int d) {
        resultStart += d;
        sourceStart += d;
        length -= d;
    }
    Region deepCopy() {
        Region head = new Region(resultStart, sourceStart, length);
        Region tail = head;
        for (Region n = next; n != null; n = n.next) {
            Region q = new Region(n.resultStart, n.sourceStart, n.length);
            tail.next = q;
            tail = q;
        }
        return head;
    }
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        Region r = this;
        do {
            if (r != this)
                buf.append(',');
            buf.append(r.resultStart);
            buf.append('-');
            buf.append(r.resultStart + r.length);
            r = r.next;
        } while (r != null);
        return buf.toString();
    }
}
src/org/eclipse/jgit/blame/ReverseWalk.java
New file
@@ -0,0 +1,113 @@
/*
 * Copyright (C) 2011, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.eclipse.jgit.blame;
import java.io.IOException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
final class ReverseWalk extends RevWalk {
    ReverseWalk(Repository repo) {
        super(repo);
    }
    @Override
    public ReverseCommit next() throws MissingObjectException,
            IncorrectObjectTypeException, IOException {
        ReverseCommit c = (ReverseCommit) super.next();
        if (c == null)
            return null;
        for (int pIdx = 0; pIdx < c.getParentCount(); pIdx++)
            ((ReverseCommit) c.getParent(pIdx)).addChild(c);
        return c;
    }
    @Override
    protected RevCommit createCommit(AnyObjectId id) {
        return new ReverseCommit(id);
    }
    static final class ReverseCommit extends RevCommit {
        private static final ReverseCommit[] NO_CHILDREN = {};
        private ReverseCommit[] children = NO_CHILDREN;
        ReverseCommit(AnyObjectId id) {
            super(id);
        }
        void addChild(ReverseCommit c) {
            // Always put the most recent child onto the front of the list.
            // This works correctly because our ReverseWalk parent (above)
            // runs in COMMIT_TIME_DESC order. Older commits will be popped
            // later and should go in front of the children list so they are
            // visited first by BlameGenerator when considering candidates.
            int cnt = children.length;
            if (cnt == 0)
                children = new ReverseCommit[] { c };
            else if (cnt == 1)
                children = new ReverseCommit[] { c, children[0] };
            else {
                ReverseCommit[] n = new ReverseCommit[1 + cnt];
                n[0] = c;
                System.arraycopy(children, 0, n, 1, cnt);
                children = n;
            }
        }
        int getChildCount() {
            return children.length;
        }
        ReverseCommit getChild(final int nth) {
            return children[nth];
        }
    }
}
tests/com/gitblit/tests/DiffUtilsTest.java
@@ -35,7 +35,7 @@
        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,
@@ -107,10 +107,11 @@
        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");
        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/GitBlitSuite.java
@@ -24,7 +24,7 @@
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepository;
import com.gitblit.FileLoginService;
import com.gitblit.FileUserService;
import com.gitblit.FileSettings;
import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
@@ -45,6 +45,7 @@
        suite.addTestSuite(ByteFormatTest.class);
        suite.addTestSuite(MarkdownUtilsTest.class);
        suite.addTestSuite(JGitUtilsTest.class);
        suite.addTestSuite(SyndicationUtilsTest.class);
        suite.addTestSuite(DiffUtilsTest.class);
        suite.addTestSuite(MetricUtilsTest.class);
        suite.addTestSuite(TicgitUtilsTest.class);
@@ -72,8 +73,8 @@
    protected void setUp() throws Exception {
        FileSettings settings = new FileSettings("distrib/gitblit.properties");
        GitBlit.self().configureContext(settings);
        FileLoginService loginService = new FileLoginService(new File("distrib/users.properties"));
        GitBlit.self().setLoginService(loginService);
        FileUserService loginService = new FileUserService(new File("distrib/users.properties"));
        GitBlit.self().setUserService(loginService);
        if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
            cloneOrFetch("helloworld.git", "https://github.com/git/hello-world.git");
tests/com/gitblit/tests/GitBlitTest.java
@@ -55,7 +55,7 @@
        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));
@@ -72,7 +72,7 @@
        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"));
@@ -82,50 +82,50 @@
        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.getString("realm.userService", 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.getInteger("realm.userService", 5) == 5);
        assertTrue(GitBlit.getBoolean("git.enableGitServlet", false) == true);
        assertTrue(GitBlit.getString("realm.realmFile", null).equals("users.properties"));
        assertTrue(GitBlit.getString("realm.userService", 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  {
    public void testAuthentication() throws Exception {
        assertTrue(GitBlit.self().authenticate("admin", "admin".toCharArray()) != null);
    }
    public void testRepositories() throws Exception  {
    public void testRepositories() throws Exception {
        assertTrue(GitBlit.self().getRepository("missing") == null);
        assertTrue(GitBlit.self().getRepositoryModel("missing") == null);
    }
tests/com/gitblit/tests/JGitUtilsTest.java
@@ -138,6 +138,7 @@
    public void testBranches() throws Exception {
        Repository repository = GitBlitSuite.getJGitRepository();
        assertTrue(JGitUtils.getLocalBranches(repository, true, 0).size() == 0);
        for (RefModel model : JGitUtils.getLocalBranches(repository, true, -1)) {
            assertTrue(model.getName().startsWith(Constants.R_HEADS));
            assertTrue(model.equals(model));
@@ -160,6 +161,7 @@
    public void testTags() throws Exception {
        Repository repository = GitBlitSuite.getJGitRepository();
        assertTrue(JGitUtils.getTags(repository, true, 5).size() == 5);
        for (RefModel model : JGitUtils.getTags(repository, true, -1)) {
            if (model.getObjectId().getName().equals("d28091fb2977077471138fe97da1440e0e8ae0da")) {
                assertTrue("Not an annotated tag!", model.isAnnotatedTag());
@@ -276,6 +278,7 @@
    }
    public void testRevlog() throws Exception {
        assertTrue(JGitUtils.getRevLog(null, 0).size() == 0);
        List<RevCommit> commits = JGitUtils.getRevLog(null, 10);
        assertTrue(commits.size() == 0);
@@ -306,6 +309,7 @@
    }
    public void testSearchRevlogs() throws Exception {
        assertTrue(JGitUtils.searchRevlogs(null, null, "java", SearchType.COMMIT, 0, 0).size() == 0);
        List<RevCommit> results = JGitUtils.searchRevlogs(null, null, "java", SearchType.COMMIT, 0,
                3);
        assertTrue(results.size() == 0);
tests/com/gitblit/tests/StringUtilsTest.java
@@ -36,6 +36,12 @@
        String output = "this<br/>is<br/>a<br/>test<br/><br/>of<br/><br/>line<br/><br/>breaking";
        assertTrue(StringUtils.breakLinesForHtml(input).equals(output));
    }
    public void testEncodeUrl() throws Exception {
        String input = "test /";
        String output = "test%20%2F";
        assertTrue(StringUtils.encodeURL(input).equals(output));
    }
    public void testEscapeForHtml() throws Exception {
        String input = "& < > \" \t";
@@ -43,6 +49,12 @@
        String outputChange = "&amp;&nbsp;&lt;&nbsp;&gt;&nbsp;&quot;&nbsp; &nbsp; &nbsp;";
        assertTrue(StringUtils.escapeForHtml(input, false).equals(outputNoChange));
        assertTrue(StringUtils.escapeForHtml(input, true).equals(outputChange));
    }
    public void testDecodeForHtml() throws Exception {
        String input = "&amp; &lt; &gt; &quot;";
        String output = "& < > \"";
        assertTrue(StringUtils.decodeFromHtml(input).equals(output));
    }
    public void testFlattenStrings() throws Exception {
@@ -70,6 +82,11 @@
        assertTrue(StringUtils.getSHA1("blob 16\000what is up, doc?").equals(
                "bd9dbf5aae1a3862dd1526723246b20206e5fc37"));
    }
    public void testMD5() throws Exception {
        assertTrue(StringUtils.getMD5("blob 16\000what is up, doc?").equals(
                "77fb8d95331f0d557472f6776d3aedf6"));
    }
    public void testRootPath() throws Exception {
        String input = "/nested/path/to/repository";
@@ -77,7 +94,7 @@
        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);
tests/com/gitblit/tests/SyndicationUtilsTest.java
New file
@@ -0,0 +1,42 @@
/*
 * 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.io.ByteArrayOutputStream;
import java.util.List;
import junit.framework.TestCase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.SyndicationUtils;
public class SyndicationUtilsTest extends TestCase {
    public void testSyndication() throws Exception {
        Repository repository = GitBlitSuite.getHelloworldRepository();
        List<RevCommit> commits = JGitUtils.getRevLog(repository, 1);
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        SyndicationUtils.toRSS("http://localhost", "Title", "Description", "Repository", commits, os);
        String feed = os.toString();
        os.close();
        assertTrue(feed.length() > 100);
        assertTrue(feed.indexOf("<title>Title</title>") > -1);
        assertTrue(feed.indexOf("<description>Description</description>") > -1);
    }
}
tools/ant-googlecode-0.0.3.jar
Binary files differ