Rafael Cavazin
2013-01-27 06ae63123c94038b90153f4847de2c57c0193db8
updating current development

Merge remote-tracking branch 'upstream/master' into translation_to_pt-br
8 files renamed
96 files modified
41 files added
9779 ■■■■■ changed files
.classpath 16 ●●●● patch | view | raw | blame | history
.gitignore 3 ●●●● patch | view | raw | blame | history
build.xml 236 ●●●● patch | view | raw | blame | history
checkstyle.xml 1 ●●●● patch | view | raw | blame | history
distrib/authority.cmd 2 ●●● patch | view | raw | blame | history
distrib/gitblit 5 ●●●●● patch | view | raw | blame | history
distrib/gitblit-centos 5 ●●●●● patch | view | raw | blame | history
distrib/gitblit-stop.cmd 2 ●●● patch | view | raw | blame | history
distrib/gitblit-ubuntu 3 ●●●● patch | view | raw | blame | history
distrib/gitblit.cmd 2 ●●● patch | view | raw | blame | history
distrib/gitblit.properties 94 ●●●● patch | view | raw | blame | history
distrib/groovy/.gitignore patch | view | raw | blame | history
distrib/groovy/blockpush.groovy patch | view | raw | blame | history
distrib/groovy/fogbugz.groovy 167 ●●●●● patch | view | raw | blame | history
distrib/groovy/jenkins.groovy patch | view | raw | blame | history
distrib/groovy/localclone.groovy patch | view | raw | blame | history
distrib/groovy/protect-refs.groovy patch | view | raw | blame | history
distrib/groovy/sendmail-html.groovy patch | view | raw | blame | history
distrib/groovy/sendmail.groovy patch | view | raw | blame | history
distrib/groovy/thebuggenie.groovy patch | view | raw | blame | history
distrib/installService.cmd 4 ●●●● patch | view | raw | blame | history
distrib/projects.conf 3 ●●●●● patch | view | raw | blame | history
docs/01_features.mkd 10 ●●●● patch | view | raw | blame | history
docs/01_setup.mkd 147 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 246 ●●●●● patch | view | raw | blame | history
docs/05_roadmap.mkd 6 ●●●●● patch | view | raw | blame | history
gitblit.iml 12 ●●●● patch | view | raw | blame | history
resources/folder_star_16x16.png patch | view | raw | blame | history
resources/folder_star_32x32.png patch | view | raw | blame | history
resources/login_nl.mkd 3 ●●●●● patch | view | raw | blame | history
resources/login_zh_CN.mkd 3 ●●●●● patch | view | raw | blame | history
resources/star_16x16.png patch | view | raw | blame | history
resources/star_32x32.png patch | view | raw | blame | history
resources/welcome_nl.mkd 3 ●●●●● patch | view | raw | blame | history
resources/welcome_zh_CN.mkd 3 ●●●●● patch | view | raw | blame | history
src/WEB-INF/web.xml 24 ●●●●● patch | view | raw | blame | history
src/com/gitblit/ConfigUserService.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 22 ●●●● patch | view | raw | blame | history
src/com/gitblit/FederationClient.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/FileSettings.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 332 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlitServer.java 54 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitFilter.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/GitServlet.java 144 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitUserService.java 51 ●●●● patch | view | raw | blame | history
src/com/gitblit/LdapUserService.java 20 ●●●● patch | view | raw | blame | history
src/com/gitblit/PagesServlet.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/RedmineUserService.java 123 ●●●● patch | view | raw | blame | history
src/com/gitblit/RobotsTxtServlet.java 10 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/GitblitAuthority.java 38 ●●●● patch | view | raw | blame | history
src/com/gitblit/build/Build.java 126 ●●●●● patch | view | raw | blame | history
src/com/gitblit/build/BuildWebXml.java 64 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/EditRepositoryDialog.java 24 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClient.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/client/IndicatorsRenderer.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/JPalette.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/client/RepositoriesPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/RepositoriesTableModel.java 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/UsersPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/client/UsersTableModel.java 21 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutClient.java 413 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutConstants.java 36 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutNioService.java 332 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutService.java 563 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutServiceConnection.java 105 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutSocketService.java 234 ●●●●● patch | view | raw | blame | history
src/com/gitblit/fanout/FanoutStats.java 98 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/Activity.java 87 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/ProjectModel.java 2 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/PushLogEntry.java 208 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryCommit.java 112 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryModel.java 53 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 11 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/ActivityUtils.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/utils/ArrayUtils.java 30 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/FileUtils.java 33 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/IssueUtils.java 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 70 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/MetricUtils.java 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/PushLogUtils.java 344 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 14 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 4 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_es.properties 101 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_ko.properties 130 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_nl.properties 443 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties 442 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties 444 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 11 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ChangePasswordPage.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitDiffPage.java 13 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/CommitPage.java 13 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 20 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 14 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html 53 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html 53 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html 55 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ForksPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ProjectPage.java 37 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ProjectsPage.java 71 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RawPage.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoriesPage.java 67 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoryPage.java 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.html 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SummaryPage.java 36 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/TreePage.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/UserPage.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ActivityPanel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/HistoryPanel.java 102 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 26 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RefsPanel.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.html 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoriesPanel.java 53 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/TeamsPanel.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/UsersPanel.html 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/UsersPanel.java 4 ●●●● patch | view | raw | blame | history
test-gitblit.properties 6 ●●●● patch | view | raw | blame | history
test-ui-gitblit.properties 1203 ●●●●● patch | view | raw | blame | history
test-ui-users.conf 44 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FanoutServiceTest.java 172 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FederationTests.java 2 ●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitBlitSuite.java 8 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitBlitTest.java 2 ●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GitServletTest.java 89 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/GroovyScriptTest.java 23 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/LdapUserServiceTest.java 16 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/PermissionsTest.java 51 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/PushLogTest.java 37 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/RedmineUserServiceTest.java 28 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/RpcTests.java 2 ●●● patch | view | raw | blame | history
tests/de/akquinet/devops/GitblitRunnable.java 134 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/LaunchWithUITestConfig.java 126 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/ManualUITestLaunch.java 14 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/TestUISuite.java 33 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/cases/UI_MultiAdminSupportTest.java 93 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/generic/AbstractUITest.java 96 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/view/Exp.java 45 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/view/GitblitDashboardView.java 100 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/view/GitblitPageView.java 73 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/view/RepoEditView.java 158 ●●●●● patch | view | raw | blame | history
tests/de/akquinet/devops/test/ui/view/RepoListView.java 130 ●●●●● patch | view | raw | blame | history
.classpath
@@ -21,9 +21,9 @@
    <classpathentry kind="lib" path="ext/lucene-queries-3.6.1.jar" sourcepath="ext/src/lucene-queries-3.6.1-sources.jar" />
    <classpathentry kind="lib" path="ext/jakarta-regexp-1.4.jar" />
    <classpathentry kind="lib" path="ext/markdownpapers-core-1.3.2.jar" sourcepath="ext/src/markdownpapers-core-1.3.2-sources.jar" />
    <classpathentry kind="lib" path="ext/org.eclipse.jgit-2.1.0.201209190230-r.jar" sourcepath="ext/src/org.eclipse.jgit-2.1.0.201209190230-r-sources.jar" />
    <classpathentry kind="lib" path="ext/org.eclipse.jgit-2.2.0.201212191850-r.jar" sourcepath="ext/src/org.eclipse.jgit-2.2.0.201212191850-r-sources.jar" />
    <classpathentry kind="lib" path="ext/jsch-0.1.44-1.jar" sourcepath="ext/src/jsch-0.1.44-1-sources.jar" />
    <classpathentry kind="lib" path="ext/org.eclipse.jgit.http.server-2.1.0.201209190230-r.jar" sourcepath="ext/src/org.eclipse.jgit.http.server-2.1.0.201209190230-r-sources.jar" />
    <classpathentry kind="lib" path="ext/org.eclipse.jgit.http.server-2.2.0.201212191850-r.jar" sourcepath="ext/src/org.eclipse.jgit.http.server-2.2.0.201212191850-r-sources.jar" />
    <classpathentry kind="lib" path="ext/bcprov-jdk15on-1.47.jar" sourcepath="ext/src/bcprov-jdk15on-1.47-sources.jar" />
    <classpathentry kind="lib" path="ext/bcmail-jdk15on-1.47.jar" sourcepath="ext/src/bcmail-jdk15on-1.47-sources.jar" />
    <classpathentry kind="lib" path="ext/bcpkix-jdk15on-1.47.jar" sourcepath="ext/src/bcpkix-jdk15on-1.47-sources.jar" />
@@ -38,6 +38,18 @@
    <classpathentry kind="lib" path="ext/xz-1.0.jar" sourcepath="ext/src/xz-1.0-sources.jar" />
    <classpathentry kind="lib" path="ext/junit-4.10.jar" sourcepath="ext/src/junit-4.10-sources.jar" />
    <classpathentry kind="lib" path="ext/hamcrest-core-1.1.jar" />
    <classpathentry kind="lib" path="ext/seleniumhq/selenium-java-2.28.0.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/selenium-api-2.28.0.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/selenium-remote-driver-2.28.0.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/selenium-support-2.28.0.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/guava-12.0.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/json-20080701.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/commons-exec-1.1.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/httpcore-4.2.1.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/httpmime-4.2.1.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/httpclient-4.2.1.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/commons-logging-1.1.1.jar"/>
    <classpathentry kind="lib" path="ext/seleniumhq/selenium-firefox-driver-2.28.0.jar"/>
    <classpathentry kind="output" path="bin/classes" />
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" />
</classpath>
.gitignore
@@ -31,4 +31,5 @@
/deploy
/*.jks
/x509test
/certs
/certs
/data
build.xml
@@ -12,10 +12,11 @@
    <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.deploy.dir" value="${basedir}/deploy" />
    <property name="project.deploy.dir" value="${basedir}/deploy" />
    <property name="project.war.dir" value="${basedir}/war" />
    <property name="project.jar.dir" value="${basedir}/jar" />
    <property name="project.site.dir" value="${basedir}/site" />
    <property name="project.site.dir" value="${basedir}/target/site" />
    <property name="project.target.dir" value="${basedir}/target" />
    <property name="project.resources.dir" value="${basedir}/resources" />    
    <property name="project.express.dir" value="${basedir}/express" />
    <property name="project.maven.repo.url" value="enter here your Maven repo URL" />
@@ -106,22 +107,66 @@
    -->
    <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">
        <!-- cleanup old builds -->
        <delete dir="${project.target.dir}" />
        <mkdir dir="${project.target.dir}" />
        <!-- cleanup old builds -->
        <delete>
            <fileset dir="${basedir}">
                <include name="*.zip" />
                <include name="*.war" />
                <include name="*.jar" />
            </fileset>
        </delete>
        <!-- copy required distribution files to data folder -->
        <mkdir dir="${basedir}/data" />
        <copy todir="${basedir}/data" overwrite="false">
            <fileset dir="${basedir}/distrib">
                <include name="gitblit.properties" />
                <include name="users.conf" />
                <include name="projects.conf" />
            </fileset>
        </copy>
        
        <!-- copy required distribution files to project folder -->
        <mkdir dir="${basedir}/certs" />
        <copy todir="${basedir}/certs" overwrite="false">
        <mkdir dir="${basedir}/data/certs" />
        <copy todir="${basedir}/data/certs" overwrite="false">
            <fileset dir="${basedir}/distrib">
                <include name="authority.conf" />
                <include name="*.tmpl" />
            </fileset>
        </copy>
        <!-- copy required distribution files to project folder -->
        <mkdir dir="${basedir}/data/groovy" />
        <copy todir="${basedir}/data/groovy" overwrite="false">
            <fileset dir="${basedir}/distrib/groovy" />
        </copy>
        <!-- upgrade existing workspace to data folder -->
        <move todir="${basedir}/data" overwrite="true" failonerror="false">
            <fileset dir="${basedir}">
                <include name="users.conf" />
                <include name="projects.conf" />
                <include name="gitblit.properties" />
                <include name="serverKeyStore.jks" />
                <include name="serverTrustStore.jks" />
            </fileset>
        </move>
        <move todir="${basedir}/data/certs" overwrite="true" failonerror="false">
            <fileset dir="${basedir}/certs" />
        </move>
        <move todir="${basedir}/data/git" overwrite="true" failonerror="false">
            <fileset dir="${basedir}/git" />
        </move>
        <move todir="${basedir}/data/proposals" overwrite="true" failonerror="false">
            <fileset dir="${basedir}/proposals" />
        </move>
        <delete dir="${basedir}/javadoc" failonerror="false" />
        <delete dir="${basedir}/site" failonerror="false" />
        <delete dir="${basedir}/temp" failonerror="false" />
        <!-- copy gitblit.properties to the WEB-INF folder.
             this file is only used for parsing setting descriptions. -->
@@ -186,18 +231,47 @@
                <exclude name="federation.properties" />
                <exclude name="openshift.mkd" />
                <exclude name="authority.conf" />
                <exclude name="users.conf" />
                <exclude name="projects.conf" />
                <exclude name="gitblit.properties" />
                <exclude name="*.tmpl" />
                <exclude name="groovy/**" />
            </fileset>
            <fileset dir="${basedir}">
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>            
        </copy>
        <copy tofile="${project.deploy.dir}/authority.jar" file="${basedir}/authority-${gb.version}.jar" />
        
        <!-- Copy the supported Groovy hook scripts -->
        <mkdir dir="${project.deploy.dir}/data/groovy" />
        <copy todir="${project.deploy.dir}/data/groovy">
            <fileset dir="${basedir}/distrib/groovy">
                <include name="sendmail.groovy" />
                <include name="sendmail-html.groovy" />
                <include name="jenkins.groovy" />
                <include name="protect-refs.groovy" />
                <include name="fogbugz.groovy" />
                <include name="thebuggenie.groovy" />
            </fileset>
        </copy>
        <copy tofile="${project.deploy.dir}/authority.jar" file="${project.target.dir}/authority-${gb.version}.jar" />
        <!-- Prepare the data folder -->
        <mkdir dir="${project.deploy.dir}/data"/>
        <copy todir="${project.deploy.dir}/data">
            <fileset dir="${basedir}/distrib">
                <include name="users.conf" />
                <include name="projects.conf" />
                <include name="gitblit.properties" />
            </fileset>
        </copy>
        <!-- Certificate templates -->
        <mkdir dir="${project.deploy.dir}/certs"/>
        <copy todir="${project.deploy.dir}/certs">
        <mkdir dir="${project.deploy.dir}/data/certs"/>
        <mkdir dir="${project.deploy.dir}/data/certs"/>
        <copy todir="${project.deploy.dir}/data/certs">
            <fileset dir="${basedir}/distrib">
                <include name="*.tmpl" />
                <include name="authority.conf" />
@@ -234,20 +308,8 @@
            <param name="docs.output.dir" value="${project.deploy.dir}/docs" />
        </antcall>
        
        <!-- Copy the supported Groovy hook scripts -->
        <mkdir dir="${project.deploy.dir}/groovy" />
        <copy todir="${project.deploy.dir}/groovy">
            <fileset dir="${basedir}/groovy">
                <include name="sendmail.groovy" />
                <include name="sendmail-html.groovy" />
                <include name="jenkins.groovy" />
                <include name="protect-refs.groovy" />
                <include name="localclone.groovy" />
            </fileset>
        </copy>
        <!-- Create Zip deployment -->        
        <zip destfile="${distribution.zipfile}">
        <zip destfile="${project.target.dir}/${distribution.zipfile}">
            <fileset dir="${project.deploy.dir}">
                <include name="**/*" />
            </fileset>
@@ -383,9 +445,6 @@
        <!-- Copy web.xml and users.conf to WEB-INF -->
        <copy todir="${project.war.dir}/WEB-INF">
            <fileset dir="${basedir}/distrib">
                 <include name="users.conf" />
            </fileset>
            <fileset dir="${basedir}/src/WEB-INF">
                 <include name="web.xml" />
            </fileset>
@@ -404,19 +463,30 @@
            <param name="docs.output.dir" value="${project.war.dir}/WEB-INF/docs" />
        </antcall>
        <!-- Copy users.conf to WEB-INF/data -->
        <mkdir dir="${project.war.dir}/WEB-INF/data" />
        <copy todir="${project.war.dir}/WEB-INF/data">
            <fileset dir="${basedir}/distrib">
                 <include name="users.conf" />
                <include name="projects.conf" />
                 <include name="gitblit.properties" />
            </fileset>
        </copy>
        <!-- Copy the supported Groovy hook scripts -->
        <mkdir dir="${project.war.dir}/WEB-INF/groovy" />
        <copy todir="${project.war.dir}/WEB-INF/groovy">
            <fileset dir="${basedir}/groovy">
        <mkdir dir="${project.war.dir}/WEB-INF/data/groovy" />
        <copy todir="${project.war.dir}/WEB-INF/data/groovy">
            <fileset dir="${basedir}/distrib/groovy">
                <include name="sendmail.groovy" />
                <include name="sendmail-html.groovy" />
                <include name="jenkins.groovy" />
                <include name="protect-refs.groovy" />
                <include name="localclone.groovy" />
                <include name="fogbugz.groovy" />
                <include name="thebuggenie.groovy" />
            </fileset>
        </copy>
        <!-- Build the WAR web.xml from the prototype web.xml and gitblit.properties -->
        <!-- Build the WAR web.xml from the prototype web.xml -->
        <java classpath="${project.build.dir}" classname="com.gitblit.build.BuildWebXml">
            <classpath refid="master-classpath" />
            
@@ -426,8 +496,6 @@
            <arg value="--destinationFile" />
            <arg value="${project.war.dir}/WEB-INF/web.xml" />
            
            <arg value="--propertiesFile" />
            <arg value="${basedir}/distrib/gitblit.properties" />
        </java>
        <!-- Gitblit resources -->
@@ -467,7 +535,7 @@
        </copy>
        <!-- Build the WAR file -->
        <jar basedir="${project.war.dir}" destfile="${distribution.warfile}" compress="true" />
        <jar basedir="${project.war.dir}" destfile="${project.target.dir}/${distribution.warfile}" compress="true" />
    </target>
    
@@ -554,7 +622,7 @@
    <target name="buildFederationClient" depends="compile" description="Builds the stand-alone Gitblit federation client">
        <echo>Building Gitblit Federation Client ${gb.version}</echo>
    
        <genjar jarfile="fedclient.jar">
        <genjar jarfile="${project.target.dir}/fedclient.jar">
            <class name="com.gitblit.FederationClientLauncher" />
            <resource file="${project.build.dir}/log4j.properties" />
            <classfilter>
@@ -575,16 +643,21 @@
        </genjar>
        
        <!-- Build the federation client zip file -->
        <zip destfile="${fedclient.zipfile}">
        <zip destfile="${project.target.dir}/${fedclient.zipfile}">
            <fileset dir="${basedir}">
                <include name="fedclient.jar" />
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <fileset dir="${project.target.dir}">
                <include name="fedclient.jar" />
            </fileset>
            <fileset dir="${basedir}/distrib">
                <include name="federation.properties" />
            </fileset>
        </zip>
        <!-- Cleanup -->
        <delete file="${project.target.dir}/fedclient.jar" />
    </target>
@@ -619,14 +692,26 @@
        <copy tofile="${deployments.root}/WEB-INF/reference.properties" 
            file="${basedir}/distrib/gitblit.properties"/>
        <!-- Copy users.conf and gitblit.properties -->
        <mkdir dir="${deployments.root}/WEB-INF/data" />
        <copy todir="${deployments.root}/WEB-INF/data">
            <fileset dir="${basedir}/distrib">
                <include name="users.conf" />
                <include name="projects.conf" />
                <include name="gitblit.properties" />
            </fileset>
        </copy>
        <!-- Copy the supported Groovy hook scripts -->
        <mkdir dir="${deployments.root}/WEB-INF/groovy" />
        <copy todir="${deployments.root}/WEB-INF/groovy">
            <fileset dir="${basedir}/groovy">
        <mkdir dir="${deployments.root}/WEB-INF/data/groovy" />
        <copy todir="${deployments.root}/WEB-INF/data/groovy">
            <fileset dir="${basedir}/distrib/groovy">
                <include name="sendmail.groovy" />
                <include name="sendmail-html.groovy" />
                <include name="jenkins.groovy" />
                <include name="protect-refs.groovy" />
                <include name="fogbugz.groovy" />
                <include name="thebuggenie.groovy" />
            </fileset>
        </copy>
                    
@@ -663,6 +748,8 @@
                <exclude name="hamcrest*.jar" />
                <exclude name="servlet*.jar" />
                <exclude name="javax.servlet*.jar" />
                <exclude name="jsslutils*.jar" />
                <exclude name="jcalendar*.jar" />
            </fileset>
        </copy>
@@ -680,7 +767,7 @@
        </jar>
        <!-- Build Express Zip file -->
        <zip destfile="${express.zipfile}">
        <zip destfile="${project.target.dir}/${express.zipfile}">
            <fileset dir="${project.express.dir}" />
        </zip>
@@ -695,7 +782,7 @@
    <target name="buildManager" depends="compile" description="Builds the stand-alone Gitblit Manager">
        <echo>Building Gitblit Manager ${gb.version}</echo>
        <genjar jarfile="manager-${gb.version}.jar">
        <genjar jarfile="${project.target.dir}/manager-${gb.version}.jar">
            <resource file="${basedir}/src/com/gitblit/client/splash.png" />
            <resource file="${basedir}/resources/gitblt-favicon.png" />
            <resource file="${basedir}/resources/gitweb-favicon.png" />
@@ -717,12 +804,15 @@
            <resource file="${basedir}/resources/commit_changes_16x16.png" />
            <resource file="${basedir}/resources/commit_merge_16x16.png" />
            <resource file="${basedir}/resources/commit_divide_16x16.png" />
            <resource file="${basedir}/resources/star_16x16.png" />
            <resource file="${basedir}/resources/blank.png" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_es.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_ja.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_ko.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_nl.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_pl.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties" />
            <class name="com.gitblit.client.GitblitManagerLauncher" />
            <classfilter>
@@ -744,13 +834,18 @@
        </genjar>
        <!-- Build Manager Zip file -->
        <zip destfile="${manager.zipfile}">
        <zip destfile="${project.target.dir}/${manager.zipfile}">
            <fileset dir="${basedir}">
                <include name="manager-${gb.version}.jar" />
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <fileset dir="${project.target.dir}">
                <include name="manager-${gb.version}.jar" />
            </fileset>
        </zip>
        <!-- Cleanup -->
        <delete file="${project.target.dir}/manager-${gb.version}.jar" />
    </target>
    
    
@@ -762,7 +857,7 @@
    <target name="buildAuthority" depends="compile" description="Builds the stand-alone Gitblit Authority">
        <echo>Building Gitblit Authority ${gb.version}</echo>
        <genjar jarfile="authority-${gb.version}.jar">
        <genjar jarfile="${project.target.dir}/authority-${gb.version}.jar">
            <resource file="${basedir}/src/com/gitblit/client/splash.png" />
            <resource file="${basedir}/resources/gitblt-favicon.png" />
            <resource file="${basedir}/resources/user_16x16.png" />
@@ -809,13 +904,15 @@
        </genjar>
        <!-- Build Authority Zip file -->
        <zip destfile="${authority.zipfile}">
        <zip destfile="${project.target.dir}/${authority.zipfile}">
            <fileset dir="${basedir}">
                <include name="authority-${gb.version}.jar" />
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <zipfileset dir="${basedir}/distrib" prefix="certs">
            <fileset dir="${project.target.dir}">
                <include name="authority-${gb.version}.jar" />
            </fileset>
            <zipfileset dir="${basedir}/distrib" prefix="data/certs">
                <include name="authority.conf" />
                <include name="mail.tmpl" />
                <include name="instructions.tmpl" />
@@ -832,9 +929,12 @@
        <echo>Building Gitblit API Library ${gb.version}</echo>
    
        <!-- Build API Library jar -->
        <genjar jarfile="gbapi-${gb.version}.jar">
        <genjar jarfile="${project.target.dir}/gbapi-${gb.version}.jar">
            <class name="com.gitblit.Keys" />
            <class name="com.gitblit.client.GitblitClient" />
            <class name="com.gitblit.models.FederationModel" />
            <class name="com.gitblit.models.FederationProposal" />
            <class name="com.gitblit.models.FederationSet" />
            <classpath refid="master-classpath" />
            <classfilter>
                <exclude name="com.google.gson." />
@@ -847,7 +947,7 @@
        </genjar>
        
        <!-- Build API sources jar -->
        <zip destfile="gbapi-${gb.version}-sources.jar">
        <zip destfile="${project.target.dir}/gbapi-${gb.version}-sources.jar">
            <fileset dir="${basedir}/src" defaultexcludes="yes">
                <include name="com/gitblit/Constants.java"/>
                <include name="com/gitblit/GitBlitException.java"/>
@@ -859,7 +959,7 @@
        </zip>
        
        <!-- Build API JavaDoc jar -->
        <javadoc destdir="${basedir}/javadoc">
        <javadoc destdir="${project.target.dir}/javadoc">
            <fileset dir="${basedir}/src" defaultexcludes="yes">
                <include name="com/gitblit/Constants.java"/>
                <include name="com/gitblit/GitBlitException.java"/>
@@ -869,18 +969,20 @@
                  <include name="com/gitblit/utils/**/*.java"/>                      
            </fileset>
        </javadoc>
        <zip destfile="gbapi-${gb.version}-javadoc.jar">
            <fileset dir="${basedir}/javadoc" />
        <zip destfile="${project.target.dir}/gbapi-${gb.version}-javadoc.jar">
            <fileset dir="${project.target.dir}/javadoc" />
        </zip>
        
        <!-- Build the API library zip file -->
        <zip destfile="${gbapi.zipfile}">
        <zip destfile="${project.target.dir}/${gbapi.zipfile}">
            <fileset dir="${basedir}">
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <fileset dir="${project.target.dir}">
                <include name="gbapi-${gb.version}.jar" />
                <include name="gbapi-${gb.version}-sources.jar" />
                <include name="gbapi-${gb.version}-javadoc.jar" />
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <fileset dir="${basedir}/ext">
                <exclude name="src/**" />
@@ -889,6 +991,16 @@
                <include name="jdom*.jar" />
            </fileset>
        </zip>
        <!-- Cleanup -->
        <delete>
            <fileset dir="${project.target.dir}">
                <include name="javadoc/**" />
                <include name="gbapi-${gb.version}.jar" />
                <include name="gbapi-${gb.version}-sources.jar" />
                <include name="gbapi-${gb.version}-javadoc.jar" />
        </fileset>
        </delete>
    </target>
        
        
@@ -1065,7 +1177,7 @@
        <java classpath="${project.build.dir}" classname="com.gitblit.build.BuildGhPages">
            <classpath refid="master-classpath" />
            <arg value="--sourceFolder" />
            <arg value="${basedir}/site" />
            <arg value="${basedir}/target/site" />
            <arg value="--repository" />
            <arg value="${basedir}" />
@@ -1089,7 +1201,7 @@
             username="${googlecode.user}" 
             password="${googlecode.password}" 
             projectname="gitblit" 
             filename="${distribution.zipfile}"
             filename="${project.target.dir}/${distribution.zipfile}"
             targetfilename="gitblit-${gb.version}.zip"
             summary="Gitblit GO v${gb.version} (standalone, integrated Gitblit server)"
             labels="Featured, Type-Package, OpSys-All" />
@@ -1099,7 +1211,7 @@
             username="${googlecode.user}" 
             password="${googlecode.password}" 
             projectname="gitblit" 
             filename="${distribution.warfile}"
             filename="${project.target.dir}/${distribution.warfile}"
             targetfilename="gitblit-${gb.version}.war"
             summary="Gitblit WAR v${gb.version} (standard WAR webapp for servlet containers)"
             labels="Featured, Type-Package, OpSys-All" />
@@ -1109,7 +1221,7 @@
            username="${googlecode.user}" 
            password="${googlecode.password}" 
            projectname="gitblit" 
            filename="${fedclient.zipfile}"
            filename="${project.target.dir}/${fedclient.zipfile}"
            targetfilename="fedclient-${gb.version}.zip"
            summary="Gitblit Federation Client v${gb.version} (command-line tool to clone data from federated Gitblit instances)"
            labels="Featured, Type-Package, OpSys-All" />
@@ -1119,7 +1231,7 @@
            username="${googlecode.user}" 
            password="${googlecode.password}" 
            projectname="gitblit" 
            filename="${manager.zipfile}"
            filename="${project.target.dir}/${manager.zipfile}"
            targetfilename="manager-${gb.version}.zip"
            summary="Gitblit Manager v${gb.version} (Swing tool to remotely administer a Gitblit server)"
            labels="Featured, Type-Package, OpSys-All" />
@@ -1129,7 +1241,7 @@
            username="${googlecode.user}" 
            password="${googlecode.password}" 
            projectname="gitblit" 
            filename="${gbapi.zipfile}"
            filename="${project.target.dir}/${gbapi.zipfile}"
            targetfilename="gbapi-${gb.version}.zip"
            summary="Gitblit API Library v${gb.version} (JSON RPC library to integrate with your software)"
            labels="Featured, Type-Package, OpSys-All" />
@@ -1139,7 +1251,7 @@
            username="${googlecode.user}" 
            password="${googlecode.password}" 
            projectname="gitblit" 
            filename="${express.zipfile}"
            filename="${project.target.dir}/${express.zipfile}"
            targetfilename="express-${gb.version}.zip"
            summary="Gitblit Express v${gb.version} (run Gitblit on RedHat's OpenShift cloud)"
            labels="Featured, Type-Package, OpSys-All" />
checkstyle.xml
@@ -53,7 +53,6 @@
        </module>
        <module name="NeedBraces" />
        <module name="RightCurly" />
        <module name="DoubleCheckedLocking" />
        <module name="EmptyStatement" />
        <module name="EqualsHashCode" />
        <module name="IllegalInstantiation" />
distrib/authority.cmd
@@ -1 +1 @@
@java -jar authority.jar
@java -jar authority.jar --baseFolder data
distrib/gitblit
@@ -3,6 +3,7 @@
set -e
GITBLIT_PATH=/opt/gitblit
GITBLIT_BASE_FOLDER=/opt/gitblit/data
GITBLIT_HTTP_PORT=0
GITBLIT_HTTPS_PORT=8443
source ${GITBLIT_PATH}/java-proxy-config.sh
@@ -14,13 +15,13 @@
  start)
        log_action_begin_msg "Starting gitblit server"
        cd $GITBLIT_PATH
        $JAVA $GITBLIT_PATH/gitblit.jar --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT > /dev/null &
        $JAVA $GITBLIT_PATH/gitblit.jar --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT --baseFolder $GITBLIT_BASE_FOLDER > /dev/null &
        log_action_end_msg $?
        ;;
  stop)
        log_action_begin_msg "Stopping gitblit server"
        cd $GITBLIT_PATH
        $JAVA $GITBLIT_PATH/gitblit.jar --stop > /dev/null &
        $JAVA $GITBLIT_PATH/gitblit.jar --baseFolder $GITBLIT_BASE_FOLDER --stop > /dev/null &
        log_action_end_msg $?
        ;;
  force-reload|restart)
distrib/gitblit-centos
@@ -6,6 +6,7 @@
# change theses values (default values)
GITBLIT_PATH=/opt/gitblit
GITBLIT_BASE_FOLDER=/opt/gitblit/data
GITBLIT_HTTP_PORT=0
GITBLIT_HTTPS_PORT=8443
source ${GITBLIT_PATH}/java-proxy-config.sh
@@ -19,7 +20,7 @@
      then
      echo $"Starting gitblit server"
      cd $GITBLIT_PATH
      $JAVA $GITBLIT_PATH/gitblit.jar --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT > /dev/null &
      $JAVA $GITBLIT_PATH/gitblit.jar --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT --baseFolder $GITBLIT_BASE_FOLDER > /dev/null &
      echo "."
      exit $RETVAL
    fi
@@ -30,7 +31,7 @@
      then
      echo $"Stopping gitblit server"
      cd $GITBLIT_PATH
      $JAVA $GITBLIT_PATH/gitblit.jar --stop > /dev/null &
      $JAVA $GITBLIT_PATH/gitblit.jar --baseFolder $GITBLIT_BASE_FOLDER --stop > /dev/null &
      echo "."
      exit $RETVAL
    fi
distrib/gitblit-stop.cmd
@@ -1 +1 @@
@java -jar gitblit.jar --stop
@java -jar gitblit.jar --stop --baseFolder data
distrib/gitblit-ubuntu
@@ -8,9 +8,10 @@
# change theses values (default values)
GITBLIT_PATH=/opt/gitblit
GITBLIT_BASE_FOLDER=/opt/gitblit/data
GITBLIT_USER="gitblit"
source ${GITBLIT_PATH}/java-proxy-config.sh
ARGS="-server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -jar gitblit.jar"
ARGS="-server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -jar gitblit.jar --baseFolder $GITBLIT_BASE_FOLDER"
RETVAL=0
distrib/gitblit.cmd
@@ -1 +1 @@
@java -jar gitblit.jar
@java -jar gitblit.jar --baseFolder data
distrib/gitblit.properties
@@ -1,4 +1,19 @@
#
# Gitblit Settings
#
# This settings file supports parameterization from the command-line for the
# following command-line parameters:
#
#   --baseFolder    ${baseFolder}    SINCE 1.2.1
#
# Settings that support ${baseFolder} parameter substitution are indicated with the
# BASEFOLDER attribute.  If the --baseFolder argument is unspecified, ${baseFolder}
# and it's trailing / will be discarded from the setting value leaving a relative
# path that is equivalent to pre-1.2.1 releases.
#
# e.g. "${baseFolder}/git" becomes "git", if --baseFolder is unspecified
#
# Git Servlet Settings
#
@@ -10,7 +25,8 @@
#
# SINCE 0.5.0
# RESTART REQUIRED
git.repositoriesFolder = git
# BASEFOLDER
git.repositoriesFolder = ${baseFolder}/git
# Build the available repository list at startup and cache this list for reuse.
# This reduces disk io when presenting the repositories page, responding to rpcs,
@@ -299,14 +315,16 @@
#
# RESTART REQUIRED
# SINCE 0.8.0
groovy.scriptsFolder = groovy
# BASEFOLDER
groovy.scriptsFolder = ${baseFolder}/groovy
# Specify the directory Grape uses for downloading libraries.
# http://groovy.codehaus.org/Grape
#
# RESTART REQUIRED
# SINCE 1.0.0
groovy.grapeFolder = groovy/grape
# BASEFOLDER
groovy.grapeFolder = ${baseFolder}/groovy/grape
# Scripts to execute on Pre-Receive.
#
@@ -366,6 +384,53 @@
groovy.customFields = 
#
# Fanout Settings
#
# Fanout is a PubSub notification service that can be used by Sparkleshare
# to eliminate repository change polling.  The fanout service runs in a separate
# thread on a separate port from the Gitblit http/https application.
# This service is provided so that Sparkleshare may be used with Gitblit in
# firewalled environments or where reliance on Sparkleshare's default notifications
# server (notifications.sparkleshare.org) is unwanted.
#
# This service maintains an open socket connection from the client to the
# Fanout PubSub service. This service may not work properly behind a proxy server.
# Specify the interface for Fanout to bind it's service.
# You may specify an ip or an empty value to bind to all interfaces.
# Specifying localhost will result in Gitblit ONLY listening to requests to
# localhost.
#
# SINCE 1.2.1
# RESTART REQUIRED
fanout.bindInterface = localhost
# port for serving the Fanout PubSub service.  <= 0 disables this service.
# On Unix/Linux systems, ports < 1024 require root permissions.
# Recommended value: 17000
#
# SINCE 1.2.1
# RESTART REQUIRED
fanout.port = 0
# Use Fanout NIO service.  If false, a multi-threaded socket service will be used.
# Be advised, the socket implementation spawns a thread per connection plus the
# connection acceptor thread.  The NIO implementation is completely single-threaded.
#
# SINCE 1.2.1
# RESTART REQUIRED
fanout.useNio = true
# Concurrent connection limit.  <= 0 disables concurrent connection throttling.
# If > 0, only the specified number of concurrent connections will be allowed
# and all other connections will be rejected.
#
# SINCE 1.2.1
# RESTART REQUIRED
fanout.connectionLimit = 0
#
# Authentication Settings
#
@@ -390,7 +455,8 @@
# Config file for storing project metadata
#
# SINCE 1.2.0
web.projectsFile = projects.conf
# BASEFOLDER
web.projectsFile = ${baseFolder}/projects.conf
# Either the full path to a user config file (users.conf)
# OR the full path to a simple user properties file (users.properties)
@@ -404,7 +470,8 @@
#
# SINCE 0.5.0
# RESTART REQUIRED
realm.userService = users.conf
# BASEFOLDER
realm.userService = ${baseFolder}/users.conf
# How to store passwords.
# Valid values are plain, md5, or combined-md5.  md5 is the hash of password.
@@ -463,7 +530,8 @@
# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html
#
# SINCE 1.0.0
web.robots.txt =
# BASEFOLDER
web.robots.txt = ${baseFolder}/robots.txt
# If true, the web ui layout will respond and adapt to the browser's dimensions.
# if false, the web ui will use a 940px fixed-width layout.
@@ -562,6 +630,7 @@
# Specifying "gitblit" uses the internal login message.
#
# SINCE 0.7.0
# BASEFOLDER
web.loginMessage = gitblit
# This is the message displayed above the repositories table.
@@ -569,6 +638,7 @@
# Specifying "gitblit" uses the internal welcome message.
#
# SINCE 0.5.0
# BASEFOLDER
web.repositoriesMessage = gitblit
# Ordered list of charsets/encodings to use when trying to display a blob.
@@ -878,7 +948,8 @@
# Use forward slashes even on Windows!!
#
# SINCE 0.6.0
federation.proposalsFolder = proposals
# BASEFOLDER
federation.proposalsFolder = ${baseFolder}/proposals
# The default pull frequency if frequency is unspecified on a registration
#
@@ -980,7 +1051,8 @@
#
# SINCE 1.0.0
# RESTART REQUIRED
realm.ldap.backingUserService = users.conf
# BASEFOLDER
realm.ldap.backingUserService = ${baseFolder}/users.conf
# Delegate team membership control to LDAP.
#
@@ -1076,7 +1148,8 @@
# default: users.conf
#
# RESTART REQUIRED
realm.redmine.backingUserService = users.conf
# BASEFOLDER
realm.redmine.backingUserService = ${baseFolder}/users.conf
# URL of the Redmine.
realm.redmine.url = http://example.com/redmine
@@ -1089,7 +1162,8 @@
#
# SINCE 0.5.0
# RESTART REQUIRED
server.tempFolder = temp
# BASEFOLDER
server.tempFolder = ${baseFolder}/temp
# Use Jetty NIO connectors.  If false, Jetty Socket connectors will be used.
#
distrib/groovy/.gitignore
distrib/groovy/blockpush.groovy
distrib/groovy/fogbugz.groovy
New file
@@ -0,0 +1,167 @@
import org.eclipse.jgit.revwalk.RevCommit;
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.gitblit.GitBlit
import com.gitblit.Keys
import com.gitblit.models.RepositoryModel
import com.gitblit.models.TeamModel
import com.gitblit.models.UserModel
import com.gitblit.utils.JGitUtils
import com.sun.org.apache.xalan.internal.xsltc.compiler.Import;
import java.text.SimpleDateFormat
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.Config
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.transport.ReceiveCommand
import org.eclipse.jgit.transport.ReceiveCommand.Result
import org.slf4j.Logger
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import java.util.Set;
import java.util.HashSet;
/**
 * Sample Gitblit Post-Receive Hook: fogbugz
 *
 * The purpose of this script is to invoke the Fogbugz API and update a case when
 * push is received based.
 *
 * Example URL - http://bugs.yourdomain.com/fogbugz/cvsSubmit.asp?ixBug=bugID&sFile=file&sPrev=x&sNew=y&ixRepository=206
 *
 * The Post-Receive hook is executed AFTER the pushed commits have been applied
 * to the Git repository.  This is the appropriate point to trigger an
 * integration build or to send a notification.
 *
 * This script is only executed when pushing to *Gitblit*, not to other Git
 * tooling you may be using.
 *
 * If this script is specified in *groovy.postReceiveScripts* of gitblit.properties
 * or web.xml then it will be executed by any repository when it receives a
 * push.  If you choose to share your script then you may have to consider
 * tailoring control-flow based on repository access restrictions.
 *
 * Scripts may also be specified per-repository in the repository settings page.
 * Shared scripts will be excluded from this list of available scripts.
 *
 * This script is dynamically reloaded and it is executed within it's own
 * exception handler so it will not crash another script nor crash Gitblit.
 *
 * If you want this hook script to fail and abort all subsequent scripts in the
 * chain, "return false" at the appropriate failure points.
 *
 * Bound Variables:
 *  gitblit            Gitblit Server                 com.gitblit.GitBlit
 *  repository        Gitblit Repository            com.gitblit.models.RepositoryModel
 *  receivePack        JGit Receive Pack            org.eclipse.jgit.transport.ReceivePack
 *  user            Gitblit User                com.gitblit.models.UserModel
 *  commands        JGit commands                 Collection<org.eclipse.jgit.transport.ReceiveCommand>
 *    url                Base url for Gitblit        String
 *  logger            Logs messages to Gitblit     org.slf4j.Logger
 *  clientLogger    Logs messages to Git client    com.gitblit.utils.ClientLogger
 *
 * Accessing Gitblit Custom Fields:
 *   def myCustomField = repository.customFields.myCustomField
 *
 * Cusom Fileds Used by This script
 *   fogbugzUrl - base URL to Fogbugz (ie. https://bugs.yourdomain.com/fogbugz/)
 *   fogbugzRepositoryId - (ixRepository value from Fogbugz Source Control configuration screen)
 *   fogbugzCommitMessageRegex - regex pattern used to match on bug id
 */
// Indicate we have started the script
logger.info("fogbugz hook triggered by ${user.username} for ${repository.name}")
/*
 * Primitive email notification.
 * This requires the mail settings to be properly configured in Gitblit.
 */
Repository r = gitblit.getRepository(repository.name)
// pull custom fields from repository specific values
// groovy.customFields = "fogbugzUrl=Fogbugz Base URL" "fogbugzRepositoryId=Fogbugz Repository ID" "fogbugzCommitMessageRegex="Fogbugz Commit Message Regular Expression"
def fogbugzUrl = repository.customFields.fogbugzUrl
def fogbugzRepositoryId = repository.customFields.fogbugzRepositoryId
def bugIdRegex = repository.customFields.fogbugzCommitMessageRegex
for (command in commands) {
    for( commit in JGitUtils.getRevLog(r, command.oldId.name, command.newId.name).reverse() ) {
        // Example URL - http://bugs.salsalabs.com/fogbugz/cvsSubmit.asp?ixBug=bugID&sFile=file&sPrev=x&sNew=y&ixRepository=206
        def bugIds = [];
        // Grab the second matcher and then filter out each numeric ID and add it to array
        (commit.getFullMessage() =~ bugIdRegex).each{ (it[1] =~ "\\d+").each {bugIds.add(it)} }
        for( file in getFiles(r, commit) ) {
            for( bugId in bugIds ) {
                def url = "${fogbugzUrl}/cvsSubmit.asp?ixBug=${bugId}&sFile=${file}&sPrev=${command.oldId.name}&sNew=${command.newId.name}&ixRepository=${fogbugzRepositoryId}"
                logger.info( url );
                // Hit the page and make sure we get an "OK" response
                def responseString = new URL(url).getText()
                if( !"OK".equals(responseString) ) {
                    throw new Exception( "Problem posting ${url} - ${responseString}" );
                }
            }
        }
    }
}
// close the repository reference
r.close()
/**
 * For a given commit, find all files part of it.
 */
def Set<String> getFiles(Repository r, RevCommit commit) {
    DiffFormatter formatter = new DiffFormatter(DisabledOutputStream.INSTANCE)
    formatter.setRepository(r)
    formatter.setDetectRenames(true)
    formatter.setDiffComparator(RawTextComparator.DEFAULT);
    def diffs
    RevWalk rw = new RevWalk(r)
    if (commit.parentCount > 0) {
        RevCommit parent = rw.parseCommit(commit.parents[0].id)
        diffs = formatter.scan(parent.tree, commit.tree)
    } else {
        diffs = formatter.scan(new EmptyTreeIterator(),
                               new CanonicalTreeParser(null, rw.objectReader, commit.tree))
    }
    rw.dispose()
    // Grab each filepath
    Set<String> fileNameSet = new HashSet<String>( diffs.size() );
    for (DiffEntry entry in diffs) {
        FileHeader header = formatter.toFileHeader(entry)
        fileNameSet.add( header.newPath )
    }
    return fileNameSet;
}
distrib/groovy/jenkins.groovy
distrib/groovy/localclone.groovy
distrib/groovy/protect-refs.groovy
distrib/groovy/sendmail-html.groovy
distrib/groovy/sendmail.groovy
distrib/groovy/thebuggenie.groovy
distrib/installService.cmd
@@ -25,12 +25,12 @@
         --StartPath="%CD%" ^
         --StartClass=com.gitblit.Launcher ^
         --StartMethod=main ^
         --StartParams="--storePassword;gitblit" ^
         --StartParams="--storePassword;gitblit;--baseFolder;%CD%\data" ^
         --StartMode=jvm ^
         --StopPath="%CD%" ^
         --StopClass=com.gitblit.Launcher ^
         --StopMethod=main ^
         --StopParams="--stop" ^
         --StopParams="--stop;--baseFolder;%CD%\data" ^
         --StopMode=jvm ^
         --Classpath="%CD%\gitblit.jar" ^
         --Jvm=auto ^
distrib/projects.conf
New file
@@ -0,0 +1,3 @@
[project "main"]
    title = Main Repositories
    description = main group of repositories
docs/01_features.mkd
@@ -20,7 +20,7 @@
- *Experimental* built-in Garbage Collection
- Ability to federate with one or more other Gitblit instances
- RSS/JSON RPC interface
- Java/Swing Gitblit Manager tool
- Java/Swing Gitblit Manager tool
- Gitweb inspired web UI
- Responsive web UI that subtracts elements to be usable on phones, tablets, and desktop browsers
- Groovy pre- and post- push hook scripts, per-repository or globally for all repositories
@@ -32,9 +32,12 @@
- Repository Owners may edit repositories through the web UI
- Administrators and Repository Owners may set the default branch through the web UI or RPC interface
- LDAP authentication and optional LDAP-controlled Team memberships
- Redmine authentication
- Gravatar integration
- Git-notes display support
- Submodule support
- Push log based on a hidden, orphan branch refs/gitblit/pushes
- Fanout PubSub notifications service for self-hosted [Sparkleshare](http://sparkleshare.org) use
- gh-pages display support (Jekyll is not supported)
- Branch metrics (uses Google Charts)
- HEAD and Branch RSS feeds
@@ -56,6 +59,9 @@
    - Spanish
    - Polish
    - Korean
    - Brazilian Portuguese
    - Dutch
    - Chinese (zh_CN)
## Gitblit GO Features
- Out-of-the-box integrated stack requiring minimal configuration
@@ -63,7 +69,7 @@
- Integrated GUI tool to facilitate x509 PKI including ssl and client certificate generation, client certificate revocation, and client certificate distribution
- Single text file for configuring server and gitblit
- A Windows service installation script and configuration tool
- Built-in AJP connector for Apache httpd
- Built-in AJP connector for Apache httpd
## Limitations
- HTTP/HTTPS are the only supported Git protocols
docs/01_setup.mkd
@@ -1,53 +1,55 @@
## Gitblit WAR Setup
## Gitblit WAR Installation & Setup
1. Download [Gitblit WAR %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) to the webapps folder of your servlet container.  
2. You may have to manually extract the WAR (zip file) to a folder within your webapps folder.
3. Copy the `WEB-INF/users.conf` file to a location outside the webapps folder that is accessible by your servlet container.
Optionally copy the example hook scripts in `WEB-INF/groovy` to a location outside the webapps folder that is accesible by your servlet container.
4. The Gitblit webapp is configured through its `web.xml` file.
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; *groovy.scriptsFolder* (set the full path to your Groovy hook scripts folder)
    - &lt;context-parameter&gt; *groovy.grapeFolder* (set the full path to your Groovy Grape artifact cache)
    - &lt;context-parameter&gt; *web.projectsFile* (set the full path to your projects metadata file)
    - &lt;context-parameter&gt; *realm.userService* (set the full path to `users.conf`)
3. By default, the Gitblit webapp is configured through `WEB-INF/data/gitblit.properties`.<br/>
Open `WEB-INF/data/gitblit.properties` in your favorite text editor and make sure to review and set:
    - &lt;context-parameter&gt; *git.packedGitLimit* (set larger than the size of your largest repository)
    - &lt;context-parameter&gt; *git.streamFileThreshold* (set larger than the size of your largest committed file)
5. You may have to restart your servlet container.
6. Open your browser to <http://localhost/gitblit> or whatever the url should be.
7. Enter the default administrator credentials: **admin / admin** and click the *Login* button
4. You may have to restart your servlet container.
5. Open your browser to <http://localhost/gitblit> or whatever the url should be.
6. Enter the default administrator credentials: **admin / admin** and click the *Login* button
    **NOTE:** Make sure to change the administrator username and/or password!! 
## Gitblit GO Setup
### WAR Data Location
By default, Gitblit WAR stores all data (users, settings, repositories, etc) in `${contextFolder}/WEB-INF/data`.  This is fine for a quick setup, but there are many reasons why you don't want to keep your data within the webapps folder of your servlet container.  You may specify an external location for your data by editing `WEB-INF/web.xml` and manipulating the *baseFolder* context parameter.  Choose a location that is writeable by your servlet container.  Your servlet container may be smart enough to recognize the change and to restart Gitblit.
On the next restart of Gitblit, Gitblit will copy the contents of the `WEB-INF/data` folder to your specified *baseFolder* **IF** the file `${baseFolder}/gitblit.properties` does not already exist.  This allows you to get going with minimal fuss.
Specifying an alternate *baseFolder* also allows for simpler upgrades in the future.
## Gitblit GO Installation & Setup
1. Download and unzip [Gitblit GO %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%GO%).  
*Its best to eliminate spaces in the path name.* 
2. The server itself is configured through a simple text file.
Open `gitblit.properties` in your favorite text editor and make sure to review and set:
    - *git.repositoryFolder* (path may be relative or absolute)
    - *groovy.scriptsFolder* (path may be relative or absolute)
    - *groovy.grapeFolder* (path may be relative or absolute)
    - *server.tempFolder* (path may be relative or absolute)
2. The server itself is configured through a simple text file.<br/>
Open `data/gitblit.properties` in your favorite text editor and make sure to review and set:
    - *server.httpPort* and *server.httpsPort*
    - *server.httpBindInterface* and *server.httpsBindInterface*  
    - *server.storePassword*
    **https** is strongly recommended because passwords are insecurely transmitted form your browser/git client using Basic authentication!
    - *git.packedGitLimit* (set larger than the size of your largest repository)
    - *git.streamFileThreshold* (set larger than the size of your largest committed file)
3. Execute `authority.cmd` or `java -jar authority.jar` from a command-line
3. Execute `authority.cmd` or `java -jar authority.jar --baseFolder data` from a command-line
    1. fill out the fields in the *new certificate defaults* dialog
    2. enter the store password used in *server.storePassword* when prompted.  This generates an SSL certificate for **localhost**.
    3. you may want to generate an SSL certificate for the hostname or ip address hostnames you are serving from<br/>**NOTE:** You can only have **one** SSL certificate specified for a port.
    5. exit the authority app
4. Execute `gitblit.cmd` or `java -jar gitblit.jar` from a command-line
4. Execute `gitblit.cmd` or `java -jar gitblit.jar --baseFolder data` from a command-line
5. Open your browser to <http://localhost:8080> or <https://localhost:8443> depending on your chosen configuration.
6. Enter the default administrator credentials: **admin / admin** and click the *Login* button    
    **NOTE:** Make sure to change the administrator username and/or password!! 
### GO Data Location
By default, Gitblit GO stores all data (users, settings, repositories, etc) in the `data` subfolder of your GO installation.  You may specify an external location for your data on the command-line by setting the *--baseFolder* argument.  If you relocate the data folder then you must supply the *--baseFolder* argument to both GO and the Certificate Authority.
If you are deploying Gitblit to a *nix platform, you might consider moving the data folder out of the GO installation folder and then creating a symlink named "data" that points to your moved folder.
### Creating your own Self-Signed SSL Certificate
Gitblit GO (and Gitblit Certificate Authority) automatically generates a Certificate Authority (CA) certificate and an ssl certificate signed by this CA certificate that is bound to *localhost*.
Remote Eclipse/EGit/JGit clients (<= 2.1.0) will fail to communicate using this certificate because JGit always verifies the hostname of the certificate, regardless of the *http.sslVerify=false* client-side setting.
Remote Eclipse/EGit/JGit clients (<= 2.2.0) will fail to communicate using this certificate because JGit always verifies the hostname of the certificate, regardless of the *http.sslVerify=false* client-side setting.
The EGit failure message is something like:
@@ -56,7 +58,7 @@
If you want to serve your repositories to another machine over https then you will want to generate a new certificate for the hostname or ip address you are serving from.
1. `authority.cmd` or `java -jar authority.jar`
1. `authority.cmd` or `java -jar authority.jar --baseFolder data`
2. Click the *new ssl certificate* button (red rosette in the toolbar in upper left of window)
3. Enter the hostname or ip address
4. Make sure the checkbox *serve https with this certificate* is checked
@@ -64,11 +66,11 @@
 
If you decide to change the value of *server.storePassword* (recommended) <u>after</u> you have already started Gitblit or Gitblit Certificate Authority, then you will have to delete the following files and then restart the Gitblit Certificate Authority app:
1. serverKeyStore.jks
2. serverTrustStore.jks
3. certs/caKeyStore.jks
4. certs/ca.crt
5. certs/caRevocationList.crl (optional)
1. data/serverKeyStore.jks
2. data/serverTrustStore.jks
3. data/certs/caKeyStore.jks
4. data/certs/ca.crt
5. data/certs/caRevocationList.crl (optional)
### Client SSL Certificates
SINCE 1.2.0
@@ -84,6 +86,7 @@
How do you make your servlet container trust a client certificate?
In the WAR variant, you will have to manually setup your servlet container to:
1. want/need client certificates
2. trust a CA certificate used to sign your client certificates
3. generate client certificates signed by your CA certificate
@@ -92,9 +95,9 @@
#### Creating SSL Certificates with Gitblit Certificate Authority
When you generate a new client certificate, a zip file bundle is created which includes a P12 keystore for browsers and a PEM keystore for Git.  Both of these are password-protected.  Additionally, a personalized README file is generated with setup instructions for popular browsers and Git.  The README is generated from `certs\instructions.tmpl` and can be modified to suit your needs.
When you generate a new client certificate, a zip file bundle is created which includes a P12 keystore for browsers and a PEM keystore for Git.  Both of these are password-protected.  Additionally, a personalized README file is generated with setup instructions for popular browsers and Git.  The README is generated from `data\certs\instructions.tmpl` and can be modified to suit your needs.
1. `authority.cmd` or `java -jar authority.jar`
1. `authority.cmd` or `java -jar authority.jar --baseFolder data`
2. Select the user for which to generate the certificate
3. Click the *new certificate* button and enter the expiration date of the certificate.  You must also enter a password for the generated keystore.  This password is *not* the same as the user's login password.  This password is used to protect the privatekey and public certificate you will generate for the selected user.  You must also enter a password hint for the user.
4. If your mail server settings are properly configured you will have a *send email* checkbox which you can use to immediately send the generated certificate bundle to the user.
@@ -130,6 +133,7 @@
#### Command-Line Parameters
Command-Line parameters override the values in `gitblit.properties` at runtime.
    --baseFolder           The default base folder for all relative file reference settings
    --repositoriesFolder   Git Repositories Folder
    --userService          Authentication and Authorization Service (filename or fully qualified classname)
    --useNio               Use NIO Connector else use Socket Connector.
@@ -143,7 +147,7 @@
    
**Example**
    java -jar gitblit.jar --userService c:\myrealm.config --storePassword something
    java -jar gitblit.jar --userService c:/myrealm.config --storePassword something --baseFolder c:/data
#### Overriding Gitblit GO's Log4j Configuration
@@ -221,9 +225,6 @@
    Alternatively, you can respecify *web.forwardSlashCharacter*.
## Upgrading Gitblit
Generally, upgrading is easy.
Since Gitblit does not use a database the only files you have to worry about are your configuration file (`gitblit.properties` or `web.xml`) and possibly your `users.conf` or `users.properties` file.
Any important changes to the setting keys or default values will always be mentioned in the [release log](releases.html).
@@ -231,26 +232,60 @@
`users.properties` and its user service implementation are deprecated as of v0.8.0.
### Upgrading Gitblit WAR
1. Backup your `web.xml` file
Backup your `web.properties` file (if you have one, these are the setting overrides from using the RPC administration service)
2. Delete currently deployed gitblit WAR
3. Deploy new WAR and overwrite the `web.xml` file with your backup
4. Review and optionally apply any new settings as indicated in the [release log](releases.html).
### Upgrading Gitblit WAR (1.2.1+)
1. Make sure your `WEB-INF/web.xml` *baseFolder* context parameter is not `${contextFolder}/WEB-INF/data`!<br/>
If it is, move your `WEB-INF/data` folder to a location writeable by your servlet container.
2. Deploy new WAR
3. Edit the new WAR's `WEB-INF/web.xml` file and set the *baseFolder* context parameter to your external baseFolder.
4. Review and optionally apply any new settings as indicated in the [release log](releases.html) to `${baseFolder}/gitblit.properties`.
 
### Upgrading Gitblit GO
### Upgrading Gitblit GO (1.2.1+)
 
1. Backup your `gitblit.properties` file
2. Backup your `users.properties` file *(if it is located in the Gitblit GO folder)*
OR
Backup your `users.conf` file *(if it is located in the Gitblit GO folder)*
3. Backup your Groovy hook scripts
4. Unzip Gitblit GO to a new folder
5. Overwrite the `gitblit.properties` file with your backup
6. Overwrite the `users.properties` file with your backup *(if it was located in the Gitblit GO folder)*
OR
Overwrite the `users.conf` file with your backup *(if it was located in the Gitblit GO folder)*
7. Review and optionally apply any new settings as indicated in the [release log](releases.html).
1. Unzip Gitblit GO to a new folder
2. Copy your `data` folder from your current Gitblit installation to the new folder and overwrite any conflicts
3. Review and optionally apply any new settings as indicated in the [release log](releases.html) to `data/gitblit.properties`.
In *nix systems, there are other tricks you can play like symlinking the `data` folder or symlinking the GO folder.
All platforms support the *--baseFolder* command-line argument.
### Upgrading Gitblit WAR (pre-1.2.1)
1. Create a `data` as outlined in step 1 of *Upgrading Gitblit GO (pre-1.2.1)*
2. Copy your existing web.xml to your data folder
3. Deploy new WAR
4. Copy the new WAR's `WEB-INF/data/gitblit.properties` file to your data folder
5. Manually apply any changes you made to your original web.xml file to the gitblit.properties file you copied to your data folder
6. Edit the new WAR's `WEB-INF/web.xml` file and set the *baseFolder* context parameter to your external baseFolder.
### Upgrading Gitblit GO (pre-1.2.1)
1. Create a `data` folder and copy the following files and folders to it:
    - **users.conf*
    - **projects.conf** *(if you have one)*
    - **gitblit.properties**
    - **serverKeystore.jks**
    - **serverTrustStore.jks**
    - *certs** folder
    - **git** folder
    - **groovy** folder
    - **proposals** folder
    - and any other custom files (robots.txt, welcome/login markdown files, etc)
    - then edit your `gitblit.properties` file and adjust the following settings:
        - *git.repositoriesFolder* = ${baseFolder}/git
        - *groovy.scriptsFolder* = ${baseFolder}/groovy
        - *groovy.grapeFolder* = ${baseFolder}/groovy/grape
        - *web.projectsFile* = ${baseFolder}/projects.conf
        - *realm.userService* = ${baseFolder}/users.conf
        - *web.robots.txt* = ${baseFolder}/robots.txt
        - *federation.proposalsFolder* = ${baseFolder}/proposals
        - *realm.ldap.backingUserService* = ${baseFolder}/users.conf
        - *realm.redmine.backingUserService* = ${baseFolder}/users.conf
        - *server.tempFolder* = ${baseFolder}/temp
2. Unzip Gitblit GO to a new folder
3. Copy your `data` folder and overwrite the folder of the same name in the just-unzipped version
4. Review and optionally apply any new settings as indicated in the [release log](releases.html) to `data/gitblit.properties`.
**NOTE:** You may need to adjust your service definitions to include the `--baseFolder data` argument.
#### Upgrading Windows Service
You may need to delete your old service definition and install a new one depending on what has changed in the release.
@@ -277,7 +312,7 @@
        federationSets = 
#### Repository Names
Repository names must be unique and are CASE-SENSITIVE ON CASE-SENSITIVE FILESYSTEMS.  The name must be composed of letters, digits, or `/ _ - . ~`<br/>
Repository names must be case-insensitive-unique but are CASE-SENSITIVE ON CASE-SENSITIVE FILESYSTEMS.  The name must be composed of letters, digits, or `/ _ - . ~`<br/>
Whitespace is illegal.
Repositories can be grouped within subfolders.  e.g. *libraries/mycoollib.git* and *libraries/myotherlib.git*
@@ -366,6 +401,10 @@
You can not use fast-forward merges on your client when using committer verification.  You must specify *--no-ff* to ensure that a merge commit is created with your identity as the committer.  Only the first parent chain is traversed when verifying commits.
#### Push Log
Gitblit v1.2.1 introduces an incomplete push mechanism.  All pushes are logged since 1.2.1, but the log has not yet been exposed through the web ui.  This will be a feature of an upcoming release.
### Teams
Since v0.8.0, Gitblit supports *teams* for the original `users.properties` user service and the current default user service `users.conf`.  Teams have assigned users and assigned repositories.  A user can be a member of multiple teams and a repository may belong to multiple teams.  This allows the administrator to quickly add a user to a team without having to keep track of all the appropriate repositories. 
docs/04_releases.mkd
@@ -1,17 +1,76 @@
## Release History
<div class="alert alert-info">
<h4>Update Note</h4>
The permissions model has changed in this release.
<p>If you are updating your server, you must also update any Gitblit Manager and Federation Client installs to 1.2.0 as well.  The data model used by the RPC mechanism has changed slightly for the new permissions infrastructure.</p>
</div>
### 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%) | [express](http://code.google.com/p/gitblit/downloads/detail?name=%EXPRESS%) | [fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) | [manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) | [api](http://code.google.com/p/gitblit/downloads/detail?name=%API%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
#### fixes
- Can't set reset settings with $ or { characters through Gitblit Manager because they are not properly escaped
#### additions
 - FogBugz post-receive hook script (github/djschny)
 - Implemented multiple repository owners (github/akquinet)
 - Chinese translation (github/dapengme, github/yin8086)
### Older Releases
<div class="alert alert-info">
<h4>Update Note 1.2.1</h4>
Because there are now several types of files and folders that must be considered Gitblit data, the default location for data has changed.
<p>You will need to move a few files around when upgrading.  Please see the Upgrading section of the <a href="setup.html">setup</a> page for details.</p>
<b>Express Users</b> make sure to update your web.xml file with the ${baseFolder} values!
</div>
#### fixes
- Fixed nullpointer on recursively calculating folder sizes when there is a named pipe or symlink in the hierarchy
- Added nullchecking when concurrently forking a repository and trying to display it's fork network (issue-187)
- Fixed bug where permission changes were not visible in the web ui to a logged-in user until the user logged-out and then logged back in again (issue-186)
- Fixed nullpointer on creating a repository with mixed case (issue 185)
- Include missing model classes in api library (issue-184)
- Fixed nullpointer when using *web.allowForking = true* && *git.cacheRepositoryList = false* (issue 182)
- Likely fix for commit and commitdiff page failures when a submodule reference changes (issue 178)
- Build project models from the repository model cache, when possible, to reduce page load time (issue 172)
- Fixed loading of Brazilian Portuguese translation from *nix server (github/inaiat)
#### additions
- Fanout PubSub service for self-hosted [Sparkleshare](http://sparkleshare.org) notifications.<br/>
This service is disabled by default.<br/>
    **New:** *fanout.bindInterface = localhost*<br/>
    **New:** *fanout.port = 0*<br/>
    **New:** *fanout.useNio = true*<br/>
    **New:** *fanout.connectionLimit = 0*
- Implemented a simple push log based on a hidden, orphan branch refs/gitblit/pushes (issue 177)<br/>
The push log is not currently visible in the ui, but the data will be collected and it will be exposed to the ui in the next release.
- Support for locally and remotely authenticated accounts in LdapUserService and RedmineUserService (issue 183)
- Added Dutch translation (github/kwoot)
#### changes
- Gitblit GO and Gitblit WAR are now both configured by `gitblit.properties`. WAR is no longer configured by `web.xml`.<br/>
However, Express for OpenShift continues to be configured by `web.xml`.
- Support for a *--baseFolder* command-line argument for Gitblit GO and Gitblit Certificate Authority
- Support for specifying a *${baseFolder}* parameter in `gitblit.properties` and `web.xml` for several settings
- Improve history display of a submodule link
- Updated Korean translation (github/ds5apn)
- Updated checkstyle definition (github/mystygage)
<div class="alert alert-info">
<h4>Update Note 1.2.0</h4>
The permissions model has changed in the 1.2.0 release.
<p>If you are updating your server, you must also update any Gitblit Manager and Federation Client installs to 1.2.0 as well.  The data model used by the RPC mechanism has changed slightly for the new permissions infrastructure.</p>
</div>
**1.2.0** *released 2012-12-31*
#### fixes
- Fixed regression in *isFrozen* (issue 181)
- Author metrics can be broken by newlines in email addresses from converted repositories (issue 176)
- Set subjectAlternativeName on generated SSL cert if CN is an ip address (issue 170)
- Fixed incorrect links on history page for files not in the current/active commit (issue 166)
- Empty repository page failed to handle missing repository (issue 160)
@@ -27,7 +86,7 @@
#### additions
- Implemented discrete repository permissions (issue 36)
- Implemented discrete repository permissions (issue 36)
    - V (view in web ui, RSS feeds, download zip)
    - R (clone)
    - RW (clone and push)
@@ -35,34 +94,34 @@
    - RWD (clone and push with ref creation, deletion)
    - RW+ (clone and push with ref creation, deletion, rewind)
While not as sophisticated as Gitolite, this does give finer access controls.  These permissions fit in cleanly with the existing users.conf and users.properties files.  In Gitblit <= 1.1.0, all your existing user accounts have RW+ access.   If you are upgrading to 1.2.0, the RW+ access is *preserved* and you will have to lower/adjust accordingly.
- Implemented *case-insensitive* regex repository permission matching (issue 36)
- Implemented *case-insensitive* regex repository permission matching (issue 36)<br/>
This allows you to specify a permission like `RW:mygroup/.*` to grant push privileges to all repositories within the *mygroup* project/folder.
- Added DELETE, CREATE, and NON-FAST-FORWARD ref change logging
- Added support for personal repositories.
- Added support for personal repositories.<br/>
Personal repositories can be created by accounts with the *create* permission and are stored in *git.repositoriesFolder/~username*.  Each user with personal repositories will have a user page, something like the GitHub profile page.  Personal repositories have all the same features as common repositories, except personal repositories can be renamed by their owner.
- Added support for server-side forking of a repository to a personal repository (issue 137)
In order to fork a repository, the user account must have the *fork* permission **and** the repository must *allow forks*.  The clone inherits the access list of its origin.  i.e. if Team A has clone access to the origin repository, then by default Team A also has clone access to the fork.  This is to facilitate collaboration.  The fork owner may change access to the fork and add/remove users/teams, etc as required <u>however</u> it should be noted that all personal forks will be enumerated in the fork network regardless of access view restrictions.  If you really must have an invisible fork, the clone it locally, create a new repository for your invisible fork, and push it back to Gitblit.
- Added support for server-side forking of a repository to a personal repository (issue 137)<br/>
In order to fork a repository, the user account must have the *fork* permission **and** the repository must *allow forks*.  The clone inherits the access list of its origin.  i.e. if Team A has clone access to the origin repository, then by default Team A also has clone access to the fork.  This is to facilitate collaboration.  The fork owner may change access to the fork and add/remove users/teams, etc as required <u>however</u> it should be noted that all personal forks will be enumerated in the fork network regardless of access view restrictions.  If you really must have an invisible fork, the clone it locally, create a new repository for your invisible fork, and push it back to Gitblit.<br/>
    **New:** *web.allowForking=true*
- Added optional *create-on-push* support
- Added optional *create-on-push* support<br/>
    **New:** *git.allowCreateOnPush=true*
- Added **experimental** JGit-based garbage collection service.  This service is disabled by default.
    **New:** *git.allowGarbageCollection=false*
    **New:** *git.garbageCollectionHour = 0*
    **New:** *git.defaultGarbageCollectionThreshold = 500k*
- Added **experimental** JGit-based garbage collection service.  This service is disabled by default.<br/>
    **New:** *git.allowGarbageCollection=false*<br/>
    **New:** *git.garbageCollectionHour = 0*<br/>
    **New:** *git.defaultGarbageCollectionThreshold = 500k*<br/>
    **New:** *git.defaultGarbageCollectionPeriod = 7 days*
- Added support for X509 client certificate authentication (github/kevinanderson1).  (issue 106)
You can require all git servlet access be authenticated by a client certificate.  You may also specify the OID fingerprint to use for mapping a certificate to a username.  It should be noted that the user account MUST already exist in Gitblit for this authentication mechanism to work; this mechanism can not be used to automatically create user accounts from a certificate.
    **New:** *git.requireClientCertificates = false*
    **New:** *git.enforceCertificateValidity = true*
- Added support for X509 client certificate authentication (github/kevinanderson1).  (issue 106)<br/>
You can require all git servlet access be authenticated by a client certificate.  You may also specify the OID fingerprint to use for mapping a certificate to a username.  It should be noted that the user account MUST already exist in Gitblit for this authentication mechanism to work; this mechanism can not be used to automatically create user accounts from a certificate.<br/>
    **New:** *git.requireClientCertificates = false*<br/>
    **New:** *git.enforceCertificateValidity = true*<br/>
    **New:** *git.certificateUsernameOIDs = CN*
- Revised clean install certificate generation to create a Gitblit GO Certificate Authority certificate; an SSL certificate signed by the CA certificate; and to create distinct server key and server trust stores.  <u>The store files have been renamed!</u>
- Added support for Gitblit GO to require usage of client certificates to access the entire server.
This is extreme and should be considered carefully since it affects every https access.  The default is to **want** client certificates.  Setting this value to *true* changes that to **need** client certificates.
- Added support for Gitblit GO to require usage of client certificates to access the entire server.<br/>
This is extreme and should be considered carefully since it affects every https access.  The default is to **want** client certificates.  Setting this value to *true* changes that to **need** client certificates.<br/>
    **New:** *server.requireClientCertificates = false*
- Added **Gitblit Certificate Authority**, an x509 PKI management tool for Gitblit GO to encourage use of x509 client certificate authentication.
- Added setting to control length of shortened commit ids
- Added setting to control length of shortened commit ids<br/>
    **New:** *web.shortCommitIdLength=8*
- Added alternate compressed download formats: tar.gz, tar.xz, tar.bzip2 (issue 174)
- Added alternate compressed download formats: tar.gz, tar.xz, tar.bzip2 (issue 174)<br/>
    **New:** *web.compressedDownloads = zip gz*
- Added simple project pages.  A project is a subfolder off the *git.repositoriesFolder*.
- Added support for X-Forwarded-Context for Apache subdomain proxy configurations (issue 135)
@@ -71,6 +130,7 @@
- Added HTML sendmail hook script and Gitblit.sendHtmlMail method (github/sauthieg)
- Added RedmineUserService (github/mallowlabs)
- Support for committer verification.  Requires use of *--no-ff* when merging branches or pull requests.  See setup page for details.
- Added Brazilian Portuguese translation (github/rafaelcavazin)
#### changes
@@ -86,14 +146,14 @@
- Expose ReceivePack to Groovy push hooks (issue 125)
- Redirect to summary page when refreshing the empty repository page on a repository that is not empty (issue 129)
- Emit a warning in the log file if running on a Tomcat-based servlet container which is unfriendly to %2F forward-slash url encoding AND Gitblit is configured to mount parameters with %2F forward-slash url encoding (Github/jpyeron, issue 126)
- LDAP admin attribute setting is now consistent with LDAP teams setting and admin teams list.
- LDAP admin attribute setting is now consistent with LDAP teams setting and admin teams list.
If *realm.ldap.maintainTeams==true* **AND** *realm.ldap.admins* is not empty, then User.canAdmin() is controlled by LDAP administrative team membership.  Otherwise, User.canAdmin() is controlled by Gitblit.
- Support servlet container authentication for existing UserModels (issue 68)
#### dependency changes
- updated to Jetty 7.6.8
- updated to JGit 2.1.0.201209190230-r
- updated to JGit 2.2.0.201212191850-r
- updated to Groovy 1.8.8
- updated to Wicket 1.4.21
- updated to Lucene 3.6.1
@@ -104,10 +164,8 @@
- added XZ for Java 1.0
<hr/>
### Older Releases
<div class="alert alert-error">
<h4>Update Note</h4>
<h4>Update Note 1.1.0</h4>
If you are updating from an earlier release AND you have indexed branches with the Lucene indexing feature, you need to be aware that this release will completely re-index your repositories.  Please be sure to provide ample heap resources as appropriate for your installation.
</div>
@@ -135,24 +193,24 @@
#### additions
- Identified repository list is now cached by default to reduce disk io and to improve performance (issue 103)
- Identified repository list is now cached by default to reduce disk io and to improve performance (issue 103)<br/>
    **New:** *git.cacheRepositoryList=true*
- Preliminary bare repository submodule support
- Preliminary bare repository submodule support<br/>
    **New:** *git.submoduleUrlPatterns=*
    - *git.submoduleUrlPatterns* is a space-delimited list of regular expressions for extracting a repository name from a submodule url.
    For example, `git.submoduleUrlPatterns = .*?://github.com/(.*)` would extract *gitblit/gitblit.git* from *git://github.git/gitblit/gitblit.git*
    - *git.submoduleUrlPatterns* is a space-delimited list of regular expressions for extracting a repository name from a submodule url.<br/>
    For example, `git.submoduleUrlPatterns = .*?://github.com/(.*)` would extract *gitblit/gitblit.git* from *git://github.git/gitblit/gitblit.git*<br/>
    **Note:** You may not need this control to work with submodules, but it is there if you do.
    - If there are no matches from *git.submoduleUrlPatterns* then the repository name is assumed to be whatever comes after the last `/` character *(e.g. gitblit.git)*
    - Gitblit will try to locate this repository relative to the current repository *(e.g. myfolder/myrepo.git, myfolder/mysubmodule.git)* and then at the root level *(mysubmodule.git)* if that fails.
    - Submodule references in a working copy will be properly identified as gitlinks, but Gitblit will not traverse into the working copy submodule repository.
- Added a repository setting to control authorization as AUTHENTICATED or NAMED. (issue 117)
- Added a repository setting to control authorization as AUTHENTICATED or NAMED. (issue 117)<br/>
NAMED is the original behavior for authorizing against a list of permitted users or permitted teams.
AUTHENTICATED allows restricted access for any authenticated user.  This is a looser authorization control.
- Added default authorization control setting (AUTHENTICATED or NAMED)
- Added default authorization control setting (AUTHENTICATED or NAMED)<br/>
    **New:** *git.defaultAuthorizationControl=NAMED*
- Added setting to control how deep Gitblit will recurse into *git.repositoriesFolder* looking for repositories (issue 103)
- Added setting to control how deep Gitblit will recurse into *git.repositoriesFolder* looking for repositories (issue 103)<br/>
    **New:** *git.searchRecursionDepth=-1*
- Added setting to specify regex exclusions for repositories (issue 103)
- Added setting to specify regex exclusions for repositories (issue 103)<br/>
    **New:** *git.searchExclusions=*
- Blob page now supports displaying images (issue 6)
- Non-image binary files can now be downloaded using the RAW link
@@ -182,38 +240,38 @@
#### changes
- **Updated Lucene index version which will force a rebuild of ALL your Lucene indexes**
- **Updated Lucene index version which will force a rebuild of ALL your Lucene indexes**<br/>
Make sure to properly set *web.blobEncodings* before starting Gitblit if you are updating!  (issue 97)
- Changed default layout for web ui from Fixed-Width layout to Responsive layout (issue 101)
- IUserService interface has changed to better accomodate custom authentication and/or custom authorization
- IUserService interface has changed to better accomodate custom authentication and/or custom authorization<br/>
    The default `users.conf` now supports persisting display names and email addresses.
- Updated Japanese translation (Github/zakki)
#### additions
- Added setting to allow specification of a robots.txt file (issue 99)
- Added setting to allow specification of a robots.txt file (issue 99)<br/>
    **New:** *web.robots.txt =*
- Added setting to control Responsive layout or Fixed-Width layout (issue 101)
- Added setting to control Responsive layout or Fixed-Width layout (issue 101)<br/>
    Responsive layout is now the default.  This layout gracefully scales the web ui from a desktop layout to a mobile layout by hiding page components.  It is easy to try, just resize your browser or point your Android/iOS device to the url of your Gitblit install.
    **New:** *web.useResponsiveLayout = true*
- Added setting to control charsets for blob string decoding.  Default encodings are UTF-8, ISO-8859-1, and server's default charset. (issue 97)
- Added setting to control charsets for blob string decoding.  Default encodings are UTF-8, ISO-8859-1, and server's default charset. (issue 97)<br/>
    **New:** *web.blobEncodings = UTF-8 ISO-8859-1*
- Exposed JGit's internal configuration settings in gitblit.properties/web.xml (issue 93)
    Review your `gitblit.properties` or `web.xml` for detailed explanations of these settings.
    **New:** *git.packedGitWindowSize = 8k*
    **New:** *git.packedGitLimit = 10m*
    **New:** *git.deltaBaseCacheLimit = 10m*
    **New:** *git.packedGitOpenFiles = 128*
    **New:** *git.streamFileThreshold = 50m*
- Exposed JGit's internal configuration settings in gitblit.properties/web.xml (issue 93)<br/>
    Review your `gitblit.properties` or `web.xml` for detailed explanations of these settings.<br/>
    **New:** *git.packedGitWindowSize = 8k*<br/>
    **New:** *git.packedGitLimit = 10m*<br/>
    **New:** *git.deltaBaseCacheLimit = 10m*<br/>
    **New:** *git.packedGitOpenFiles = 128*<br/>
    **New:** *git.streamFileThreshold = 50m*<br/>
    **New:** *git.packedGitMmap = false*
- Added default access restriction.  Applies to new repositories and repositories that have not been configured with Gitblit. (issue 88)
- Added default access restriction.  Applies to new repositories and repositories that have not been configured with Gitblit. (issue 88)<br/>
    **New:** *git.defaultAccessRestriction = NONE*
- Added Ivy 2.2.0 dependency which enables Groovy Grapes, a mechanism to resolve and retrieve library dependencies from a Maven 2 repository within a Groovy push hook script
- Added setting to control Groovy Grape root folder (location where resolved dependencies are stored)
    [Grape](http://groovy.codehaus.org/Grape) allows you to add Maven dependencies to your pre-/post-receive hook script classpath.
- Added setting to control Groovy Grape root folder (location where resolved dependencies are stored)<br/>
    [Grape](http://groovy.codehaus.org/Grape) allows you to add Maven dependencies to your pre-/post-receive hook script classpath.<br/>
    **New:** *groovy.grapeFolder = groovy/grape*
- Added LDAP User Service with many new *realm.ldap* keys (Github/jcrygier)
- Added support for custom repository properties for Groovy hooks (Github/jcrygier)
- Added support for custom repository properties for Groovy hooks (Github/jcrygier)<br/>
    Custom repository properties complement hook scripts by providing text field prompts in the web ui and the Gitblit Manager for the defined properties.  This allows your push hooks to be parameterized.
- Added script to facilitate proxy environment setup on Linux (Github/mragab)
- Added Polish translation (Lukasz Jader)
@@ -278,23 +336,23 @@
#### additions
- Added optional Lucene branch indexing (issue 16)
    **New:** *web.allowLuceneIndexing = true*
- Added optional Lucene branch indexing (issue 16)<br/>
    **New:** *web.allowLuceneIndexing = true*<br/>
    **New:** *web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip*
Repository branches may be optionally indexed by Lucene for improved searching.  To use this feature you must specify which branches to index within the *Edit Repository* page; _no repositories are automatically indexed_.  Gitblit will build or incrementally update enrolled repositories on a 2 minute cycle. (i.e you will have to wait 2-3 minutes after respecifying indexed branches or pushing new commits before Gitblit will build/update the repository's Lucene index.)
If a repository has Lucene-indexed branches the *search* form on the repository pages will redirect to the root-level Lucene search page and only the content of those branches can be searched.
If a repository has Lucene-indexed branches the *search* form on the repository pages will redirect to the root-level Lucene search page and only the content of those branches can be searched.<br/>
If the repository does not specify any indexed branches then repository commit-traversal search is used.
**Note:** Initial indexing of an existing repository can be memory-exhaustive. Be sure to provide your Gitblit server adequate heap space to index your repositories (e.g. -Xmx1024M).
**Note:** Initial indexing of an existing repository can be memory-exhaustive. Be sure to provide your Gitblit server adequate heap space to index your repositories (e.g. -Xmx1024M).<br/>
See the [setup](setup.html) page for additional details.
- Allow specifying timezone to use for Gitblit which is independent of both the JVM and the system timezone (issue 54)
- Allow specifying timezone to use for Gitblit which is independent of both the JVM and the system timezone (issue 54)<br/>
    **New:** *web.timezone =*
- Added a built-in AJP connector for integrating Gitblit GO into an Apache mod_proxy setup (issue 59)
    **New:** *server.ajpPort = 0*
- Added a built-in AJP connector for integrating Gitblit GO into an Apache mod_proxy setup (issue 59)<br/>
    **New:** *server.ajpPort = 0*<br/>
    **New:** *server.ajpBindInterface = localhost*
- On the Repositories page show a bang *!* character in the color swatch of a repository with a working copy (issue 49)
- On the Repositories page show a bang *!* character in the color swatch of a repository with a working copy (issue 49)<br/>
Push requests to these repositories will be rejected.
- On all non-bare Repository pages show *WORKING COPY* in the upper right corner (issue 49)
- New setting to prevent display/serving non-bare repositories
- New setting to prevent display/serving non-bare repositories<br/>
    **New:** *git.onlyAccessBareRepositories = false*
- Added *protect-refs.groovy* (Github/plm)
- Allow setting default branch (relinking HEAD) to a branch or a tag (Github/plm)
@@ -348,31 +406,31 @@
#### additions
- Platform-independent, Groovy push hook script mechanism.
Hook scripts can be set per-repository, per-team, or globally for all repositories.
    **New:** *groovy.scriptsFolder = groovy*
    **New:** *groovy.preReceiveScripts =*
- Platform-independent, Groovy push hook script mechanism.<br/>
Hook scripts can be set per-repository, per-team, or globally for all repositories.<br/>
    **New:** *groovy.scriptsFolder = groovy*<br/>
    **New:** *groovy.preReceiveScripts =*<br/>
    **New:** *groovy.postReceiveScripts =*
- *sendmail.groovy* for optional email notifications on push.
- *sendmail.groovy* for optional email notifications on push.<br/>
You must properly configure your SMTP server settings in `gitblit.properties` or `web.xml` to use *sendmail.groovy*.
- New global key for mailing lists.  This is used in conjunction with the *sendmail.groovy* hook script.  All repositories that use the *sendmail.groovy* script will include these addresses in the notification process.  Please see the Setup page for more details about configuring sendmail.
- New global key for mailing lists.  This is used in conjunction with the *sendmail.groovy* hook script.  All repositories that use the *sendmail.groovy* script will include these addresses in the notification process.  Please see the Setup page for more details about configuring sendmail.<br/>
    **New:** *mail.mailingLists =*
- *com.gitblit.GitblitUserService*.  This is a wrapper object for the built-in user service implementations.  For those wanting to only implement custom authentication it is recommended to subclass GitblitUserService and override the appropriate methods.  Going forward, this will help insulate custom authentication from new IUserService API and/or changes in model classes.
- New default user service implementation: *com.gitblit.ConfigUserService* (`users.conf`)
- New default user service implementation: *com.gitblit.ConfigUserService* (`users.conf`)<br/>
This user service implementation allows for serialization and deserialization of more sophisticated Gitblit User objects without requiring the encoding trickery now present in FileUserService (users.properties).  This will open the door for more advanced Gitblit features.
For those upgrading from an earlier Gitblit version, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit <u>however</u> you will have to manually set *realm.userService=users.conf* to switch to the new user service.
The original `users.properties` file and it's corresponding implementation are **deprecated**.
For those upgrading from an earlier Gitblit version, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit <u>however</u> you will have to manually set *realm.userService=users.conf* to switch to the new user service.<br/>
The original `users.properties` file and it's corresponding implementation are **deprecated**.<br/>
    **New:** *realm.userService = users.conf*
- Teams for specifying user-repository access in bulk.  Teams may also specify mailing lists addresses and pre- & post- receive hook scripts.
- Gravatar integration
- Gravatar integration<br/>
    **New:** *web.allowGravatar = true*
- Activity page for aggregated repository activity.  This is a timeline of commit activity over the last N days for one or more repositories.
   **New:** *web.activityDuration = 14*
   **New:** *web.timeFormat = HH:mm*
- Activity page for aggregated repository activity.  This is a timeline of commit activity over the last N days for one or more repositories.<br/>
   **New:** *web.activityDuration = 14*<br/>
   **New:** *web.timeFormat = HH:mm*<br/>
   **New:** *web.datestampLongFormat = EEEE, MMMM d, yyyy*
- *Filters* menu for the Repositories page and Activity page.  You can filter by federation set, team, and simple custom regular expressions.  Custom expressions can be stored in `gitblit.properties` or `web.xml` or directly defined in your url (issue 27)
- *Filters* menu for the Repositories page and Activity page.  You can filter by federation set, team, and simple custom regular expressions.  Custom expressions can be stored in `gitblit.properties` or `web.xml` or directly defined in your url (issue 27)<br/>
   **New:** *web.customFilters=*
- Flash-based 1-step *copy to clipboard* of the primary repository url based on Clippy
- Flash-based 1-step *copy to clipboard* of the primary repository url based on Clippy<br/>
   **New:** *web.allowFlashCopyToClipboard = true*
- JavaScript-based 3-step (click, ctrl+c, enter) *copy to clipboard* of the primary repository url in the event that you do not want to use Flash on your installation
- Empty repositories now link to an *empty repository* page which gives some direction to the user for the next step in using Gitblit.  This page displays the primary push/clone url of the repository and gives sample syntax for the git command-line client. (issue 31)
@@ -382,7 +440,7 @@
#### changes
- Dropped display of trailing .git from repository names
- Gitblit GO is now monolithic like the WAR build. (issue 30)
- Gitblit GO is now monolithic like the WAR build. (issue 30)<br/>
This change helps adoption of GO in environments without an internet connection or with a restricted connection.
- Unit testing framework has been migrated to JUnit4 syntax and the test suite has been redesigned to run all unit tests, including rpc, federation, and git push/clone tests
@@ -402,25 +460,25 @@
**0.7.0** &nbsp; *released 2011-11-11*
- **security**: fixed security hole when cloning clone-restricted repository with TortoiseGit (issue 28)
- improved: updated ui with Twitter's Bootstrap CSS toolkit
- improved: updated ui with Twitter's Bootstrap CSS toolkit<br/>
    **New:** *web.loginMessage = gitblit*
- improved: repositories list performance by caching repository sizes (issue 27)
- improved: summary page performance by caching metric calculations (issue 25)
- added: authenticated JSON RPC mechanism
    **New:** *web.enableRpcServlet = true*
    **New:** *web.enableRpcManagement = false*
- added: authenticated JSON RPC mechanism<br/>
    **New:** *web.enableRpcServlet = true*<br/>
    **New:** *web.enableRpcManagement = false*<br/>
    **New:** *web.enableRpcAdministration = false*
- added: Gitblit API RSS/JSON RPC library
- added: Gitblit Manager (Java/Swing Application) for remote administration of a Gitblit server.
- added: per-repository setting to skip size calculation (faster repositories page loading)
- added: per-repository setting to skip summary metrics calculation (faster summary page loading)
- added: IUserService.setup(IStoredSettings) for custom user service implementations
- added: setting to control Gitblit GO context path for proxy setups *(Github/trygvis)*
- added: setting to control Gitblit GO context path for proxy setups *(Github/trygvis)*<br/>
    **New:** *server.contextPath = /*
- added: *combined-md5* password storage option which stores the hash of username+password as the password *(Github/alyandon)*
- added: repository owners are automatically granted access for git, feeds, and zip downloads without explicitly selecting them *(Github/dadalar)*
- added: RSS feeds now include regex substitutions on commit messages for bug trackers, etc
- fixed: federation protocol timestamps.  dates are now serialized to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard.
- fixed: federation protocol timestamps.  dates are now serialized to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard.<br/>
    **This breaks 0.6.0 federation clients/servers.**
- fixed: collision on rename for repositories and users
- fixed: Gitblit can now browse the Linux kernel repository (issue 25)
@@ -437,14 +495,14 @@
**0.6.0** &nbsp; *released 2011-09-27*
- added: federation feature to allow gitblit instances (or gitblit federation clients) to pull repositories and, optionally, settings and accounts from other gitblit instances.  This is something like [svn-sync](http://svnbook.red-bean.com/en/1.5/svn.ref.svnsync.html) for gitblit.
    **New:** *federation.name =*
    **New:** *federation.passphrase =*
    **New:** *federation.allowProposals = false*
    **New:** *federation.proposalsFolder = proposals*
    **New:** *federation.defaultFrequency = 60 mins*
    **New:** *federation.sets =*
    **New:** *mail.* settings for sending emails
- added: federation feature to allow gitblit instances (or gitblit federation clients) to pull repositories and, optionally, settings and accounts from other gitblit instances.  This is something like [svn-sync](http://svnbook.red-bean.com/en/1.5/svn.ref.svnsync.html) for gitblit.<br/>
    **New:** *federation.name =*<br/>
    **New:** *federation.passphrase =*<br/>
    **New:** *federation.allowProposals = false*<br/>
    **New:** *federation.proposalsFolder = proposals*<br/>
    **New:** *federation.defaultFrequency = 60 mins*<br/>
    **New:** *federation.sets =*<br/>
    **New:** *mail.* settings for sending emails<br/>
    **New:** user role *#notfederated* to prevent a user account from being pulled by a federated Gitblit instance
- added: google-gson dependency
- added: javamail dependency
@@ -465,9 +523,9 @@
- fixed: users can now change their passwords (issue 1)
- fixed: always show root repository group first, i.e. don't sort root group with other groups
- fixed: tone-down repository group header color
- added: optionally display repository on-disk size on repositories page
- added: optionally display repository on-disk size on repositories page<br/>
    **New:** *web.showRepositorySizes = true*
- added: forward-slashes ('/', %2F) can be encoded using a custom character to workaround some servlet container default security measures for proxy servers
- added: forward-slashes ('/', %2F) can be encoded using a custom character to workaround some servlet container default security measures for proxy servers<br/>
    **New:** *web.forwardSlashCharacter = /*
- updated: MarkdownPapers 1.1.0
- updated: Jetty 7.4.3
docs/05_roadmap.mkd
@@ -21,9 +21,11 @@
### IDEAS
* Gitblit: Pull requests
* Gitblit: Watch/Star like github with personalized activity feed
* Gitblit: Push database or orphan branch
* Gitblit: Re-use the EGit branch visualization table cell renderer as some sort of servlet
* Gitblit: diff should highlight inserted/removed fragment compared to original line
* Gitblit: implement branch permission controls as Groovy pre-receive script.
*Maintain permissions text file similar to a gitolite configuration file or svn authz file.*
* Gitblit: respect Gerrit branch permissions
* Gitblit: Consider creating more Git model objects and exposing them via the JSON RPC interface to allow inspection/retrieval of Git commits, Git trees, etc from Gitblit.
* Gitblit: Blame coloring by author (issue 2)
gitblit.iml
@@ -207,13 +207,13 @@
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="org.eclipse.jgit-2.1.0.201209190230-r.jar">
      <library name="org.eclipse.jgit-2.2.0.201212191850-r.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/org.eclipse.jgit-2.1.0.201209190230-r.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/org.eclipse.jgit-2.2.0.201212191850-r.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/org.eclipse.jgit-2.1.0.201209190230-r-sources.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/src/org.eclipse.jgit-2.2.0.201212191850-r-sources.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
@@ -229,13 +229,13 @@
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="org.eclipse.jgit.http.server-2.1.0.201209190230-r.jar">
      <library name="org.eclipse.jgit.http.server-2.2.0.201212191850-r.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/org.eclipse.jgit.http.server-2.1.0.201209190230-r.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/org.eclipse.jgit.http.server-2.2.0.201212191850-r.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/org.eclipse.jgit.http.server-2.1.0.201209190230-r-sources.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/src/org.eclipse.jgit.http.server-2.2.0.201212191850-r-sources.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
resources/folder_star_16x16.png
resources/folder_star_32x32.png
resources/login_nl.mkd
New file
@@ -0,0 +1,3 @@
## Aanmelden aub
Vul aub uw aanmeldgegevens in voor toegang tot deze Gitblit site.
resources/login_zh_CN.mkd
New file
@@ -0,0 +1,3 @@
## 请登录
请输入身份信息以登陆Gitblit。
resources/star_16x16.png
resources/star_32x32.png
resources/welcome_nl.mkd
New file
@@ -0,0 +1,3 @@
## Welkom bij Gitblit
Een snelle en makkelijke manier voor het hosten en bekijken van uw eigen [Git](http://www.git-scm.com) repositories.
resources/welcome_zh_CN.mkd
New file
@@ -0,0 +1,3 @@
## 欢迎访问 Gitblit
快速便捷的 [Git](http://www.git-scm.com) 版本库管理方案。
src/WEB-INF/web.xml
@@ -3,6 +3,30 @@
    xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <!-- The base folder is used to specify the root location of your Gitblit data.
            ${baseFolder}/gitblit.properties
            ${baseFolder}/users.conf
            ${baseFolder}/projects.conf
            ${baseFolder}/robots.txt
            ${baseFolder}/git
            ${baseFolder}/groovy
            ${baseFolder}/groovy/grape
            ${baseFolder}/proposals
        By default, this location is WEB-INF/data.  It is recommended to set this
        path to a location outside your webapps folder that is writable by your
        servlet container.  Gitblit will copy the WEB-INF/data files to that
        location for you when it restarts.  This approach makes upgrading simpler.
        All you have to do is set this parameter for the new release and then
        review the defaults for any new settings.  Settings are always versioned
        with a SINCE x.y.z attribute and also noted in the release changelog.
        -->
    <context-param>
        <param-name>baseFolder</param-name>
        <param-value>${contextFolder}/WEB-INF/data</param-value>
    </context-param>
    <!-- PARAMS --> 
     
    <!-- Gitblit Context Listener --><!-- STRIP     
src/com/gitblit/ConfigUserService.java
@@ -409,6 +409,10 @@
            // Read realm file
            read();
            UserModel model = users.remove(username.toLowerCase());
            if (model == null) {
                // user does not exist
                return false;
            }
            // remove user from team
            for (TeamModel team : model.teams) {
                TeamModel t = teams.get(team.name);
src/com/gitblit/Constants.java
@@ -34,7 +34,7 @@
    // The build script extracts this exact line so be careful editing it
    // and only use A-Z a-z 0-9 .-_ in the string.
    public static final String VERSION = "1.2.0-SNAPSHOT";
    public static final String VERSION = "1.3.0-SNAPSHOT";
    // The build script extracts this exact line so be careful editing it
    // and only use A-Z a-z 0-9 .-_ in the string.
@@ -42,7 +42,7 @@
    // The build script extracts this exact line so be careful editing it
    // and only use A-Z a-z 0-9 .-_ in the string.
    public static final String JGIT_VERSION = "JGit 2.1.0 (201209190230-r)";
    public static final String JGIT_VERSION = "JGit 2.2.0 (201212191850-r)";
    public static final String ADMIN_ROLE = "#admin";
    
@@ -88,10 +88,18 @@
    
    public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ssZ";
    
    public static final String R_GITBLIT = "refs/gitblit/";
    public static final String baseFolder = "baseFolder";
    public static final String baseFolder$ = "${" + baseFolder + "}";
    public static final String contextFolder$ = "${contextFolder}";
    public static String getGitBlitVersion() {
        return NAME + " v" + VERSION;
    }
    /**
     * Enumeration representing the four access restriction levels.
     */
@@ -405,6 +413,14 @@
            return ordinal() <= COOKIE.ordinal();
        }
    }
    public static enum AccountType {
        LOCAL, LDAP, REDMINE;
        public boolean isLocal() {
            return this == LOCAL;
        }
    }
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
src/com/gitblit/FederationClient.java
@@ -76,7 +76,7 @@
        }
        // configure the Gitblit singleton for minimal, non-server operation
        GitBlit.self().configureContext(settings, false);
        GitBlit.self().configureContext(settings, null, false);
        FederationPullExecutor executor = new FederationPullExecutor(registrations, params.isDaemon);
        executor.run();
        if (!params.isDaemon) {
src/com/gitblit/FileSettings.java
@@ -104,7 +104,7 @@
    }
    
    private String regExEscape(String input) {
        return input.replace(".", "\\.");
        return input.replace(".", "\\.").replace("$", "\\$").replace("{", "\\{");
    }
    /**
src/com/gitblit/GitBlit.java
@@ -85,6 +85,9 @@
import com.gitblit.Constants.FederationToken;
import com.gitblit.Constants.PermissionType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.fanout.FanoutNioService;
import com.gitblit.fanout.FanoutService;
import com.gitblit.fanout.FanoutSocketService;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.FederationSet;
@@ -154,8 +157,14 @@
    private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>();
    
    private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
    private final ObjectCache<String> projectMarkdownCache = new ObjectCache<String>();
    private final ObjectCache<String> projectRepositoriesMarkdownCache = new ObjectCache<String>();
    private ServletContext servletContext;
    private File baseFolder;
    private File repositoriesFolder;
@@ -176,6 +185,8 @@
    private TimeZone timezone;
    
    private FileBasedConfig projectConfigs;
    private FanoutService fanoutService;
    public GitBlit() {
        if (gitblit == null) {
@@ -385,12 +396,8 @@
     * @return the file
     */
    public static File getFileOrFolder(String fileOrFolder) {
        String openShift = System.getenv("OPENSHIFT_DATA_DIR");
        if (!StringUtils.isEmpty(openShift)) {
            // running on RedHat OpenShift
            return new File(openShift, fileOrFolder);
        }
        return new File(fileOrFolder);
        return com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$,
                self().baseFolder, fileOrFolder);
    }
    /**
@@ -400,7 +407,7 @@
     * @return the repositories folder path
     */
    public static File getRepositoriesFolder() {
        return getFileOrFolder(Keys.git.repositoriesFolder, "git");
        return getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
    }
    /**
@@ -410,7 +417,7 @@
     * @return the proposals folder path
     */
    public static File getProposalsFolder() {
        return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
        return getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
    }
    /**
@@ -420,9 +427,9 @@
     * @return the Groovy scripts folder path
     */
    public static File getGroovyScriptsFolder() {
        return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
        return getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy");
    }
    /**
     * Updates the list of server settings.
     * 
@@ -467,36 +474,48 @@
        this.userService.setup(settings);
    }
    
    public boolean supportsAddUser() {
        return supportsCredentialChanges(new UserModel(""));
    }
    /**
     * Returns true if the user's credentials can be changed.
     * 
     * @param user
     * @return true if the user service supports credential changes
     */
    public boolean supportsCredentialChanges() {
        return userService.supportsCredentialChanges();
    public boolean supportsCredentialChanges(UserModel user) {
        return (user != null && user.isLocalAccount()) || userService.supportsCredentialChanges();
    }
    /**
     * Returns true if the user's display name can be changed.
     * 
     * @param user
     * @return true if the user service supports display name changes
     */
    public boolean supportsDisplayNameChanges() {
        return userService.supportsDisplayNameChanges();
    public boolean supportsDisplayNameChanges(UserModel user) {
        return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();
    }
    /**
     * Returns true if the user's email address can be changed.
     * 
     * @param user
     * @return true if the user service supports email address changes
     */
    public boolean supportsEmailAddressChanges() {
        return userService.supportsEmailAddressChanges();
    public boolean supportsEmailAddressChanges(UserModel user) {
        return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();
    }
    /**
     * Returns true if the user's team memberships can be changed.
     * 
     * @param user
     * @return true if the user service supports team membership changes
     */
    public boolean supportsTeamMembershipChanges() {
        return userService.supportsTeamMembershipChanges();
    public boolean supportsTeamMembershipChanges(UserModel user) {
        return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
    }
    /**
@@ -785,6 +804,10 @@
     * @return the effective list of permissions for the user
     */
    public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
        if (StringUtils.isEmpty(user.username)) {
            // new user
            return new ArrayList<RegistrantAccessPermission>();
        }
        Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();
        set.addAll(user.getRepositoryPermissions());
        // Flag missing repositories
@@ -916,14 +939,14 @@
            for (RepositoryModel model : getRepositoryModels(user)) {
                if (model.isUsersPersonalRepository(username)) {
                    // personal repository
                    model.owner = user.username;
                    model.addOwner(user.username);
                    String oldRepositoryName = model.name;
                    model.name = "~" + user.username + model.name.substring(model.projectPath.length());
                    model.projectPath = "~" + user.username;
                    updateRepositoryModel(oldRepositoryName, model, false);
                } else if (model.isOwner(username)) {
                    // common/shared repo
                    model.owner = user.username;
                    model.addOwner(user.username);
                    updateRepositoryModel(model.name, model, false);
                }
            }
@@ -1339,7 +1362,7 @@
        }
        // check for updates
        Repository r = getRepository(repositoryName);
        Repository r = getRepository(model.name);
        if (r == null) {
            // repository is missing
            removeFromCachedRepositoryList(repositoryName);
@@ -1351,8 +1374,8 @@
        if (config.isOutdated()) {
            // reload model
            logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
            model = loadRepositoryModel(repositoryName);
            removeFromCachedRepositoryList(repositoryName);
            model = loadRepositoryModel(model.name);
            removeFromCachedRepositoryList(model.name);
            addToCachedRepositoryList(model);
        } else {
            // update a few repository parameters 
@@ -1402,7 +1425,30 @@
                }
                project.title = projectConfigs.getString("project", name, "title");
                project.description = projectConfigs.getString("project", name, "description");
                configs.put(name.toLowerCase(), project);
                // project markdown
                File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/project.mkd");
                if (pmkd.exists()) {
                    Date lm = new Date(pmkd.lastModified());
                    if (!projectMarkdownCache.hasCurrent(name, lm)) {
                        String mkd = com.gitblit.utils.FileUtils.readContent(pmkd,  "\n");
                        projectMarkdownCache.updateObject(name, lm, mkd);
                    }
                    project.projectMarkdown = projectMarkdownCache.getObject(name);
                }
                // project repositories markdown
                File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/repositories.mkd");
                if (rmkd.exists()) {
                    Date lm = new Date(rmkd.lastModified());
                    if (!projectRepositoriesMarkdownCache.hasCurrent(name, lm)) {
                        String mkd = com.gitblit.utils.FileUtils.readContent(rmkd,  "\n");
                        projectRepositoriesMarkdownCache.updateObject(name, lm, mkd);
                    }
                    project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(name);
                }
                configs.put(name.toLowerCase(), project);
            }
            projectCache.clear();
            projectCache.putAll(configs);
@@ -1526,6 +1572,49 @@
    }
    
    /**
     * Returns the list of project models that are referenced by the supplied
     * repository model    list.  This is an alternative method exists to ensure
     * Gitblit does not call getRepositoryModels(UserModel) twice in a request.
     *
     * @param repositoryModels
     * @param includeUsers
     * @return a list of project models
     */
    public List<ProjectModel> getProjectModels(List<RepositoryModel> repositoryModels, boolean includeUsers) {
        Map<String, ProjectModel> projects = new LinkedHashMap<String, ProjectModel>();
        for (RepositoryModel repository : repositoryModels) {
            if (!includeUsers && repository.isPersonalRepository()) {
                // exclude personal repositories
                continue;
            }
            if (!projects.containsKey(repository.projectPath)) {
                ProjectModel project = getProjectModel(repository.projectPath);
                if (project == null) {
                    logger.warn(MessageFormat.format("excluding project \"{0}\" from project list because it is empty!",
                            repository.projectPath));
                    continue;
                }
                projects.put(repository.projectPath, project);
                // clear the repo list in the project because that is the system
                // list, not the user-accessible list and start building the
                // user-accessible list
                project.repositories.clear();
                project.repositories.add(repository.name);
                project.lastChange = repository.lastChange;
            } else {
                // update the user-accessible list
                // this is used for repository count
                ProjectModel project = projects.get(repository.projectPath);
                project.repositories.add(repository.name);
                if (project.lastChange.before(repository.lastChange)) {
                    project.lastChange = repository.lastChange;
                }
            }
        }
        return new ArrayList<ProjectModel>(projects.values());
    }
    /**
     * Workaround JGit.  I need to access the raw config object directly in order
     * to see if the config is dirty so that I can reload a repository model.
     * If I use the stock JGit method to get the config it already reloads the
@@ -1561,7 +1650,7 @@
        }
        RepositoryModel model = new RepositoryModel();
        model.isBare = r.isBare();
        File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "git");
        File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
        if (model.isBare) {
            model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
        } else {
@@ -1576,7 +1665,7 @@
        
        if (config != null) {
            model.description = getConfig(config, "description", "");
            model.owner = getConfig(config, "owner", "");
            model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", "")));
            model.useTickets = getConfig(config, "useTickets", false);
            model.useDocs = getConfig(config, "useDocs", false);
            model.allowForks = getConfig(config, "allowForks", true);
@@ -1624,6 +1713,7 @@
        }
        model.HEAD = JGitUtils.getHEADRef(r);
        model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
        model.sparkleshareId = JGitUtils.getSparkleshareId(r);
        r.close();
        
        if (model.origin != null && model.origin.startsWith("file://")) {
@@ -1652,7 +1742,18 @@
     * @return true if the repository exists
     */
    public boolean hasRepository(String repositoryName) {
        if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
        return hasRepository(repositoryName, false);
    }
    /**
     * Determines if this server has the requested repository.
     *
     * @param name
     * @param caseInsensitive
     * @return true if the repository exists
     */
    public boolean hasRepository(String repositoryName, boolean caseSensitiveCheck) {
        if (!caseSensitiveCheck && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
            // if we are caching use the cache to determine availability
            // otherwise we end up adding a phantom repository to the cache
            return repositoryListCache.containsKey(repositoryName.toLowerCase());
@@ -1728,7 +1829,7 @@
            ProjectModel project = getProjectModel(userProject);
            for (String repository : project.repositories) {
                if (repository.startsWith(userProject)) {
                    RepositoryModel model = repositoryListCache.get(repository);
                    RepositoryModel model = getRepositoryModel(repository);
                    if (model.originRepository.equalsIgnoreCase(origin)) {
                        // user has a fork
                        return model.name;
@@ -1749,24 +1850,53 @@
     */
    public ForkModel getForkNetwork(String repository) {
        if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
            // find the root
            // find the root, cached
            RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
            while (model.originRepository != null) {
                model = repositoryListCache.get(model.originRepository);
            }
            ForkModel root = getForkModelFromCache(model.name);
            return root;
        } else {
            // find the root, non-cached
            RepositoryModel model = getRepositoryModel(repository.toLowerCase());
            while (model.originRepository != null) {
                model = getRepositoryModel(model.originRepository);
            }
            ForkModel root = getForkModel(model.name);
            return root;
        }
        return null;
    }
    private ForkModel getForkModelFromCache(String repository) {
        RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
        if (model == null) {
            return null;
        }
        ForkModel fork = new ForkModel(model);
        if (!ArrayUtils.isEmpty(model.forks)) {
            for (String aFork : model.forks) {
                ForkModel fm = getForkModelFromCache(aFork);
                if (fm != null) {
                    fork.forks.add(fm);
                }
            }
        }
        return fork;
    }
    
    private ForkModel getForkModel(String repository) {
        RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
        RepositoryModel model = getRepositoryModel(repository.toLowerCase());
        if (model == null) {
            return null;
        }
        ForkModel fork = new ForkModel(model);
        if (!ArrayUtils.isEmpty(model.forks)) {
            for (String aFork : model.forks) {
                ForkModel fm = getForkModel(aFork);
                fork.forks.add(fm);
                if (fm != null) {
                    fork.forks.add(fm);
                }
            }
        }
        return fork;
@@ -1937,7 +2067,7 @@
            if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
                repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
            }
            if (new File(repositoriesFolder, repository.name).exists()) {
            if (hasRepository(repository.name)) {
                throw new GitBlitException(MessageFormat.format(
                        "Can not create repository ''{0}'' because it already exists.",
                        repository.name));
@@ -2053,7 +2183,7 @@
    public void updateConfiguration(Repository r, RepositoryModel repository) {
        StoredConfig config = r.getConfig();
        config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
        config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
        config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners));
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
        config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
@@ -2911,12 +3041,15 @@
     * 
     * @param settings
     */
    public void configureContext(IStoredSettings settings, boolean startFederation) {
        logger.info("Reading configuration from " + settings.toString());
    public void configureContext(IStoredSettings settings, File folder, boolean startFederation) {
        this.settings = settings;
        this.baseFolder = folder;
        repositoriesFolder = getRepositoriesFolder();
        logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
        logger.info("Gitblit base folder     = " + folder.getAbsolutePath());
        logger.info("Git repositories folder = " + repositoriesFolder.getAbsolutePath());
        logger.info("Gitblit settings        = " + settings.toString());
        // prepare service executors
        mailExecutor = new MailExecutor(settings);
@@ -2938,7 +3071,7 @@
        serverStatus = new ServerStatus(isGO());
        if (this.userService == null) {
            String realm = settings.getString(Keys.realm.userService, "users.properties");
            String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
            IUserService loginService = null;
            try {
                // check to see if this "file" is a login service class
@@ -2951,7 +3084,7 @@
        }
        
        // load and cache the project metadata
        projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect());
        projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "${baseFolder}/projects.conf"), FS.detect());
        getProjectConfigs();
        
        // schedule mail engine
@@ -3017,6 +3150,32 @@
        }
        ContainerUtils.CVE_2007_0450.test();
        // startup Fanout PubSub service
        if (settings.getInteger(Keys.fanout.port, 0) > 0) {
            String bindInterface = settings.getString(Keys.fanout.bindInterface, null);
            int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT);
            boolean useNio = settings.getBoolean(Keys.fanout.useNio, true);
            int limit = settings.getInteger(Keys.fanout.connectionLimit, 0);
            if (useNio) {
                if (StringUtils.isEmpty(bindInterface)) {
                    fanoutService = new FanoutNioService(port);
                } else {
                    fanoutService = new FanoutNioService(bindInterface, port);
                }
            } else {
                if (StringUtils.isEmpty(bindInterface)) {
                    fanoutService = new FanoutSocketService(port);
                } else {
                    fanoutService = new FanoutSocketService(bindInterface, port);
                }
            }
            fanoutService.setConcurrentConnectionLimit(limit);
            fanoutService.setAllowAllChannelAnnouncements(false);
            fanoutService.start();
        }
    }
    
    private void logTimezone(String type, TimeZone zone) {
@@ -3040,39 +3199,63 @@
    public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) {
        servletContext = contextEvent.getServletContext();
        if (settings == null) {
            // Gitblit WAR is running in a servlet container
            // Gitblit is running in a servlet container
            ServletContext context = contextEvent.getServletContext();
            WebXmlSettings webxmlSettings = new WebXmlSettings(context);
            // gitblit.properties file located within the webapp
            String webProps = context.getRealPath("/WEB-INF/gitblit.properties");
            if (!StringUtils.isEmpty(webProps)) {
                File overrideFile = new File(webProps);
                webxmlSettings.applyOverrides(overrideFile);
            }
            File contextFolder = new File(context.getRealPath("/"));
            String openShift = System.getenv("OPENSHIFT_DATA_DIR");
            
            // gitblit.properties file located outside the deployed war
            // folder lie, for example, on RedHat OpenShift.
            File overrideFile = getFileOrFolder("gitblit.properties");
            if (!overrideFile.getPath().equals("gitblit.properties")) {
                webxmlSettings.applyOverrides(overrideFile);
            }
            configureContext(webxmlSettings, true);
            if (!StringUtils.isEmpty(openShift)) {
                // Gitblit is running in OpenShift/JBoss
                File base = new File(openShift);
            // Copy the included scripts to the configured groovy folder
            File localScripts = getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
            if (!localScripts.exists()) {
                File includedScripts = new File(context.getRealPath("/WEB-INF/groovy"));
                if (!includedScripts.equals(localScripts)) {
                    try {
                        com.gitblit.utils.FileUtils.copy(localScripts, includedScripts.listFiles());
                    } catch (IOException e) {
                        logger.error(MessageFormat.format(
                                "Failed to copy included Groovy scripts from {0} to {1}",
                                includedScripts, localScripts));
                // gitblit.properties setting overrides
                File overrideFile = new File(base, "gitblit.properties");
                webxmlSettings.applyOverrides(overrideFile);
                // Copy the included scripts to the configured groovy folder
                File localScripts = new File(base, webxmlSettings.getString(Keys.groovy.scriptsFolder, "groovy"));
                if (!localScripts.exists()) {
                    File warScripts = new File(contextFolder, "/WEB-INF/data/groovy");
                    if (!warScripts.equals(localScripts)) {
                        try {
                            com.gitblit.utils.FileUtils.copy(localScripts, warScripts.listFiles());
                        } catch (IOException e) {
                            logger.error(MessageFormat.format(
                                    "Failed to copy included Groovy scripts from {0} to {1}",
                                    warScripts, localScripts));
                        }
                    }
                }
                // configure context using the web.xml
                configureContext(webxmlSettings, base, true);
            } else {
                // Gitblit is running in a standard servlet container
                logger.info("WAR contextFolder is " + contextFolder.getAbsolutePath());
                String path = webxmlSettings.getString(Constants.baseFolder, Constants.contextFolder$ + "/WEB-INF/data");
                File base = com.gitblit.utils.FileUtils.resolveParameter(Constants.contextFolder$, contextFolder, path);
                base.mkdirs();
                // try to copy the data folder contents to the baseFolder
                File localSettings = new File(base, "gitblit.properties");
                if (!localSettings.exists()) {
                    File contextData = new File(contextFolder, "/WEB-INF/data");
                    if (!base.equals(contextData)) {
                        try {
                            com.gitblit.utils.FileUtils.copy(base, contextData.listFiles());
                        } catch (IOException e) {
                            logger.error(MessageFormat.format(
                                    "Failed to copy included data from {0} to {1}",
                                contextData, base));
                        }
                    }
                }
                // delegate all config to baseFolder/gitblit.properties file
                FileSettings settings = new FileSettings(localSettings.getAbsolutePath());
                configureContext(settings, base, true);
            }
        }
        
@@ -3090,6 +3273,9 @@
        scheduledExecutor.shutdownNow();
        luceneExecutor.close();
        gcExecutor.close();
        if (fanoutService != null) {
            fanoutService.stop();
        }
    }
    
    /**
@@ -3134,15 +3320,17 @@
        // create a Gitblit repository model for the clone
        RepositoryModel cloneModel = repository.cloneAs(cloneName);
        // owner has REWIND/RW+ permissions
        cloneModel.owner = user.username;
        cloneModel.addOwner(user.username);
        updateRepositoryModel(cloneName, cloneModel, false);
        // add the owner of the source repository to the clone's access list
        if (!StringUtils.isEmpty(repository.owner)) {
            UserModel originOwner = getUserModel(repository.owner);
            if (originOwner != null) {
                originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
                updateUserModel(originOwner.username, originOwner, false);
        if (!ArrayUtils.isEmpty(repository.owners)) {
            for (String owner : repository.owners) {
                UserModel originOwner = getUserModel(owner);
                if (originOwner != null) {
                    originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
                    updateUserModel(originOwner.username, originOwner, false);
                }
            }
        }
src/com/gitblit/GitBlitServer.java
@@ -84,10 +84,29 @@
    private static Logger logger;
    public static void main(String... args) {
        // filter out the baseFolder parameter
        List<String> filtered = new ArrayList<String>();
        String folder = "data";
        for (int i = 0; i< args.length; i++) {
            String arg = args[i];
            if (arg.equals("--baseFolder")) {
                if (i + 1 == args.length) {
                    System.out.println("Invalid --baseFolder parameter!");
                    System.exit(-1);
                } else if (args[i + 1] != ".") {
                    folder = args[i + 1];
                }
                i = i + 1;
            } else {
                filtered.add(arg);
            }
        }
        Params.baseFolder = folder;
        Params params = new Params();
        JCommander jc = new JCommander(params);
        try {
            jc.parse(args);
            jc.parse(filtered.toArray(new String[filtered.size()]));
            if (params.help) {
                usage(jc, null);
            }
@@ -147,13 +166,13 @@
     * Start Gitblit GO.
     */
    private static void start(Params params) {
        FileSettings settings = Params.FILESETTINGS;
        final File baseFolder = new File(Params.baseFolder).getAbsoluteFile();
        FileSettings settings = params.FILESETTINGS;
        if (!StringUtils.isEmpty(params.settingsfile)) {
            if (new File(params.settingsfile).exists()) {
                settings = new FileSettings(params.settingsfile);                
            }
        }
        logger = LoggerFactory.getLogger(GitBlitServer.class);
        logger.info(Constants.BORDER);
        logger.info("            _____  _  _    _      _  _  _");
@@ -197,11 +216,10 @@
        // conditionally configure the https connector
        if (params.securePort > 0) {
            final File folder = new File(System.getProperty("user.dir"));
            File certificatesConf = new File(folder, X509Utils.CA_CONFIG);
            File serverKeyStore = new File(folder, X509Utils.SERVER_KEY_STORE);
            File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
            File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);
            File certificatesConf = new File(baseFolder, X509Utils.CA_CONFIG);
            File serverKeyStore = new File(baseFolder, X509Utils.SERVER_KEY_STORE);
            File serverTrustStore = new File(baseFolder, X509Utils.SERVER_TRUST_STORE);
            File caRevocationList = new File(baseFolder, X509Utils.CA_REVOCATION_LIST);
            // generate CA & web certificates, create certificate stores
            X509Metadata metadata = new X509Metadata("localhost", params.storePassword);
@@ -218,12 +236,12 @@
            }
            
            metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
            X509Utils.prepareX509Infrastructure(metadata, folder, new X509Log() {
            X509Utils.prepareX509Infrastructure(metadata, baseFolder, new X509Log() {
                @Override
                public void log(String message) {
                    BufferedWriter writer = null;
                    try {
                        writer = new BufferedWriter(new FileWriter(new File(folder, X509Utils.CERTS + File.separator + "log.txt"), true));
                        writer = new BufferedWriter(new FileWriter(new File(baseFolder, X509Utils.CERTS + File.separator + "log.txt"), true));
                        writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
                        writer.newLine();
                        writer.flush();
@@ -277,7 +295,7 @@
        // tempDir is where the embedded Gitblit web application is expanded and
        // where Jetty creates any necessary temporary files
        File tempDir = new File(params.temp);
        File tempDir = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.temp);
        if (tempDir.exists()) {
            try {
                FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY);
@@ -361,7 +379,7 @@
        // Setup the GitBlit context
        GitBlit gitblit = GitBlit.self();
        gitblit.configureContext(settings, true);
        gitblit.configureContext(settings, baseFolder, true);
        rootContext.addEventListener(gitblit);
        try {
@@ -532,7 +550,9 @@
    @Parameters(separators = " ")
    private static class Params {
        private static final FileSettings FILESETTINGS = new FileSettings(Constants.PROPERTIES_FILE);
        public static String baseFolder;
        private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath());
        /*
         * Server parameters
@@ -551,14 +571,14 @@
         */
        @Parameter(names = { "--repositoriesFolder" }, description = "Git Repositories Folder")
        public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder,
                "repos");
                "git");
        /*
         * Authentication Parameters
         */
        @Parameter(names = { "--userService" }, description = "Authentication and Authorization Service (filename or fully qualified classname)")
        public String userService = FILESETTINGS.getString(Keys.realm.userService,
                "users.properties");
                "users.conf");
        /*
         * JETTY Parameters
@@ -567,10 +587,10 @@
        public Boolean useNIO = FILESETTINGS.getBoolean(Keys.server.useNio, true);
        @Parameter(names = "--httpPort", description = "HTTP port for to serve. (port <= 0 will disable this connector)")
        public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 80);
        public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 0);
        @Parameter(names = "--httpsPort", description = "HTTPS port to serve.  (port <= 0 will disable this connector)")
        public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 443);
        public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 8443);
        @Parameter(names = "--ajpPort", description = "AJP port to serve.  (port <= 0 will disable this connector)")
        public Integer ajpPort = FILESETTINGS.getInteger(Keys.server.ajpPort, 0);
src/com/gitblit/GitFilter.java
@@ -222,7 +222,7 @@
                // create repository
                RepositoryModel model = new RepositoryModel();
                model.name = repository;
                model.owner = user.username;
                model.addOwner(user.username);
                model.projectPath = StringUtils.getFirstPathElement(repository);
                if (model.isUsersPersonalRepository(user.username)) {
                    // personal repository, default to private for user
src/com/gitblit/GitServlet.java
@@ -18,17 +18,14 @@
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletConfig;
@@ -37,7 +34,9 @@
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
import org.eclipse.jgit.http.server.resolver.DefaultUploadPackFactory;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PostReceiveHook;
@@ -45,6 +44,8 @@
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceiveCommand.Result;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.RefFilter;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.slf4j.Logger;
@@ -55,7 +56,9 @@
import com.gitblit.models.UserModel;
import com.gitblit.utils.ClientLogger;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.IssueUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.PushLogUtils;
import com.gitblit.utils.StringUtils;
/**
@@ -83,7 +86,7 @@
        groovyDir = GitBlit.getGroovyScriptsFolder();
        try {
            // set Grape root
            File grapeRoot = new File(GitBlit.getString(Keys.groovy.grapeFolder, "groovy/grape")).getAbsoluteFile();
            File grapeRoot = GitBlit.getFileOrFolder(Keys.groovy.grapeFolder, "${baseFolder}/groovy/grape").getAbsoluteFile();
            grapeRoot.mkdirs();
            System.setProperty("grape.root", grapeRoot.getAbsolutePath());
            
@@ -124,7 +127,40 @@
                rp.setAllowDeletes(user.canDeleteRef(repository));
                rp.setAllowNonFastForwards(user.canRewindRef(repository));
                
                if (repository.isFrozen) {
                    throw new ServiceNotEnabledException();
                }
                return rp;
            }
        });
        // override the default upload pack to exclude gitblit refs
        setUploadPackFactory(new DefaultUploadPackFactory() {
            @Override
            public UploadPack create(final HttpServletRequest req, final Repository db)
                    throws ServiceNotEnabledException, ServiceNotAuthorizedException {
                UploadPack up = super.create(req, db);
                RefFilter refFilter = new RefFilter() {
                    @Override
                    public Map<String, Ref> filter(Map<String, Ref> refs) {
                        // admin accounts can access all refs
                        UserModel user = GitBlit.self().authenticate(req);
                        if (user == null) {
                            user = UserModel.ANONYMOUS;
                        }
                        if (user.canAdmin()) {
                            return refs;
                        }
                        // normal users can not clone gitblit refs
                        refs.remove(IssueUtils.GB_ISSUES);
                        refs.remove(PushLogUtils.GB_PUSHES);
                        return refs;
                    }
                };
                up.setRefFilter(refFilter);
                return up;
            }
        });
        super.init(new GitblitServletConfig(config));
@@ -244,9 +280,6 @@
                            .getName(), cmd.getResult(), cmd.getMessage()));
                }
            }
            // Experimental
            // runNativeScript(rp, "hooks/pre-receive", commands);
        }
        /**
@@ -260,12 +293,11 @@
                logger.info("skipping post-receive hooks, no refs created, updated, or removed");
                return;
            }
            RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
            scripts.addAll(repository.postReceiveScripts);
            UserModel user = getUserModel(rp);
            runGroovy(repository, user, commands, rp, scripts);
            RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
            // log ref changes
            for (ReceiveCommand cmd : commands) {
                if (Result.OK.equals(cmd.getResult())) {
                    // add some logging for important ref changes
@@ -284,9 +316,20 @@
                    }
                }
            }
            // update push log
            try {
                PushLogUtils.updatePushLog(user, rp.getRepository(), commands);
                logger.info(MessageFormat.format("{0} push log updated", repository.name));
            } catch (Exception e) {
                logger.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
            }
            
            // Experimental
            // runNativeScript(rp, "hooks/post-receive", commands);
            // run Groovy hook scripts
            Set<String> scripts = new LinkedHashSet<String>();
            scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
            scripts.addAll(repository.postReceiveScripts);
            runGroovy(repository, user, commands, rp, scripts);
        }
        /**
@@ -355,77 +398,6 @@
                } catch (Exception e) {
                    logger.error(
                            MessageFormat.format("Failed to execute Groovy script {0}", script), e);
                }
            }
        }
        /**
         * Runs the native push hook script.
         *
         * http://book.git-scm.com/5_git_hooks.html
         * http://longair.net/blog/2011/04/09/missing-git-hooks-documentation/
         *
         * @param rp
         * @param script
         * @param commands
         */
        @SuppressWarnings("unused")
        protected void runNativeScript(ReceivePack rp, String script,
                Collection<ReceiveCommand> commands) {
            Repository repository = rp.getRepository();
            File scriptFile = new File(repository.getDirectory(), script);
            int resultCode = 0;
            if (scriptFile.exists()) {
                try {
                    logger.debug("executing " + scriptFile);
                    Process process = Runtime.getRuntime().exec(scriptFile.getAbsolutePath(), null,
                            repository.getDirectory());
                    BufferedReader reader = new BufferedReader(new InputStreamReader(
                            process.getInputStream()));
                    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                            process.getOutputStream()));
                    for (ReceiveCommand command : commands) {
                        switch (command.getType()) {
                        case UPDATE:
                            // updating a ref
                            writer.append(MessageFormat.format("{0} {1} {2}\n", command.getOldId()
                                    .getName(), command.getNewId().getName(), command.getRefName()));
                            break;
                        case CREATE:
                            // new ref
                            // oldrev hard-coded to 40? weird.
                            writer.append(MessageFormat.format("40 {0} {1}\n", command.getNewId()
                                    .getName(), command.getRefName()));
                            break;
                        }
                    }
                    resultCode = process.waitFor();
                    // read and buffer stdin
                    // this is supposed to be piped back to the git client.
                    // not sure how to do that right now.
                    StringBuilder sb = new StringBuilder();
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        sb.append(line).append('\n');
                    }
                    logger.debug(sb.toString());
                } catch (Throwable e) {
                    resultCode = -1;
                    logger.error(
                            MessageFormat.format("Failed to execute {0}",
                                    scriptFile.getAbsolutePath()), e);
                }
            }
            // reject push
            if (resultCode != 0) {
                for (ReceiveCommand command : commands) {
                    command.setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format(
                            "Native script {0} rejected push or failed",
                            scriptFile.getAbsolutePath()));
                }
            }
        }
src/com/gitblit/GitblitUserService.java
@@ -23,9 +23,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccountType;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;
/**
 * This class wraps the default user service and is recommended as the starting
@@ -48,6 +50,8 @@
public class GitblitUserService implements IUserService {
    protected IUserService serviceImpl;
    protected final String ExternalAccount = "#externalAccount";
    private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);
@@ -56,7 +60,7 @@
    @Override
    public void setup(IStoredSettings settings) {
        File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf");
        File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "${baseFolder}/users.conf");
        serviceImpl = createUserService(realmFile);
        logger.info("GUS delegating to " + serviceImpl.toString());
    }
@@ -144,12 +148,16 @@
    @Override
    public UserModel authenticate(char[] cookie) {
        return serviceImpl.authenticate(cookie);
        UserModel user = serviceImpl.authenticate(cookie);
        setAccountType(user);
        return user;
    }
    @Override
    public UserModel authenticate(String username, char[] password) {
        return serviceImpl.authenticate(username, password);
        UserModel user = serviceImpl.authenticate(username, password);
        setAccountType(user);
        return user;
    }
    
    @Override
@@ -159,7 +167,9 @@
    @Override
    public UserModel getUserModel(String username) {
        return serviceImpl.getUserModel(username);
        UserModel user = serviceImpl.getUserModel(username);
        setAccountType(user);
        return user;
    }
    @Override
@@ -174,8 +184,8 @@
    @Override
    public boolean updateUserModel(String username, UserModel model) {
        if (supportsCredentialChanges()) {
            if (!supportsTeamMembershipChanges()) {
        if (model.isLocalAccount() || supportsCredentialChanges()) {
            if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
                //  teams are externally controlled - copy from original model
                UserModel existingModel = getUserModel(username);
                
@@ -188,7 +198,7 @@
        if (model.username.equals(username)) {
            // passwords are not persisted by the backing user service
            model.password = null;
            if (!supportsTeamMembershipChanges()) {
            if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
                //  teams are externally controlled- copy from original model
                UserModel existingModel = getUserModel(username);
                
@@ -218,7 +228,11 @@
    @Override
    public List<UserModel> getAllUsers() {
        return serviceImpl.getAllUsers();
        List<UserModel> users = serviceImpl.getAllUsers();
        for (UserModel user : users) {
            setAccountType(user);
        }
        return users;
    }
    @Override
@@ -300,4 +314,25 @@
    public boolean deleteRepositoryRole(String role) {
        return serviceImpl.deleteRepositoryRole(role);
    }
    protected boolean isLocalAccount(String username) {
        UserModel user = getUserModel(username);
        return user != null && user.isLocalAccount();
    }
    protected void setAccountType(UserModel user) {
        if (user != null) {
            if (!StringUtils.isEmpty(user.password)
                    && !ExternalAccount.equalsIgnoreCase(user.password)
                    && !"StoredInLDAP".equalsIgnoreCase(user.password)) {
                user.accountType = AccountType.LOCAL;
            } else {
                user.accountType = getAccountType();
            }
        }
    }
    protected AccountType getAccountType() {
        return AccountType.LOCAL;
    }
}
src/com/gitblit/LdapUserService.java
@@ -25,6 +25,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccountType;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
@@ -50,9 +51,9 @@
public class LdapUserService extends GitblitUserService {
    public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
    private IStoredSettings settings;
    private IStoredSettings settings;
    public LdapUserService() {
        super();
    }
@@ -60,7 +61,7 @@
    @Override
    public void setup(IStoredSettings settings) {
        this.settings = settings;
        String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf");
        String file = settings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");
        File realmFile = GitBlit.getFileOrFolder(file);
        serviceImpl = createUserService(realmFile);
@@ -155,9 +156,19 @@
    public boolean supportsTeamMembershipChanges() {
        return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);
    }
    @Override
    protected AccountType getAccountType() {
         return AccountType.LDAP;
    }
    @Override
    public UserModel authenticate(String username, char[] password) {
        if (isLocalAccount(username)) {
            // local account, bypass LDAP authentication
            return super.authenticate(username, password);
        }
        String simpleUsername = getSimpleUsername(username);
        
        LDAPConnection ldapConnection = getLdapConnection();
@@ -239,7 +250,8 @@
        setAdminAttribute(user);
        
        // Don't want visibility into the real password, make up a dummy
        user.password = "StoredInLDAP";
        user.password = ExternalAccount;
        user.accountType = getAccountType();
        
        // Get full name Attribute
        String displayName = settings.getString(Keys.realm.ldap.displayName, "");        
src/com/gitblit/PagesServlet.java
@@ -170,7 +170,7 @@
                        content = JGitUtils.getStringContent(r, tree, resource, encodings).getBytes(
                                Constants.ENCODING);
                    } else {
                        content = JGitUtils.getByteContent(r, tree, resource);
                        content = JGitUtils.getByteContent(r, tree, resource, false);
                    }
                    response.setContentType(contentType);
                } catch (Exception e) {
src/com/gitblit/RedmineUserService.java
@@ -9,7 +9,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccountType;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ConnectionUtils;
import com.gitblit.utils.StringUtils;
import com.google.gson.Gson;
@@ -45,7 +47,7 @@
    public void setup(IStoredSettings settings) {
        this.settings = settings;
        String file = settings.getString(Keys.realm.redmine.backingUserService, "users.conf");
        String file = settings.getString(Keys.realm.redmine.backingUserService, "${baseFolder}/users.conf");
        File realmFile = GitBlit.getFileOrFolder(file);
        serviceImpl = createUserService(realmFile);
@@ -71,54 +73,110 @@
    public boolean supportsTeamMembershipChanges() {
        return false;
    }
     @Override
    protected AccountType getAccountType() {
        return AccountType.REDMINE;
    }
    @Override
    public UserModel authenticate(String username, char[] password) {
        String urlText = this.settings.getString(Keys.realm.redmine.url, "");
        if (!urlText.endsWith("/")) {
            urlText.concat("/");
        }
        String apiKey = String.valueOf(password);
        if (isLocalAccount(username)) {
            // local account, bypass Redmine authentication
            return super.authenticate(username, password);
        }
        String jsonString = null;
        try {
            String jsonString = getCurrentUserAsJson(urlText, apiKey);
            RedmineCurrent current = new Gson().fromJson(jsonString, RedmineCurrent.class);
            String login = current.user.login;
            boolean canAdmin = true;
            // non admin user can not get login name
            if (StringUtils.isEmpty(login)) {
                canAdmin = false;
                login = current.user.mail;
            }
            UserModel userModel = new UserModel(login);
            userModel.canAdmin = canAdmin;
            userModel.displayName = current.user.firstname + " " + current.user.lastname;
            userModel.emailAddress = current.user.mail;
            userModel.cookie = StringUtils.getSHA1(userModel.username + new String(password));
            return userModel;
        } catch (IOException e) {
            logger.error("authenticate", e);
            // first attempt by username/password
            jsonString = getCurrentUserAsJson(username, password);
        } catch (Exception e1) {
            logger.warn("Failed to authenticate via username/password against Redmine");
            try {
                // second attempt is by apikey
                jsonString = getCurrentUserAsJson(null, password);
                username = null;
            } catch (Exception e2) {
                logger.error("Failed to authenticate via apikey against Redmine", e2);
                return null;
            }
        }
        return null;
        if (StringUtils.isEmpty(jsonString)) {
            logger.error("Received empty authentication response from Redmine");
            return null;
        }
        RedmineCurrent current = null;
        try {
            current = new Gson().fromJson(jsonString, RedmineCurrent.class);
        } catch (Exception e) {
            logger.error("Failed to deserialize Redmine json response: " + jsonString, e);
            return null;
        }
        if (StringUtils.isEmpty(username)) {
            // if the username has been reset because of apikey authentication
            // then use the email address of the user. this is the original
            // behavior as contributed by github/mallowlabs
            username = current.user.mail;
        }
        UserModel user = getUserModel(username);
        if (user == null)    // create user object for new authenticated user
            user = new UserModel(username.toLowerCase());
        // create a user cookie
        if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
            user.cookie = StringUtils.getSHA1(user.username + new String(password));
        }
        // update user attributes from Redmine
        user.accountType = getAccountType();
        user.displayName = current.user.firstname + " " + current.user.lastname;
        user.emailAddress = current.user.mail;
        user.password = ExternalAccount;
        if (!StringUtils.isEmpty(current.user.login)) {
            // only admin users can get login name
            // evidently this is an undocumented behavior of Redmine
            user.canAdmin = true;
        }
        // TODO consider Redmine group mapping for team membership
        // http://www.redmine.org/projects/redmine/wiki/Rest_Users
        // push the changes to the backing user service
        super.updateUserModel(user);
        return user;
    }
    private String getCurrentUserAsJson(String url, String apiKey) throws IOException {
    private String getCurrentUserAsJson(String username, char [] password) throws IOException {
        if (testingJson != null) { // for testing
            return testingJson;
        }
        String apiUrl = url + "users/current.json?key=" + apiKey;
        HttpURLConnection http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, null, null);
        String url = this.settings.getString(Keys.realm.redmine.url, "");
        if (!url.endsWith("/")) {
            url.concat("/");
        }
        HttpURLConnection http;
        if (username == null) {
            // apikey authentication
            String apiKey = String.valueOf(password);
            String apiUrl = url + "users/current.json?key=" + apiKey;
            http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, null, null);
        } else {
            // username/password BASIC authentication
            String apiUrl = url + "users/current.json";
            http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, username, password);
        }
        http.setRequestMethod("GET");
        http.connect();
        InputStreamReader reader = new InputStreamReader(http.getInputStream());
        return IOUtils.toString(reader);
    }
    /**
     * set json response. do NOT invoke from production code.
     * @param json json
@@ -126,5 +184,4 @@
    public void setTestingCurrentUserAsJson(String json) {
        this.testingJson = json;
    }
}
src/com/gitblit/RobotsTxtServlet.java
@@ -24,7 +24,6 @@
import javax.servlet.http.HttpServletResponse;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.StringUtils;
/**
 * Handles requests for robots.txt
@@ -55,13 +54,10 @@
    protected void processRequest(javax.servlet.http.HttpServletRequest request,
            javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
            java.io.IOException {
        String robotstxt = GitBlit.getString(Keys.web.robots.txt, null);
        File file = GitBlit.getFileOrFolder(Keys.web.robots.txt, null);
        String content = "";
        if (!StringUtils.isEmpty(robotstxt)) {
            File robotsfile = new File(robotstxt);
            if (robotsfile.exists()) {
                content = FileUtils.readContent(robotsfile, "\n");
            }
        if (file.exists()) {
            content = FileUtils.readContent(file, "\n");
        }
        response.getWriter().append(content);
    }
src/com/gitblit/authority/GitblitAuthority.java
@@ -138,6 +138,21 @@
    private JButton newSSLCertificate;
    public static void main(String... args) {
        // filter out the baseFolder parameter
        String folder = "data";
        for (int i = 0; i< args.length; i++) {
            String arg = args[i];
            if (arg.equals("--baseFolder")) {
                if (i + 1 == args.length) {
                    System.out.println("Invalid --baseFolder parameter!");
                    System.exit(-1);
                } else if (args[i + 1] != ".") {
                    folder = args[i+1];
                }
                break;
            }
        }
        final String baseFolder = folder;
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
@@ -145,7 +160,7 @@
                } catch (Exception e) {
                }
                GitblitAuthority authority = new GitblitAuthority();
                authority.initialize();
                authority.initialize(baseFolder);
                authority.setLocationRelativeTo(null);
                authority.setVisible(true);
            }
@@ -158,7 +173,7 @@
        defaultSorter = new TableRowSorter<UserCertificateTableModel>(tableModel);
    }
    
    public void initialize() {
    public void initialize(String baseFolder) {
        setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
        setTitle("Gitblit Certificate Authority v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")");
        setContentPane(getUI());
@@ -174,10 +189,10 @@
            }
        });        
        setSizeAndPosition();
        File folder = new File(System.getProperty("user.dir"));
        File folder = new File(baseFolder).getAbsoluteFile();
        load(folder);
        setSizeAndPosition();
    }
    
    private void setSizeAndPosition() {
@@ -230,7 +245,7 @@
    }
    
    private StoredConfig getConfig() throws IOException, ConfigInvalidException {
        File configFile  = new File(System.getProperty("user.dir"), X509Utils.CA_CONFIG);
        File configFile  = new File(folder, X509Utils.CA_CONFIG);
        FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
        config.load();
        return config;
@@ -243,30 +258,31 @@
        }
        gitblitSettings = new FileSettings(file.getAbsolutePath());
        mail = new MailExecutor(gitblitSettings);
        String us = gitblitSettings.getString(Keys.realm.userService, "users.conf");
        String us = gitblitSettings.getString(Keys.realm.userService, "${baseFolder}/users.conf");
        String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
        IUserService service = null;
        if (!ext.equals("conf") && !ext.equals("properties")) {
            if (us.equals("com.gitblit.LdapUserService")) {
                us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "users.conf");
                us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");
            } else if (us.equals("com.gitblit.LdapUserService")) {
                us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "users.conf");
                us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "${baseFolder}/users.conf");
            }
        }
        if (us.endsWith(".conf")) {
            service = new ConfigUserService(new File(us));
            service = new ConfigUserService(FileUtils.resolveParameter(Constants.baseFolder$, folder, us));
        } else {
            throw new RuntimeException("Unsupported user service: " + us);
        }
        
        service = new ConfigUserService(new File(us));
        service = new ConfigUserService(FileUtils.resolveParameter(Constants.baseFolder$, folder, us));
        return service;
    }
    
    private void load(File folder) {
        this.folder = folder;
        this.userService = loadUsers(folder);
        System.out.println(Constants.baseFolder$ + " set to " + folder);
        if (userService == null) {
            JOptionPane.showMessageDialog(this, MessageFormat.format("Sorry, {0} doesn't look like a Gitblit GO installation.", folder));
        } else {
src/com/gitblit/build/Build.java
@@ -109,6 +109,20 @@
        downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.RUNTIME);
        downloadFromApache(MavenObject.XZ, BuildType.RUNTIME);
        //needed for selenium ui tests
        downloadFromApacheToExtSelenium(MavenObject.SEL_API, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_FF, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_JAVA, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_REMOTE, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_SUPPORT, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.GUAVA, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.JSON, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.COMMONS_EXEC, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPCLIENT, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPCORE, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPMIME, BuildType.RUNTIME);
        downloadFromApacheToExtSelenium(MavenObject.COMMONS_LOGGING, BuildType.RUNTIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
        downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME);
    }
@@ -148,6 +162,20 @@
        downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.XZ, BuildType.COMPILETIME);
        //needed for selenium ui tests
        downloadFromApacheToExtSelenium(MavenObject.SEL_API, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_FF, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_JAVA, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_REMOTE, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.SEL_SUPPORT, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.GUAVA, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.JSON, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.COMMONS_EXEC, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPCLIENT, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPCORE, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.HTTPMIME, BuildType.COMPILETIME);
        downloadFromApacheToExtSelenium(MavenObject.COMMONS_LOGGING, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
@@ -217,7 +245,7 @@
        Properties properties = new Properties();
        FileInputStream is = null;
        try {
            is = new FileInputStream(Constants.PROPERTIES_FILE);
            is = new FileInputStream(new File("distrib", Constants.PROPERTIES_FILE));
            properties.load(is);
        } catch (Throwable t) {
            t.printStackTrace();
@@ -398,7 +426,7 @@
     *            the maven object to download.
     * @return
     */
    private static List<File> downloadFromMaven(String mavenRoot, MavenObject mo, BuildType type) {
    private static List<File> downloadFromMaven(String mavenRoot, MavenObject mo, BuildType type, String targetFolder) {
        List<File> downloads = new ArrayList<File>();
        String[] jars = { "" };
        if (BuildType.RUNTIME.equals(type)) {
@@ -407,9 +435,9 @@
            jars = new String[] { "-sources" };
        }
        for (String jar : jars) {
            File targetFile = mo.getLocalFile("ext", jar);
            File targetFile = mo.getLocalFile(targetFolder, jar);
            if ("-sources".equals(jar)) {
                File relocated = new File("ext/src", targetFile.getName());
                File relocated = new File(targetFolder+"/src", targetFile.getName());
                if (targetFile.exists()) {
                    // move -sources jar to ext/src folder
                    targetFile.renameTo(relocated);
@@ -500,6 +528,31 @@
            removeObsoleteArtifacts(mo, type, targetFile.getParentFile());
        }
        return downloads;
    }
    /**
     * Download a file from the official Apache Maven repository.
     *
     * @param mo
     *            the maven object to download.
     * @return
     */
    private static List<File> downloadFromApacheToExtSelenium(MavenObject mo,
            BuildType type) {
        return downloadFromMaven("http://repo1.maven.org/maven2/", mo, type,
                "ext/seleniumhq");
    }
    /**
     * Download a file from the official Apache Maven repository.
     *
     * @param mo
     *            the maven object to download.
     * @return
     */
    private static List<File> downloadFromMaven(String mavenRoot,
            MavenObject mo, BuildType type) {
        return downloadFromMaven(mavenRoot, mo, type, "ext");
    }
    
    private static void removeObsoleteArtifacts(final MavenObject mo, final BuildType type, File folder) {
@@ -675,17 +728,17 @@
                "");
        public static final MavenObject JGIT = new MavenObject(
                "JGit", "org/eclipse/jgit", "org.eclipse.jgit", "2.1.0.201209190230-r",
                "JGit", "org/eclipse/jgit", "org.eclipse.jgit", "2.2.0.201212191850-r",
                1600000, 1565000, 3460000,
                "5e7296d21645a479a1054fc96f3ec8469cede137",
                "5f492aaeae1beda2a31d1efa182f5d34e76d7b77",
                "97d0761b9dd618d1f9f6c16c35c3ddf045ba536c",
                "08dcf9546f4d61e1b8a50df5da5513006023b64b",
                "");
        public static final MavenObject JGIT_HTTP = new MavenObject(
                "JGit", "org/eclipse/jgit", "org.eclipse.jgit.http.server", "2.1.0.201209190230-r",
                "JGit", "org/eclipse/jgit", "org.eclipse.jgit.http.server", "2.2.0.201212191850-r",
                68000, 62000, 110000,
                "0bd9e5801c246d6f8ad9268d18c45ca9915f9a50",
                "210c434c38ddcf2126af250018d5845ea41ff502",
                "8ad4fc4fb9529d645249bb46ad7e54d98436cb65",
                "3385cf294957d1d34c1270b468853aea347b36ca",
                "");
        public static final MavenObject JSCH = new MavenObject(
@@ -796,6 +849,59 @@
                "ecff5cb8b1189514c9d1d8d68eb77ac372e000c9",
                "f95e32a5d2dd8da643c4419814415b9704312993", "");
        public static final MavenObject SEL_JAVA = new MavenObject(
                "selenium-java", "org/seleniumhq/selenium", "selenium-java",
                "2.28.0", 984098, 0, 0,
                "7606286989ac9cb942cc206d975ffe187c18d605", "4ede08d293dc153989a337cd0d31d26421433af5", "");
        public static final MavenObject SEL_API = new MavenObject(
                "selenium-api", "org/seleniumhq/selenium", "selenium-api",
                "2.28.0", 984098, 0, 0,
                "c4044c40fff65cd25135a5f443638a2b1ccaeac5", "35fc6ec0804ae32b16a56627e69bdcb69995c515", "");
        public static final MavenObject SEL_REMOTE = new MavenObject(
                "selenium-remote-driver", "org/seleniumhq/selenium",
                "selenium-remote-driver", "2.28.0", 984098, 0, 0,
                "c67f97cd94e02afec92b0ac881844febb4fc90be", "51a9c30de3c8c203cb7a474a10842443005a5fb4", "");
        public static final MavenObject SEL_SUPPORT = new MavenObject(
                "selenium-support", "org/seleniumhq/selenium",
                "selenium-support", "2.28.0", 984098, 0, 0,
                "caf68d6310425f583bc592c08e43066b35eb94f6", "ce3831a601f5f50fda2f4604decde409b6c735a7", "");
        public static final MavenObject SEL_FF = new MavenObject(
                "selenium-firefox-driver", "org/seleniumhq/selenium",
                "selenium-firefox-driver", "2.28.0", 984098, 0, 0,
                "a7c34e45dba39e65467b900aa67611aaa039692d", "aa8cd5fb49ca75a53d5b143406ea3d81ab3eddfd", "");
        public static final MavenObject GUAVA = new MavenObject("guava",
                "com/google/guava", "guava", "12.0", 984098, 0, 0,
                "5bc66dd95b79db1e437eb08adba124a3e4088dc0", "f8b98e61865bed3c39b978ee3bf5c7fb990c4032", "");
        public static final MavenObject JSON = new MavenObject("json",
                "org/json", "json", "20080701", 984098, 0, 0,
                "d652f102185530c93b66158b1859f35d45687258", "71bd54221e701df9d112bf9ba2918e13b0671f3a", "");
        public static final MavenObject COMMONS_EXEC = new MavenObject(
                "commons-exec", "org/apache/commons", "commons-exec", "1.1",
                984098, 0, 0, "07dfdf16fade726000564386825ed6d911a44ba1", "f60bea898e18b308099862e8634d589b06a8b0be",
                "");
        public static final MavenObject HTTPCORE = new MavenObject("httpcore",
                "org/apache/httpcomponents", "httpcore", "4.2.1", 984098, 0, 0,
                "2d503272bf0a8b5f92d64db78b4ba9abbaccc6fd", "3f6caf5334fa83607b82e2f32dd128a9d8a0ea5e", "");
        public static final MavenObject HTTPMIME = new MavenObject("httpmime",
                "org/apache/httpcomponents", "httpmime", "4.2.1", 984098, 0, 0,
                "7c772bace9aa31a728c39a88c6ff66a7cd177e89", "", "4e453843ae47f1c2d70e2eb2c13c037de4b614c4");
        public static final MavenObject HTTPCLIENT = new MavenObject(
                "httpclient", "org/apache/httpcomponents", "httpclient",
                "4.2.1", 984098, 0, 0,
                "b69bd03af60bf487b3ae1209a644ecac587bf6fc", "6b27312b9c28b59aaeb6c21f3490045690c703d3", "");
        public static final MavenObject COMMONS_LOGGING = new MavenObject(
                "commons-logging", "commons-logging", "commons-logging",
                "1.1.1", 984098, 0, 0,
                "5043bfebc3db072ed80fbd362e7caf00e885d8ae", "f3f156cbff0e0fb0d64bfce31a352cce4a33bc19", "");
        public final String name;
        public final String group;
        public final String artifact;
src/com/gitblit/build/BuildWebXml.java
@@ -60,44 +60,44 @@
    }
    private static void generateWebXml(Params params) throws Exception {
        StringBuilder parameters = new StringBuilder();
        // Read the current Gitblit properties
        BufferedReader propertiesReader = new BufferedReader(new FileReader(new File(
                params.propertiesFile)));
        if (params.propertiesFile != null) {
            BufferedReader propertiesReader = new BufferedReader(new FileReader(new File(
                    params.propertiesFile)));
        Vector<Setting> settings = new Vector<Setting>();
        List<String> comments = new ArrayList<String>();
        String line = null;
        while ((line = propertiesReader.readLine()) != null) {
            if (line.length() == 0) {
                comments.clear();
            } else {
                if (line.charAt(0) == '#') {
                    if (line.length() > 1) {
                        comments.add(line.substring(1).trim());
                    }
                } else {
                    String[] kvp = line.split("=", 2);
                    String key = kvp[0].trim();
                    if (!skipKey(key)) {
                        Setting s = new Setting(key, kvp[1].trim(), comments);
                        settings.add(s);
                    }
            Vector<Setting> settings = new Vector<Setting>();
            List<String> comments = new ArrayList<String>();
            String line = null;
            while ((line = propertiesReader.readLine()) != null) {
                if (line.length() == 0) {
                    comments.clear();
                } else {
                    if (line.charAt(0) == '#') {
                        if (line.length() > 1) {
                            comments.add(line.substring(1).trim());
                        }
                    } else {
                        String[] kvp = line.split("=", 2);
                        String key = kvp[0].trim();
                        if (!skipKey(key)) {
                            Setting s = new Setting(key, kvp[1].trim(), comments);
                            settings.add(s);
                        }
                        comments.clear();
                    }
                }
            }
        }
        propertiesReader.close();
            propertiesReader.close();
        StringBuilder parameters = new StringBuilder();
        for (Setting setting : settings) {
            for (String comment : setting.comments) {
                parameters.append(MessageFormat.format(COMMENT_PATTERN, comment));
            for (Setting setting : settings) {
                for (String comment : setting.comments) {
                    parameters.append(MessageFormat.format(COMMENT_PATTERN, comment));
                }
                parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name,
                        StringUtils.escapeForHtml(setting.value, false)));
            }
            parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name,
                    StringUtils.escapeForHtml(setting.value, false)));
        }
        // Read the prototype web.xml file
        File webxml = new File(params.sourceFile);
        char[] buffer = new char[(int) webxml.length()];
@@ -150,11 +150,11 @@
        @Parameter(names = { "--sourceFile" }, description = "Source web.xml file", required = true)
        public String sourceFile;
        @Parameter(names = { "--propertiesFile" }, description = "Properties settings file", required = true)
        @Parameter(names = { "--propertiesFile" }, description = "Properties settings file")
        public String propertiesFile;
        @Parameter(names = { "--destinationFile" }, description = "Destination web.xml file", required = true)
        public String destinationFile;
    }
}
src/com/gitblit/client/EditRepositoryDialog.java
@@ -38,7 +38,6 @@
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.ImageIcon;
import javax.swing.JButton;
@@ -117,7 +116,7 @@
    private JComboBox federationStrategy;
    private JComboBox ownerField;
    private JPalette<String> ownersPalette;
    private JComboBox headRefField;
    
@@ -126,7 +125,7 @@
    private JTextField gcThreshold;
    
    private JComboBox maxActivityCommits;
    private RegistrantPermissionsPanel usersPalette;
    private JPalette<String> setsPalette;
@@ -207,7 +206,7 @@
        gcThreshold = new JTextField(8);
        gcThreshold.setText(anRepository.gcThreshold);
        ownerField = new JComboBox();
        ownersPalette = new JPalette<String>(true);
        useTickets = new JCheckBox(Translation.get("gb.useTicketsDescription"),
                anRepository.useTickets);
@@ -334,10 +333,10 @@
        usersPalette = new RegistrantPermissionsPanel(RegistrantType.USER);
        JPanel northFieldsPanel = new JPanel(new GridLayout(0, 1, 0, 5));
        northFieldsPanel.add(newFieldPanel(Translation.get("gb.owner"), ownerField));
        JPanel northFieldsPanel = new JPanel(new BorderLayout(0, 5));
        northFieldsPanel.add(newFieldPanel(Translation.get("gb.owners"), ownersPalette), BorderLayout.NORTH);
        northFieldsPanel.add(newFieldPanel(Translation.get("gb.accessRestriction"),
                accessRestriction), BorderLayout.NORTH);
                accessRestriction), BorderLayout.CENTER);
        JPanel northAccessPanel = new JPanel(new BorderLayout(5, 5));
        northAccessPanel.add(northFieldsPanel, BorderLayout.NORTH);
@@ -556,8 +555,8 @@
        repository.name = rname;
        repository.description = descriptionField.getText();
        repository.owner = ownerField.getSelectedItem() == null ? null
                : ownerField.getSelectedItem().toString();
        repository.owners.clear();
        repository.owners.addAll(ownersPalette.getSelections());
        repository.HEAD = headRefField.getSelectedItem() == null ? null
                : headRefField.getSelectedItem().toString();
        repository.gcPeriod = (Integer) gcPeriod.getSelectedItem();
@@ -629,11 +628,8 @@
        this.allowNamed.setSelected(!authenticated);
    }
    public void setUsers(String owner, List<String> all, List<RegistrantAccessPermission> permissions) {
        ownerField.setModel(new DefaultComboBoxModel(all.toArray()));
        if (!StringUtils.isEmpty(owner)) {
            ownerField.setSelectedItem(owner);
        }
    public void setUsers(List<String> owners, List<String> all, List<RegistrantAccessPermission> permissions) {
        ownersPalette.setObjects(all, owners);
        usersPalette.setObjects(all, permissions);
    }
src/com/gitblit/client/GitblitClient.java
@@ -162,7 +162,7 @@
    }
    public boolean isOwner(RepositoryModel model) {
        return account != null && account.equalsIgnoreCase(model.owner);
        return model.isOwner(account);
    }
    public String getURL(String action, String repository, String objectId) {
src/com/gitblit/client/IndicatorsRenderer.java
@@ -55,6 +55,8 @@
    private final ImageIcon federatedIcon;
    
    private final ImageIcon forkIcon;
    private final ImageIcon sparkleshareIcon;
    public IndicatorsRenderer() {
        super(new FlowLayout(FlowLayout.RIGHT, 1, 0));
@@ -67,6 +69,7 @@
        frozenIcon = new ImageIcon(getClass().getResource("/cold_16x16.png"));
        federatedIcon = new ImageIcon(getClass().getResource("/federated_16x16.png"));
        forkIcon = new ImageIcon(getClass().getResource("/commit_divide_16x16.png"));
        sparkleshareIcon = new ImageIcon(getClass().getResource("/star_16x16.png"));
    }
    @Override
@@ -80,6 +83,11 @@
        if (value instanceof RepositoryModel) {
            StringBuilder tooltip = new StringBuilder();
            RepositoryModel model = (RepositoryModel) value;
            if (model.isSparkleshared()) {
                JLabel icon = new JLabel(sparkleshareIcon);
                tooltip.append(Translation.get("gb.isSparkleshared")).append("<br/>");
                add(icon);
            }
            if (model.isFork()) {
                JLabel icon = new JLabel(forkIcon);
                tooltip.append(Translation.get("gb.isFork")).append("<br/>");
src/com/gitblit/client/JPalette.java
@@ -144,7 +144,7 @@
        table.getColumn(table.getColumnName(0)).setCellRenderer(nameRenderer);
        JScrollPane jsp = new JScrollPane(table);
        jsp.setPreferredSize(new Dimension(225, 175));
        jsp.setPreferredSize(new Dimension(225, 160));
        JPanel panel = new JPanel(new BorderLayout());
        JLabel jlabel = new JLabel(label);
        jlabel.setFont(jlabel.getFont().deriveFont(Font.BOLD));
src/com/gitblit/client/RepositoriesPanel.java
@@ -49,8 +49,8 @@
import com.gitblit.Constants;
import com.gitblit.Constants.RpcRequest;
import com.gitblit.Keys;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.FeedModel;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.StringUtils;
@@ -453,7 +453,7 @@
        dialog.setLocationRelativeTo(RepositoriesPanel.this);
        List<String> usernames = gitblit.getUsernames();
        List<RegistrantAccessPermission> members = gitblit.getUserAccessPermissions(repository);
        dialog.setUsers(repository.owner, usernames, members);
        dialog.setUsers(new ArrayList<String>(repository.owners), usernames, members);
        dialog.setTeams(gitblit.getTeamnames(), gitblit.getTeamAccessPermissions(repository));
        dialog.setRepositories(gitblit.getRepositories());
        dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
src/com/gitblit/client/RepositoriesTableModel.java
@@ -23,6 +23,7 @@
import javax.swing.table.AbstractTableModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.ArrayUtils;
/**
 * Table model of a list of repositories.
@@ -111,7 +112,7 @@
        case Description:
            return model.description;
        case Owner:
            return model.owner;
            return ArrayUtils.toString(model.owners);
        case Indicators:
            return model;
        case Last_Change:
src/com/gitblit/client/UsersPanel.java
@@ -112,8 +112,8 @@
        String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
        table.getColumn(name).setCellRenderer(nameRenderer);
        
        int w = 125;
        name = table.getColumnName(UsersTableModel.Columns.AccessLevel.ordinal());
        int w = 130;
        name = table.getColumnName(UsersTableModel.Columns.Type.ordinal());
        table.getColumn(name).setMinWidth(w);
        table.getColumn(name).setMaxWidth(w);
        name = table.getColumnName(UsersTableModel.Columns.Teams.ordinal());
src/com/gitblit/client/UsersTableModel.java
@@ -36,7 +36,7 @@
    List<UserModel> list;
    enum Columns {
        Name, Display_Name, AccessLevel, Teams, Repositories;
        Name, Display_Name, Type, Teams, Repositories;
        @Override
        public String toString() {
@@ -71,8 +71,8 @@
            return Translation.get("gb.name");
        case Display_Name:
            return Translation.get("gb.displayName");
        case AccessLevel:
            return Translation.get("gb.accessLevel");
        case Type:
            return Translation.get("gb.type");
        case Teams:
            return Translation.get("gb.teamMemberships");
        case Repositories:
@@ -101,11 +101,18 @@
            return model.username;
        case Display_Name:
            return model.displayName;
        case AccessLevel:
            if (model.canAdmin()) {
                return "administrator";
        case Type:
            StringBuilder sb = new StringBuilder();
            if (model.accountType != null) {
                sb.append(model.accountType.name());
            }
            return "";
            if (model.canAdmin()) {
                if (sb.length() > 0) {
                    sb.append(", ");
                }
                sb.append("admin");
            }
            return sb.toString();
        case Teams:
            return (model.teams == null || model.teams.size() == 0) ? "" : String
                    .valueOf(model.teams.size());
src/com/gitblit/fanout/FanoutClient.java
New file
@@ -0,0 +1,413 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Fanout client class.
 *
 * @author James Moger
 *
 */
public class FanoutClient implements Runnable {
    private final static Logger logger = LoggerFactory.getLogger(FanoutClient.class);
    private final int clientTimeout = 500;
    private final int reconnectTimeout = 2000;
    private final String host;
    private final int port;
    private final List<FanoutListener> listeners;
    private String id;
    private volatile Selector selector;
    private volatile SocketChannel socketCh;
    private Thread clientThread;
    private final AtomicBoolean isConnected;
    private final AtomicBoolean isRunning;
    private final AtomicBoolean isAutomaticReconnect;
    private final ByteBuffer writeBuffer;
    private final ByteBuffer readBuffer;
    private final CharsetDecoder decoder;
    private final Set<String> subscriptions;
    private boolean resubscribe;
    public interface FanoutListener {
        public void pong(Date timestamp);
        public void announcement(String channel, String message);
    }
    public static class FanoutAdapter implements FanoutListener {
        public void pong(Date timestamp) { }
        public void announcement(String channel, String message) { }
    }
    public static void main(String args[]) throws Exception {
        FanoutClient client = new FanoutClient("localhost", 2000);
        client.addListener(new FanoutAdapter() {
            @Override
            public void pong(Date timestamp) {
                System.out.println("Pong. " + timestamp);
            }
            @Override
            public void announcement(String channel, String message) {
                System.out.println(MessageFormat.format("Here ye, Here ye. {0} says {1}", channel, message));
            }
        });
        client.start();
        Thread.sleep(5000);
        client.ping();
        client.subscribe("james");
        client.announce("james", "12345");
        client.subscribe("c52f99d16eb5627877ae957df7ce1be102783bd5");
        while (true) {
            Thread.sleep(10000);
            client.ping();
        }
    }
    public FanoutClient(String host, int port) {
        this.host = host;
        this.port = port;
        readBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH);
        writeBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH);
        decoder = Charset.forName(FanoutConstants.CHARSET).newDecoder();
        listeners = Collections.synchronizedList(new ArrayList<FanoutListener>());
        subscriptions = new LinkedHashSet<String>();
        isRunning = new AtomicBoolean(false);
        isConnected = new AtomicBoolean(false);
        isAutomaticReconnect = new AtomicBoolean(true);
    }
    public void addListener(FanoutListener listener) {
        listeners.add(listener);
    }
    public void removeListener(FanoutListener listener) {
        listeners.remove(listener);
    }
    public boolean isAutomaticReconnect() {
        return isAutomaticReconnect.get();
    }
    public void setAutomaticReconnect(boolean value) {
        isAutomaticReconnect.set(value);
    }
    public void ping() {
        confirmConnection();
        write("ping");
    }
    public void status() {
        confirmConnection();
        write("status");
    }
    public void subscribe(String channel) {
        confirmConnection();
        if (subscriptions.add(channel)) {
            write("subscribe " + channel);
        }
    }
    public void unsubscribe(String channel) {
        confirmConnection();
        if (subscriptions.remove(channel)) {
            write("unsubscribe " + channel);
        }
    }
    public void announce(String channel, String message) {
        confirmConnection();
        write("announce " + channel + " " + message);
    }
    private void confirmConnection() {
        if (!isConnected()) {
            throw new RuntimeException("Fanout client is disconnected!");
        }
    }
    public boolean isConnected() {
        return isRunning.get() && socketCh != null && isConnected.get();
    }
    /**
     * Start client connection and return immediately.
     */
    public void start() {
        if (isRunning.get()) {
            logger.warn("Fanout client is already running");
            return;
        }
        clientThread = new Thread(this, "Fanout client");
        clientThread.start();
    }
    /**
     * Start client connection and wait until it has connected.
     */
    public void startSynchronously() {
        start();
        while (!isConnected()) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }
    }
    /**
     * Stops client connection.  This method returns when the connection has
     * been completely shutdown.
     */
    public void stop() {
        if (!isRunning.get()) {
            logger.warn("Fanout client is not running");
            return;
        }
        isRunning.set(false);
        try {
            if (clientThread != null) {
                clientThread.join();
                clientThread = null;
            }
        } catch (InterruptedException e1) {
        }
    }
    @Override
    public void run() {
        resetState();
        isRunning.set(true);
        while (isRunning.get()) {
            // (re)connect
            if (socketCh == null) {
                try {
                    InetAddress addr = InetAddress.getByName(host);
                    socketCh = SocketChannel.open(new InetSocketAddress(addr, port));
                    socketCh.configureBlocking(false);
                    selector = Selector.open();
                    id = FanoutConstants.getLocalSocketId(socketCh.socket());
                    socketCh.register(selector, SelectionKey.OP_READ);
                } catch (Exception e) {
                    logger.error(MessageFormat.format("failed to open client connection to {0}:{1,number,0}", host, port), e);
                    try {
                        Thread.sleep(reconnectTimeout);
                    } catch (InterruptedException x) {
                    }
                    continue;
                }
            }
            // read/write
            try {
                selector.select(clientTimeout);
                Iterator<SelectionKey> i = selector.selectedKeys().iterator();
                while (i.hasNext()) {
                    SelectionKey key = i.next();
                    i.remove();
                    if (key.isReadable()) {
                        // read message
                        String content = read();
                        String[] lines = content.split("\n");
                        for (String reply : lines) {
                            logger.trace(MessageFormat.format("fanout client {0} received: {1}", id, reply));
                            if (!processReply(reply)) {
                                logger.error(MessageFormat.format("fanout client {0} received unknown message", id));
                            }
                        }
                    } else if (key.isWritable()) {
                        // resubscribe
                        if (resubscribe) {
                            resubscribe = false;
                            logger.info(MessageFormat.format("fanout client {0} re-subscribing to {1} channels", id, subscriptions.size()));
                            for (String subscription : subscriptions) {
                                write("subscribe " + subscription);
                            }
                        }
                        socketCh.register(selector, SelectionKey.OP_READ);
                    }
                }
            } catch (IOException e) {
                logger.error(MessageFormat.format("fanout client {0} error: {1}", id, e.getMessage()));
                closeChannel();
                if (!isAutomaticReconnect.get()) {
                    isRunning.set(false);
                    continue;
                }
            }
        }
        closeChannel();
        resetState();
    }
    protected void resetState() {
        readBuffer.clear();
        writeBuffer.clear();
        isRunning.set(false);
        isConnected.set(false);
    }
    private void closeChannel() {
        try {
            if (socketCh != null) {
                socketCh.close();
                socketCh = null;
                selector.close();
                selector = null;
                isConnected.set(false);
            }
        } catch (IOException x) {
        }
    }
    protected boolean processReply(String reply) {
        String[] fields = reply.split("!", 2);
        if (fields.length == 1) {
            try {
                long time = Long.parseLong(fields[0]);
                Date date = new Date(time);
                firePong(date);
            } catch (Exception e) {
            }
            return true;
        } else if (fields.length == 2) {
            String channel = fields[0];
            String message = fields[1];
            if (FanoutConstants.CH_DEBUG.equals(channel)) {
                // debug messages are for internal use
                if (FanoutConstants.MSG_CONNECTED.equals(message)) {
                    isConnected.set(true);
                    resubscribe = subscriptions.size() > 0;
                    if (resubscribe) {
                        try {
                            // register for async resubscribe
                            socketCh.register(selector, SelectionKey.OP_WRITE);
                        } catch (Exception e) {
                            logger.error("an error occurred", e);
                        }
                    }
                }
                logger.debug(MessageFormat.format("fanout client {0} < {1}", id, reply));
            } else {
                fireAnnouncement(channel, message);
            }
            return true;
        } else {
            // unknown message
            return false;
        }
    }
    protected void firePong(Date timestamp) {
        logger.info(MessageFormat.format("fanout client {0} < pong {1,date,yyyy-MM-dd HH:mm:ss}", id, timestamp));
        for (FanoutListener listener : listeners) {
            try {
                listener.pong(timestamp);
            } catch (Throwable t) {
                logger.error("FanoutListener threw an exception!", t);
            }
        }
    }
    protected void fireAnnouncement(String channel, String message) {
        logger.info(MessageFormat.format("fanout client {0} < announcement {1} {2}", id, channel, message));
        for (FanoutListener listener : listeners) {
            try {
                listener.announcement(channel, message);
            } catch (Throwable t) {
                logger.error("FanoutListener threw an exception!", t);
            }
        }
    }
    protected synchronized String read() throws IOException {
        readBuffer.clear();
        long len = socketCh.read(readBuffer);
        if (len == -1) {
            logger.error(MessageFormat.format("fanout client {0} lost connection to {1}:{2,number,0}, end of stream", id, host, port));
            socketCh.close();
            return null;
        } else {
            readBuffer.flip();
            String content = decoder.decode(readBuffer).toString();
            readBuffer.clear();
            return content;
        }
    }
    protected synchronized boolean write(String message) {
        try {
            logger.info(MessageFormat.format("fanout client {0} > {1}", id, message));
            byte [] bytes = message.getBytes(FanoutConstants.CHARSET);
            writeBuffer.clear();
            writeBuffer.put(bytes);
            if (bytes[bytes.length - 1] != 0xa) {
                writeBuffer.put((byte) 0xa);
            }
            writeBuffer.flip();
            // loop until write buffer has been completely sent
            long written = 0;
            long toWrite = writeBuffer.remaining();
            while (written != toWrite) {
                written += socketCh.write(writeBuffer);
                try {
                    Thread.sleep(10);
                } catch (Exception x) {
                }
            }
            return true;
        } catch (IOException e) {
            logger.error("fanout client {0} error: {1}", id, e.getMessage());
        }
        return false;
    }
}
src/com/gitblit/fanout/FanoutConstants.java
New file
@@ -0,0 +1,36 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.net.Socket;
public class FanoutConstants {
    public final static String CHARSET = "ISO-8859-1";
    public final static int BUFFER_LENGTH = 512;
    public final static String CH_ALL = "all";
    public final static String CH_DEBUG = "debug";
    public final static String MSG_CONNECTED = "connected...";
    public final static String MSG_BUSY = "busy";
    public static String getRemoteSocketId(Socket socket) {
        return socket.getInetAddress().getHostAddress() + ":" + socket.getPort();
    }
    public static String getLocalSocketId(Socket socket) {
        return socket.getInetAddress().getHostAddress() + ":" + socket.getLocalPort();
    }
}
src/com/gitblit/fanout/FanoutNioService.java
New file
@@ -0,0 +1,332 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * A single-thread NIO implementation of https://github.com/travisghansen/fanout
 *
 * This implementation uses channels and selectors, which are the Java analog of
 * the Linux epoll mechanism used in the original fanout C code.
 *
 * @author James Moger
 *
 */
public class FanoutNioService extends FanoutService {
    private final static Logger logger = LoggerFactory.getLogger(FanoutNioService.class);
    private volatile ServerSocketChannel serviceCh;
    private volatile Selector selector;
    public static void main(String[] args) throws Exception {
        FanoutNioService pubsub = new FanoutNioService(null, DEFAULT_PORT);
        pubsub.setStrictRequestTermination(false);
        pubsub.setAllowAllChannelAnnouncements(false);
        pubsub.start();
    }
    /**
     * Create a single-threaded fanout service.
     *
     * @param host
     * @param port
     *            the port for running the fanout PubSub service
     * @throws IOException
     */
    public FanoutNioService(int port) {
        this(null, port);
    }
    /**
     * Create a single-threaded fanout service.
     *
     * @param bindInterface
     *            the ip address to bind for the service, may be null
     * @param port
     *            the port for running the fanout PubSub service
     * @throws IOException
     */
    public FanoutNioService(String bindInterface, int port) {
        super(bindInterface, port, "Fanout nio service");
    }
    @Override
    protected boolean isConnected() {
        return serviceCh != null;
    }
    @Override
    protected boolean connect() {
        if (serviceCh == null) {
            try {
                serviceCh = ServerSocketChannel.open();
                serviceCh.configureBlocking(false);
                serviceCh.socket().setReuseAddress(true);
                serviceCh.socket().bind(host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port));
                selector = Selector.open();
                serviceCh.register(selector, SelectionKey.OP_ACCEPT);
                logger.info(MessageFormat.format("{0} is ready on {1}:{2,number,0}",
                        name, host == null ? "0.0.0.0" : host, port));
            } catch (IOException e) {
                logger.error(MessageFormat.format("failed to open {0} on {1}:{2,number,0}",
                        name, name, host == null ? "0.0.0.0" : host, port), e);
                return false;
            }
        }
        return true;
    }
    @Override
    protected void disconnect() {
        try {
            if (serviceCh != null) {
                // close all active client connections
                Map<String, SocketChannel> clients = getCurrentClientSockets();
                for (Map.Entry<String, SocketChannel> client : clients.entrySet()) {
                    closeClientSocket(client.getKey(), client.getValue());
                }
                // close service socket channel
                logger.debug(MessageFormat.format("closing {0} socket channel", name));
                serviceCh.socket().close();
                serviceCh.close();
                serviceCh = null;
                selector.close();
                selector = null;
            }
        } catch (IOException e) {
            logger.error(MessageFormat.format("failed to disconnect {0}", name), e);
        }
    }
    @Override
    protected void listen() throws IOException {
        while (selector.select(serviceTimeout) > 0) {
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyItr = keys.iterator();
            while (keyItr.hasNext()) {
                SelectionKey key = (SelectionKey) keyItr.next();
                if (key.isAcceptable()) {
                    // new fanout client connection
                    ServerSocketChannel sch = (ServerSocketChannel) key.channel();
                    try {
                        SocketChannel ch = sch.accept();
                        ch.configureBlocking(false);
                        configureClientSocket(ch.socket());
                        FanoutNioConnection connection = new FanoutNioConnection(ch);
                        addConnection(connection);
                        // register to send the queued message
                        ch.register(selector, SelectionKey.OP_WRITE, connection);
                    } catch (IOException e) {
                        logger.error("error accepting fanout connection", e);
                    }
                } else if (key.isReadable()) {
                    // read fanout client request
                    SocketChannel ch = (SocketChannel) key.channel();
                    FanoutNioConnection connection = (FanoutNioConnection) key.attachment();
                    try {
                        connection.read(ch, isStrictRequestTermination());
                        int replies = 0;
                        Iterator<String> reqItr = connection.requestQueue.iterator();
                        while (reqItr.hasNext()) {
                            String req = reqItr.next();
                            String reply = processRequest(connection, req);
                            reqItr.remove();
                            if (reply != null) {
                                replies++;
                            }
                        }
                        if (replies > 0) {
                            // register to send the replies to requests
                            ch.register(selector, SelectionKey.OP_WRITE, connection);
                        } else {
                            // re-register for next read
                            ch.register(selector, SelectionKey.OP_READ, connection);
                        }
                    } catch (IOException e) {
                        logger.error(MessageFormat.format("fanout connection {0} error: {1}", connection.id, e.getMessage()));
                        removeConnection(connection);
                        closeClientSocket(connection.id, ch);
                    }
                } else if (key.isWritable()) {
                    // asynchronous reply to fanout client request
                    SocketChannel ch = (SocketChannel) key.channel();
                    FanoutNioConnection connection = (FanoutNioConnection) key.attachment();
                    try {
                        connection.write(ch);
                        if (hasConnection(connection)) {
                            // register for next read
                            ch.register(selector, SelectionKey.OP_READ, connection);
                        } else {
                            // Connection was rejected due to load or
                            // some other reason. Close it.
                            closeClientSocket(connection.id, ch);
                        }
                    } catch (IOException e) {
                        logger.error(MessageFormat.format("fanout connection {0}: {1}", connection.id, e.getMessage()));
                        removeConnection(connection);
                        closeClientSocket(connection.id, ch);
                    }
                }
                keyItr.remove();
            }
        }
    }
    protected void closeClientSocket(String id, SocketChannel ch) {
        try {
            ch.close();
        } catch (IOException e) {
            logger.error(MessageFormat.format("fanout connection {0}", id), e);
        }
    }
    protected void broadcast(Collection<FanoutServiceConnection> connections, String channel, String message) {
        super.broadcast(connections, channel, message);
        // register queued write
        Map<String, SocketChannel> sockets = getCurrentClientSockets();
        for (FanoutServiceConnection connection : connections) {
            SocketChannel ch = sockets.get(connection.id);
            if (ch == null) {
                logger.warn(MessageFormat.format("fanout connection {0} has been disconnected", connection.id));
                removeConnection(connection);
                continue;
            }
            try {
                ch.register(selector, SelectionKey.OP_WRITE, connection);
            } catch (IOException e) {
                logger.error(MessageFormat.format("failed to register write op for fanout connection {0}", connection.id));
            }
        }
    }
    protected Map<String, SocketChannel> getCurrentClientSockets() {
        Map<String, SocketChannel> sockets = new HashMap<String, SocketChannel>();
        for (SelectionKey key : selector.keys()) {
            if (key.channel() instanceof SocketChannel) {
                SocketChannel ch = (SocketChannel) key.channel();
                String id = FanoutConstants.getRemoteSocketId(ch.socket());
                sockets.put(id, ch);
            }
        }
        return sockets;
    }
    /**
     * FanoutNioConnection handles reading/writing messages from a remote fanout
     * connection.
     *
     * @author James Moger
     *
     */
    static class FanoutNioConnection extends FanoutServiceConnection {
        final ByteBuffer readBuffer;
        final ByteBuffer writeBuffer;
        final List<String> requestQueue;
        final List<String> replyQueue;
        final CharsetDecoder decoder;
        FanoutNioConnection(SocketChannel ch) {
            super(ch.socket());
            readBuffer = ByteBuffer.allocate(FanoutConstants.BUFFER_LENGTH);
            writeBuffer = ByteBuffer.allocate(FanoutConstants.BUFFER_LENGTH);
            requestQueue = new ArrayList<String>();
            replyQueue = new ArrayList<String>();
            decoder = Charset.forName(FanoutConstants.CHARSET).newDecoder();
        }
        protected void read(SocketChannel ch, boolean strictRequestTermination) throws CharacterCodingException, IOException {
            long bytesRead = 0;
            readBuffer.clear();
            bytesRead = ch.read(readBuffer);
            readBuffer.flip();
            if (bytesRead == -1) {
                throw new IOException("lost client connection, end of stream");
            }
            if (readBuffer.limit() == 0) {
                return;
            }
            CharBuffer cbuf = decoder.decode(readBuffer);
            String req = cbuf.toString();
            String [] lines = req.split(strictRequestTermination ? "\n" : "\n|\r");
            requestQueue.addAll(Arrays.asList(lines));
        }
        protected void write(SocketChannel ch) throws IOException {
            Iterator<String> itr = replyQueue.iterator();
            while (itr.hasNext()) {
                String reply = itr.next();
                writeBuffer.clear();
                logger.debug(MessageFormat.format("fanout reply to {0}: {1}", id, reply));
                byte [] bytes = reply.getBytes(FanoutConstants.CHARSET);
                writeBuffer.put(bytes);
                if (bytes[bytes.length - 1] != 0xa) {
                    writeBuffer.put((byte) 0xa);
                }
                writeBuffer.flip();
                // loop until write buffer has been completely sent
                int written = 0;
                int toWrite = writeBuffer.remaining();
                while (written != toWrite) {
                    written += ch.write(writeBuffer);
                    try {
                        Thread.sleep(10);
                    } catch (Exception x) {
                    }
                }
                itr.remove();
            }
            writeBuffer.clear();
        }
        @Override
        protected void reply(String content) throws IOException {
            // queue the reply
            // replies are transmitted asynchronously from the requests
            replyQueue.add(content);
        }
    }
}
src/com/gitblit/fanout/FanoutService.java
New file
@@ -0,0 +1,563 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * Base class for Fanout service implementations.
 *
 * Subclass implementations can be used as a Sparkleshare PubSub notification
 * server.  This allows Sparkleshare to be used in conjunction with Gitblit
 * behind a corporate firewall that restricts or prohibits client internet access
 * to the default Sparkleshare PubSub server: notifications.sparkleshare.org
 *
 * @author James Moger
 *
 */
public abstract class FanoutService implements Runnable {
    private final static Logger logger = LoggerFactory.getLogger(FanoutService.class);
    public final static int DEFAULT_PORT = 17000;
    protected final static int serviceTimeout = 5000;
    protected final String host;
    protected final int port;
    protected final String name;
    private Thread serviceThread;
    private final Map<String, FanoutServiceConnection> connections;
    private final Map<String, Set<FanoutServiceConnection>> subscriptions;
    protected final AtomicBoolean isRunning;
    private final AtomicBoolean strictRequestTermination;
    private final AtomicBoolean allowAllChannelAnnouncements;
    private final AtomicInteger concurrentConnectionLimit;
    private final Date bootDate;
    private final AtomicLong rejectedConnectionCount;
    private final AtomicInteger peakConnectionCount;
    private final AtomicLong totalConnections;
    private final AtomicLong totalAnnouncements;
    private final AtomicLong totalMessages;
    private final AtomicLong totalSubscribes;
    private final AtomicLong totalUnsubscribes;
    private final AtomicLong totalPings;
    protected FanoutService(String host, int port, String name) {
        this.host = host;
        this.port = port;
        this.name = name;
        connections = new ConcurrentHashMap<String, FanoutServiceConnection>();
        subscriptions = new ConcurrentHashMap<String, Set<FanoutServiceConnection>>();
        subscriptions.put(FanoutConstants.CH_ALL, new ConcurrentSkipListSet<FanoutServiceConnection>());
        isRunning = new AtomicBoolean(false);
        strictRequestTermination = new AtomicBoolean(false);
        allowAllChannelAnnouncements = new AtomicBoolean(false);
        concurrentConnectionLimit = new AtomicInteger(0);
        bootDate = new Date();
        rejectedConnectionCount = new AtomicLong(0);
        peakConnectionCount = new AtomicInteger(0);
        totalConnections = new AtomicLong(0);
        totalAnnouncements = new AtomicLong(0);
        totalMessages = new AtomicLong(0);
        totalSubscribes = new AtomicLong(0);
        totalUnsubscribes = new AtomicLong(0);
        totalPings = new AtomicLong(0);
    }
    /*
     * Abstract methods
     */
    protected abstract boolean isConnected();
    protected abstract boolean connect();
    protected abstract void listen() throws IOException;
    protected abstract void disconnect();
    /**
     * Returns true if the service requires \n request termination.
     *
     * @return true if request requires \n termination
     */
    public boolean isStrictRequestTermination() {
        return strictRequestTermination.get();
    }
    /**
     * Control the termination of fanout requests. If true, fanout requests must
     * be terminated with \n. If false, fanout requests may be terminated with
     * \n, \r, \r\n, or \n\r. This is useful for debugging with a telnet client.
     *
     * @param isStrictTermination
     */
    public void setStrictRequestTermination(boolean isStrictTermination) {
        strictRequestTermination.set(isStrictTermination);
    }
    /**
     * Returns the maximum allowable concurrent fanout connections.
     *
     * @return the maximum allowable concurrent connection count
     */
    public int getConcurrentConnectionLimit() {
        return concurrentConnectionLimit.get();
    }
    /**
     * Sets the maximum allowable concurrent fanout connection count.
     *
     * @param value
     */
    public void setConcurrentConnectionLimit(int value) {
        concurrentConnectionLimit.set(value);
    }
    /**
     * Returns true if connections are allowed to announce on the all channel.
     *
     * @return true if connections are allowed to announce on the all channel
     */
    public boolean allowAllChannelAnnouncements() {
        return allowAllChannelAnnouncements.get();
    }
    /**
     * Allows/prohibits connections from announcing on the ALL channel.
     *
     * @param value
     */
    public void setAllowAllChannelAnnouncements(boolean value) {
        allowAllChannelAnnouncements.set(value);
    }
    /**
     * Returns the current connections
     *
     * @param channel
     * @return map of current connections keyed by their id
     */
    public Map<String, FanoutServiceConnection> getCurrentConnections() {
        return connections;
    }
    /**
     * Returns all subscriptions
     *
     * @return map of current subscriptions keyed by channel name
     */
    public Map<String, Set<FanoutServiceConnection>> getCurrentSubscriptions() {
        return subscriptions;
    }
    /**
     * Returns the subscriptions for the specified channel
     *
     * @param channel
     * @return set of subscribed connections for the specified channel
     */
    public Set<FanoutServiceConnection> getCurrentSubscriptions(String channel) {
        return subscriptions.get(channel);
    }
    /**
     * Returns the runtime statistics object for this service.
     *
     * @return stats
     */
    public FanoutStats getStatistics() {
        FanoutStats stats = new FanoutStats();
        // settings
        stats.allowAllChannelAnnouncements = allowAllChannelAnnouncements();
        stats.concurrentConnectionLimit = getConcurrentConnectionLimit();
        stats.strictRequestTermination = isStrictRequestTermination();
        // runtime stats
        stats.bootDate = bootDate;
        stats.rejectedConnectionCount = rejectedConnectionCount.get();
        stats.peakConnectionCount = peakConnectionCount.get();
        stats.totalConnections = totalConnections.get();
        stats.totalAnnouncements = totalAnnouncements.get();
        stats.totalMessages = totalMessages.get();
        stats.totalSubscribes = totalSubscribes.get();
        stats.totalUnsubscribes = totalUnsubscribes.get();
        stats.totalPings = totalPings.get();
        stats.currentConnections = connections.size();
        stats.currentChannels = subscriptions.size();
        stats.currentSubscriptions = subscriptions.size() * connections.size();
        return stats;
    }
    /**
     * Returns true if the service is ready.
     *
     * @return true, if the service is ready
     */
    public boolean isReady() {
        if (isRunning.get()) {
            return isConnected();
        }
        return false;
    }
    /**
     * Start the Fanout service thread and immediatel return.
     *
     */
    public void start() {
        if (isRunning.get()) {
            logger.warn(MessageFormat.format("{0} is already running", name));
            return;
        }
        serviceThread = new Thread(this);
        serviceThread.setName(MessageFormat.format("{0} {1}:{2,number,0}", name, host == null ? "all" : host, port));
        serviceThread.start();
    }
    /**
     * Start the Fanout service thread and wait until it is accepting connections.
     *
     */
    public void startSynchronously() {
        start();
        while (!isReady()) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }
    }
    /**
     * Stop the Fanout service.  This method returns when the service has been
     * completely shutdown.
     */
    public void stop() {
        if (!isRunning.get()) {
            logger.warn(MessageFormat.format("{0} is not running", name));
            return;
        }
        logger.info(MessageFormat.format("stopping {0}...", name));
        isRunning.set(false);
        try {
            if (serviceThread != null) {
                serviceThread.join();
                serviceThread = null;
            }
        } catch (InterruptedException e1) {
            logger.error("", e1);
        }
        logger.info(MessageFormat.format("stopped {0}", name));
    }
    /**
     * Main execution method of the service
     */
    @Override
    public final void run() {
        disconnect();
        resetState();
        isRunning.set(true);
        while (isRunning.get()) {
            if (connect()) {
                try {
                    listen();
                } catch (IOException e) {
                    logger.error(MessageFormat.format("error processing {0}", name), e);
                    isRunning.set(false);
                }
            } else {
                try {
                    Thread.sleep(serviceTimeout);
                } catch (InterruptedException x) {
                }
            }
        }
        disconnect();
        resetState();
    }
    protected void resetState() {
        // reset state data
        connections.clear();
        subscriptions.clear();
        rejectedConnectionCount.set(0);
        peakConnectionCount.set(0);
        totalConnections.set(0);
        totalAnnouncements.set(0);
        totalMessages.set(0);
        totalSubscribes.set(0);
        totalUnsubscribes.set(0);
        totalPings.set(0);
    }
    /**
     * Configure the client connection socket.
     *
     * @param socket
     * @throws SocketException
     */
    protected void configureClientSocket(Socket socket) throws SocketException {
        socket.setKeepAlive(true);
        socket.setSoLinger(true, 0); // immediately discard any remaining data
    }
    /**
     * Add the connection to the connections map.
     *
     * @param connection
     * @return false if the connection was rejected due to too many concurrent
     *         connections
     */
    protected boolean addConnection(FanoutServiceConnection connection) {
        int limit = getConcurrentConnectionLimit();
        if (limit > 0 && connections.size() > limit) {
            logger.info(MessageFormat.format("hit {0,number,0} connection limit, rejecting fanout connection", concurrentConnectionLimit));
            increment(rejectedConnectionCount);
            connection.busy();
            return false;
        }
        // add the connection to our map
        connections.put(connection.id, connection);
        // track peak number of concurrent connections
        if (connections.size() > peakConnectionCount.get()) {
            peakConnectionCount.set(connections.size());
        }
        logger.info("fanout new connection " + connection.id);
        connection.connected();
        return true;
    }
    /**
     * Remove the connection from the connections list and from subscriptions.
     *
     * @param connection
     */
    protected void removeConnection(FanoutServiceConnection connection) {
        connections.remove(connection.id);
        Iterator<Map.Entry<String, Set<FanoutServiceConnection>>> itr = subscriptions.entrySet().iterator();
        while (itr.hasNext()) {
            Map.Entry<String, Set<FanoutServiceConnection>> entry = itr.next();
            Set<FanoutServiceConnection> subscriptions = entry.getValue();
            subscriptions.remove(connection);
            if (!FanoutConstants.CH_ALL.equals(entry.getKey())) {
                if (subscriptions.size() == 0) {
                    itr.remove();
                    logger.info(MessageFormat.format("fanout remove channel {0}, no subscribers", entry.getKey()));
                }
            }
        }
        logger.info(MessageFormat.format("fanout connection {0} removed", connection.id));
    }
    /**
     * Tests to see if the connection is being monitored by the service.
     *
     * @param connection
     * @return true if the service is monitoring the connection
     */
    protected boolean hasConnection(FanoutServiceConnection connection) {
        return connections.containsKey(connection.id);
    }
    /**
     * Reply to a connection on the specified channel.
     *
     * @param connection
     * @param channel
     * @param message
     * @return the reply
     */
    protected String reply(FanoutServiceConnection connection, String channel, String message) {
        if (channel != null && channel.length() > 0) {
            increment(totalMessages);
        }
        return connection.reply(channel, message);
    }
    /**
     * Service method to broadcast a message to all connections.
     *
     * @param message
     */
    public void broadcastAll(String message) {
        broadcast(connections.values(), FanoutConstants.CH_ALL, message);
        increment(totalAnnouncements);
    }
    /**
     * Service method to broadcast a message to connections subscribed to the
     * channel.
     *
     * @param message
     */
    public void broadcast(String channel, String message) {
        List<FanoutServiceConnection> connections = new ArrayList<FanoutServiceConnection>(subscriptions.get(channel));
        broadcast(connections, channel, message);
        increment(totalAnnouncements);
    }
    /**
     * Broadcast a message to connections subscribed to the specified channel.
     *
     * @param connections
     * @param channel
     * @param message
     */
    protected void broadcast(Collection<FanoutServiceConnection> connections, String channel, String message) {
        for (FanoutServiceConnection connection : connections) {
            reply(connection, channel, message);
        }
    }
    /**
     * Process an incoming Fanout request.
     *
     * @param connection
     * @param req
     * @return the reply to the request, may be null
     */
    protected String processRequest(FanoutServiceConnection connection, String req) {
        logger.info(MessageFormat.format("fanout request from {0}: {1}", connection.id, req));
        String[] fields = req.split(" ", 3);
        String action = fields[0];
        String channel = fields.length >= 2 ? fields[1] : null;
        String message = fields.length >= 3 ? fields[2] : null;
        try {
            return processRequest(connection, action, channel, message);
        } catch (IllegalArgumentException e) {
            // invalid action
            logger.error(MessageFormat.format("fanout connection {0} requested invalid action {1}", connection.id, action));
            logger.error(asHexArray(req));
        }
        return null;
    }
    /**
     * Process the Fanout request.
     *
     * @param connection
     * @param action
     * @param channel
     * @param message
     * @return the reply to the request, may be null
     * @throws IllegalArgumentException
     */
    protected String processRequest(FanoutServiceConnection connection, String action, String channel, String message) throws IllegalArgumentException {
        if ("ping".equals(action)) {
            // ping
            increment(totalPings);
            return reply(connection, null, "" + System.currentTimeMillis());
        } else if ("info".equals(action)) {
            // info
            String info = getStatistics().info();
            return reply(connection, null, info);
        } else if ("announce".equals(action)) {
            // announcement
            if (!allowAllChannelAnnouncements.get() && FanoutConstants.CH_ALL.equals(channel)) {
                // prohibiting connection-sourced all announcements
                logger.warn(MessageFormat.format("fanout connection {0} attempted to announce {1} on ALL channel", connection.id, message));
            } else if ("debug".equals(channel)) {
                // prohibiting connection-sourced debug announcements
                logger.warn(MessageFormat.format("fanout connection {0} attempted to announce {1} on DEBUG channel", connection.id, message));
            } else {
                // acceptable announcement
                List<FanoutServiceConnection> connections = new ArrayList<FanoutServiceConnection>(subscriptions.get(channel));
                connections.remove(connection); // remove announcer
                broadcast(connections, channel, message);
                increment(totalAnnouncements);
            }
        } else if ("subscribe".equals(action)) {
            // subscribe
            if (!subscriptions.containsKey(channel)) {
                logger.info(MessageFormat.format("fanout new channel {0}", channel));
                subscriptions.put(channel, new ConcurrentSkipListSet<FanoutServiceConnection>());
            }
            subscriptions.get(channel).add(connection);
            logger.debug(MessageFormat.format("fanout connection {0} subscribed to channel {1}", connection.id, channel));
            increment(totalSubscribes);
        } else if ("unsubscribe".equals(action)) {
            // unsubscribe
            if (subscriptions.containsKey(channel)) {
                subscriptions.get(channel).remove(connection);
                if (subscriptions.get(channel).size() == 0) {
                    subscriptions.remove(channel);
                }
                increment(totalUnsubscribes);
            }
        } else {
            // invalid action
            throw new IllegalArgumentException(action);
        }
        return null;
    }
    private String asHexArray(String req) {
        StringBuilder sb = new StringBuilder();
        for (char c : req.toCharArray()) {
            sb.append(Integer.toHexString(c)).append(' ');
        }
        return "[ " + sb.toString().trim() + " ]";
    }
    /**
     * Increment a long and prevent negative rollover.
     *
     * @param counter
     */
    private void increment(AtomicLong counter) {
        long v = counter.incrementAndGet();
        if (v < 0) {
            counter.set(0);
        }
    }
    @Override
    public String toString() {
        return name;
    }
}
src/com/gitblit/fanout/FanoutServiceConnection.java
New file
@@ -0,0 +1,105 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.IOException;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * FanoutServiceConnection handles reading/writing messages from a remote fanout
 * connection.
 *
 * @author James Moger
 *
 */
public abstract class FanoutServiceConnection implements Comparable<FanoutServiceConnection> {
    private static final Logger logger = LoggerFactory.getLogger(FanoutServiceConnection.class);
    public final String id;
    protected FanoutServiceConnection(Socket socket) {
        this.id = FanoutConstants.getRemoteSocketId(socket);
    }
    protected abstract void reply(String content) throws IOException;
    /**
     * Send the connection a debug channel connected message.
     *
     * @param message
     */
    protected void connected() {
        reply(FanoutConstants.CH_DEBUG, FanoutConstants.MSG_CONNECTED);
    }
    /**
     * Send the connection a debug channel busy message.
     *
     * @param message
     */
    protected void busy() {
        reply(FanoutConstants.CH_DEBUG, FanoutConstants.MSG_BUSY);
    }
    /**
     * Send the connection a message for the specified channel.
     *
     * @param channel
     * @param message
     * @return the reply
     */
    protected String reply(String channel, String message) {
        String content;
        if (channel != null) {
            content = channel + "!" + message;
        } else {
            content = message;
        }
        try {
            reply(content);
        } catch (Exception e) {
            logger.error("failed to reply to fanout connection " + id, e);
        }
        return content;
    }
    @Override
    public int compareTo(FanoutServiceConnection c) {
        return id.compareTo(c.id);
    }
    @Override
    public boolean equals(Object o) {
        if (o instanceof FanoutServiceConnection) {
            return id.equals(((FanoutServiceConnection) o).id);
        }
        return false;
    }
    @Override
    public int hashCode() {
        return id.hashCode();
    }
    @Override
    public String toString() {
        return id;
    }
}
src/com/gitblit/fanout/FanoutSocketService.java
New file
@@ -0,0 +1,234 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.text.MessageFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * A multi-threaded socket implementation of https://github.com/travisghansen/fanout
 *
 * This implementation creates a master acceptor thread which accepts incoming
 * fanout connections and then spawns a daemon thread for each accepted connection.
 * If there are 100 concurrent fanout connections, there are 101 threads.
 *
 * @author James Moger
 *
 */
public class FanoutSocketService extends FanoutService {
    private final static Logger logger = LoggerFactory.getLogger(FanoutSocketService.class);
    private volatile ServerSocket serviceSocket;
    public static void main(String[] args) throws Exception {
        FanoutSocketService pubsub = new FanoutSocketService(null, DEFAULT_PORT);
        pubsub.setStrictRequestTermination(false);
        pubsub.setAllowAllChannelAnnouncements(false);
        pubsub.start();
    }
    /**
     * Create a multi-threaded fanout service.
     *
     * @param port
     *            the port for running the fanout PubSub service
     * @throws IOException
     */
    public FanoutSocketService(int port) {
        this(null, port);
    }
    /**
     * Create a multi-threaded fanout service.
     *
     * @param bindInterface
     *            the ip address to bind for the service, may be null
     * @param port
     *            the port for running the fanout PubSub service
     * @throws IOException
     */
    public FanoutSocketService(String bindInterface, int port) {
        super(bindInterface, port, "Fanout socket service");
    }
    @Override
    protected boolean isConnected() {
        return serviceSocket != null;
    }
    @Override
    protected boolean connect() {
        if (serviceSocket == null) {
            try {
                serviceSocket = new ServerSocket();
                serviceSocket.setReuseAddress(true);
                serviceSocket.setSoTimeout(serviceTimeout);
                serviceSocket.bind(host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port));
                logger.info(MessageFormat.format("{0} is ready on {1}:{2,number,0}",
                        name, host == null ? "0.0.0.0" : host, serviceSocket.getLocalPort()));
            } catch (IOException e) {
                logger.error(MessageFormat.format("failed to open {0} on {1}:{2,number,0}",
                        name, host == null ? "0.0.0.0" : host, port), e);
                return false;
            }
        }
        return true;
    }
    @Override
    protected void disconnect() {
        try {
            if (serviceSocket != null) {
                logger.debug(MessageFormat.format("closing {0} server socket", name));
                serviceSocket.close();
                serviceSocket = null;
            }
        } catch (IOException e) {
            logger.error(MessageFormat.format("failed to disconnect {0}", name), e);
        }
    }
    /**
     * This accepts incoming fanout connections and spawns connection threads.
     */
    @Override
    protected void listen() throws IOException {
        try {
            Socket socket;
            socket = serviceSocket.accept();
            configureClientSocket(socket);
            FanoutSocketConnection connection = new FanoutSocketConnection(socket);
            if (addConnection(connection)) {
                // spawn connection daemon thread
                Thread connectionThread = new Thread(connection);
                connectionThread.setDaemon(true);
                connectionThread.setName("Fanout " + connection.id);
                connectionThread.start();
            } else {
                // synchronously close the connection and remove it
                removeConnection(connection);
                connection.closeConnection();
                connection = null;
            }
        } catch (SocketTimeoutException e) {
            // ignore accept timeout exceptions
        }
    }
    /**
     * FanoutSocketConnection handles reading/writing messages from a remote fanout
     * connection.
     *
     * @author James Moger
     *
     */
    class FanoutSocketConnection extends FanoutServiceConnection implements Runnable {
        Socket socket;
        FanoutSocketConnection(Socket socket) {
            super(socket);
            this.socket = socket;
        }
        /**
         * Connection thread read/write method.
         */
        @Override
        public void run() {
            try {
                StringBuilder sb = new StringBuilder();
                BufferedInputStream is = new BufferedInputStream(socket.getInputStream());
                byte[] buffer = new byte[FanoutConstants.BUFFER_LENGTH];
                int len = 0;
                while (true) {
                    while (is.available() > 0) {
                        len = is.read(buffer);
                        for (int i = 0; i < len; i++) {
                            byte b = buffer[i];
                            if (b == 0xa || (!isStrictRequestTermination() && b == 0xd)) {
                                String req = sb.toString();
                                sb.setLength(0);
                                if (req.length() > 0) {
                                    // ignore empty request strings
                                    processRequest(this, req);
                                }
                            } else {
                                sb.append((char) b);
                            }
                        }
                    }
                    if (!isRunning.get()) {
                        // service has stopped, terminate client connection
                        break;
                    } else {
                        Thread.sleep(500);
                    }
                }
            } catch (Throwable t) {
                if (t instanceof SocketException) {
                    logger.error(MessageFormat.format("fanout connection {0}: {1}", id, t.getMessage()));
                } else if (t instanceof SocketTimeoutException) {
                    logger.error(MessageFormat.format("fanout connection {0}: {1}", id, t.getMessage()));
                } else {
                    logger.error(MessageFormat.format("exception while handling fanout connection {0}", id), t);
                }
            } finally {
                closeConnection();
            }
            logger.info(MessageFormat.format("thread for fanout connection {0} is finished", id));
        }
        @Override
        protected void reply(String content) throws IOException {
            // synchronously send reply
            logger.debug(MessageFormat.format("fanout reply to {0}: {1}", id, content));
            OutputStream os = socket.getOutputStream();
            byte [] bytes = content.getBytes(FanoutConstants.CHARSET);
            os.write(bytes);
            if (bytes[bytes.length - 1] != 0xa) {
                os.write(0xa);
            }
            os.flush();
        }
        protected void closeConnection() {
            // close the connection socket
            try {
                socket.close();
            } catch (IOException e) {
            }
            socket = null;
            // remove this connection from the service
            removeConnection(this);
        }
    }
}
src/com/gitblit/fanout/FanoutStats.java
New file
@@ -0,0 +1,98 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.fanout;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Date;
/**
 * Encapsulates the runtime stats of a fanout service.
 *
 * @author James Moger
 *
 */
public class FanoutStats implements Serializable {
    private static final long serialVersionUID = 1L;
    public long concurrentConnectionLimit;
    public boolean allowAllChannelAnnouncements;
    public boolean strictRequestTermination;
    public Date bootDate;
    public long rejectedConnectionCount;
    public int peakConnectionCount;
    public long currentChannels;
    public long currentSubscriptions;
    public long currentConnections;
    public long totalConnections;
    public long totalAnnouncements;
    public long totalMessages;
    public long totalSubscribes;
    public long totalUnsubscribes;
    public long totalPings;
    public String info() {
        int i = 0;
        StringBuilder sb = new StringBuilder();
        sb.append(infoStr(i++, "boot date"));
        sb.append(infoStr(i++, "strict request termination"));
        sb.append(infoStr(i++, "allow connection \"all\" announcements"));
        sb.append(infoInt(i++, "concurrent connection limit"));
        sb.append(infoInt(i++, "concurrent limit rejected connections"));
        sb.append(infoInt(i++, "peak connections"));
        sb.append(infoInt(i++, "current connections"));
        sb.append(infoInt(i++, "current channels"));
        sb.append(infoInt(i++, "current subscriptions"));
        sb.append(infoInt(i++, "user-requested subscriptions"));
        sb.append(infoInt(i++, "total connections"));
        sb.append(infoInt(i++, "total announcements"));
        sb.append(infoInt(i++, "total messages"));
        sb.append(infoInt(i++, "total subscribes"));
        sb.append(infoInt(i++, "total unsubscribes"));
        sb.append(infoInt(i++, "total pings"));
        String template = sb.toString();
        String info = MessageFormat.format(template,
                bootDate.toString(),
                Boolean.toString(strictRequestTermination),
                Boolean.toString(allowAllChannelAnnouncements),
                concurrentConnectionLimit,
                rejectedConnectionCount,
                peakConnectionCount,
                currentConnections,
                currentChannels,
                currentSubscriptions,
                currentSubscriptions == 0 ? 0 : (currentSubscriptions - currentConnections),
                        totalConnections,
                        totalAnnouncements,
                        totalMessages,
                        totalSubscribes,
                        totalUnsubscribes,
                        totalPings);
        return info;
    }
    private String infoStr(int index, String label) {
        return label + ": {" + index + "}\n";
    }
    private String infoInt(int index, String label) {
        return label + ": {" + index + ",number,0}\n";
    }
}
src/com/gitblit/models/Activity.java
@@ -25,9 +25,9 @@
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
/**
@@ -93,8 +93,7 @@
            }
            repositoryMetrics.get(repository).count++;
            String author = commit.getAuthorIdent().getEmailAddress()
                    .toLowerCase();
            String author = StringUtils.removeNewlines(commit.getAuthorIdent().getEmailAddress()).toLowerCase();
            if (!authorMetrics.containsKey(author)) {
                authorMetrics.put(author, new Metric(author));
            }
@@ -126,87 +125,5 @@
    public int compareTo(Activity o) {
        // reverse chronological order
        return o.startDate.compareTo(startDate);
    }
    /**
     * Model class to represent a RevCommit, it's source repository, and the
     * branch. This class is used by the activity page.
     *
     * @author James Moger
     */
    public static class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
        private static final long serialVersionUID = 1L;
        public final String repository;
        public final String branch;
        private final RevCommit commit;
        private List<RefModel> refs;
        public RepositoryCommit(String repository, String branch, RevCommit commit) {
            this.repository = repository;
            this.branch = branch;
            this.commit = commit;
        }
        public void setRefs(List<RefModel> refs) {
            this.refs = refs;
        }
        public List<RefModel> getRefs() {
            return refs;
        }
        public String getName() {
            return commit.getName();
        }
        public String getShortName() {
            return commit.getName().substring(0, 8);
        }
        public String getShortMessage() {
            return commit.getShortMessage();
        }
        public int getParentCount() {
            return commit.getParentCount();
        }
        public PersonIdent getAuthorIdent() {
            return commit.getAuthorIdent();
        }
        public PersonIdent getCommitterIdent() {
            return commit.getCommitterIdent();
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof RepositoryCommit) {
                RepositoryCommit commit = (RepositoryCommit) o;
                return repository.equals(commit.repository) && getName().equals(commit.getName());
            }
            return false;
        }
        @Override
        public int hashCode() {
            return (repository + commit).hashCode();
        }
        @Override
        public int compareTo(RepositoryCommit o) {
            // reverse-chronological order
            if (commit.getCommitTime() > o.commit.getCommitTime()) {
                return -1;
            } else if (commit.getCommitTime() < o.commit.getCommitTime()) {
                return 1;
            }
            return 0;
        }
    }
}
src/com/gitblit/models/ProjectModel.java
@@ -39,6 +39,8 @@
    public String description;
    public final Set<String> repositories = new HashSet<String>();
    
    public String projectMarkdown;
    public String repositoriesMarkdown;
    public Date lastChange;
    public final boolean isRoot;
src/com/gitblit/models/PushLogEntry.java
New file
@@ -0,0 +1,208 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.models;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;
/**
 * Model class to represent a push into a repository.
 *
 * @author James Moger
 */
public class PushLogEntry implements Serializable, Comparable<PushLogEntry> {
    private static final long serialVersionUID = 1L;
    public final String repository;
    public final Date date;
    public final UserModel user;
    private final Set<RepositoryCommit> commits;
    private final Map<String, ReceiveCommand.Type> refUpdates;
    /**
     * Constructor for specified duration of push from start date.
     *
     * @param repository
     *            the repository that received the push
     * @param date
     *            the date of the push
     * @param user
     *            the user who pushed
     */
    public PushLogEntry(String repository, Date date, UserModel user) {
        this.repository = repository;
        this.date = date;
        this.user = user;
        this.commits = new LinkedHashSet<RepositoryCommit>();
        this.refUpdates = new HashMap<String, ReceiveCommand.Type>();
    }
    /**
     * Tracks the change type for the specified ref.
     *
     * @param ref
     * @param type
     */
    public void updateRef(String ref, ReceiveCommand.Type type) {
        if (!refUpdates.containsKey(ref)) {
            refUpdates.put(ref, type);
        }
    }
    /**
     * Adds a commit to the push entry object as long as the commit is not a
     * duplicate.
     *
     * @param branch
     * @param commit
     * @return a RepositoryCommit, if one was added. Null if this is duplicate
     *         commit
     */
    public RepositoryCommit addCommit(String branch, RevCommit commit) {
        RepositoryCommit commitModel = new RepositoryCommit(repository, branch, commit);
        if (commits.add(commitModel)) {
            return commitModel;
        }
        return null;
    }
    /**
     * Returns true if this push contains a non-fastforward ref update.
     *
     * @return true if this is a non-fastforward push
     */
    public boolean isNonFastForward() {
        for (Map.Entry<String, ReceiveCommand.Type> entry : refUpdates.entrySet()) {
            if (ReceiveCommand.Type.UPDATE_NONFASTFORWARD.equals(entry.getValue())) {
                return true;
            }
        }
        return false;
    }
    /**
     * Returns the list of branches changed by the push.
     *
     * @return a list of branches
     */
    public List<String> getChangedBranches() {
        return getChangedRefs(Constants.R_HEADS);
    }
    /**
     * Returns the list of tags changed by the push.
     *
     * @return a list of tags
     */
    public List<String> getChangedTags() {
        return getChangedRefs(Constants.R_TAGS);
    }
    /**
     * Gets the changed refs in the push.
     *
     * @param baseRef
     * @return the changed refs
     */
    protected List<String> getChangedRefs(String baseRef) {
        Set<String> refs = new HashSet<String>();
        for (String ref : refUpdates.keySet()) {
            if (baseRef == null || ref.startsWith(baseRef)) {
                refs.add(ref);
            }
        }
        List<String> list = new ArrayList<String>(refs);
        Collections.sort(list);
        return list;
    }
    /**
     * The total number of commits in the push.
     *
     * @return the number of commits in the push
     */
    public int getCommitCount() {
        return commits.size();
    }
    /**
     * Returns all commits in the push.
     *
     * @return a list of commits
     */
    public List<RepositoryCommit> getCommits() {
        List<RepositoryCommit> list = new ArrayList<RepositoryCommit>(commits);
        Collections.sort(list);
        return list;
    }
    /**
     * Returns all commits that belong to a particular ref
     *
     * @param ref
     * @return a list of commits
     */
    public List<RepositoryCommit> getCommits(String ref) {
        List<RepositoryCommit> list = new ArrayList<RepositoryCommit>();
        for (RepositoryCommit commit : commits) {
            if (commit.branch.equals(ref)) {
                list.add(commit);
            }
        }
        Collections.sort(list);
        return list;
    }
    @Override
    public int compareTo(PushLogEntry o) {
        // reverse chronological order
        return o.date.compareTo(date);
    }
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1} pushed {2,number,0} commit{3} to {4} ",
                date, user.getDisplayName(), commits.size(), commits.size() == 1 ? "":"s", repository));
        for (Map.Entry<String, ReceiveCommand.Type> entry : refUpdates.entrySet()) {
            String ref = entry.getKey();
            ReceiveCommand.Type type = entry.getValue();
            sb.append("\n  ").append(ref).append(' ').append(type.name()).append('\n');
            for (RepositoryCommit commit : getCommits(ref)) {
                sb.append("    ").append(commit.toString()).append('\n');
            }
        }
        return sb.toString();
    }
}
src/com/gitblit/models/RepositoryCommit.java
New file
@@ -0,0 +1,112 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.models;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.List;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
/**
 * Model class to represent a RevCommit, it's source repository, and the branch.
 * This class is used by the activity page.
 *
 * @author James Moger
 */
public class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
    private static final long serialVersionUID = 1L;
    public final String repository;
    public final String branch;
    private final RevCommit commit;
    private List<RefModel> refs;
    public RepositoryCommit(String repository, String branch, RevCommit commit) {
        this.repository = repository;
        this.branch = branch;
        this.commit = commit;
    }
    public void setRefs(List<RefModel> refs) {
        this.refs = refs;
    }
    public List<RefModel> getRefs() {
        return refs;
    }
    public String getName() {
        return commit.getName();
    }
    public String getShortName() {
        return commit.getName().substring(0, 8);
    }
    public String getShortMessage() {
        return commit.getShortMessage();
    }
    public int getParentCount() {
        return commit.getParentCount();
    }
    public PersonIdent getAuthorIdent() {
        return commit.getAuthorIdent();
    }
    public PersonIdent getCommitterIdent() {
        return commit.getCommitterIdent();
    }
    @Override
    public boolean equals(Object o) {
        if (o instanceof RepositoryCommit) {
            RepositoryCommit commit = (RepositoryCommit) o;
            return repository.equals(commit.repository) && getName().equals(commit.getName());
        }
        return false;
    }
    @Override
    public int hashCode() {
        return (repository + commit).hashCode();
    }
    @Override
    public int compareTo(RepositoryCommit o) {
        // reverse-chronological order
        if (commit.getCommitTime() > o.commit.getCommitTime()) {
            return -1;
        } else if (commit.getCommitTime() < o.commit.getCommitTime()) {
            return 1;
        }
        return 0;
    }
    @Override
    public String toString() {
        return MessageFormat.format("{0} {1} {2,date,yyyy-MM-dd HH:mm} {3} {4}",
                getShortName(), branch, getCommitterIdent().getWhen(), getAuthorIdent().getName(),
                getShortMessage());
    }
}
src/com/gitblit/models/RepositoryModel.java
@@ -17,6 +17,7 @@
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -43,7 +44,7 @@
    // field names are reflectively mapped in EditRepository page
    public String name;
    public String description;
    public String owner;
    public List<String> owners;
    public Date lastChange;
    public boolean hasCommits;
    public boolean showRemoteBranches;
@@ -82,6 +83,7 @@
    
    public transient boolean isCollectingGarbage;
    public Date lastGC;
    public String sparkleshareId;
    
    public RepositoryModel() {
        this("", "", "", new Date(0));
@@ -90,13 +92,15 @@
    public RepositoryModel(String name, String description, String owner, Date lastchange) {
        this.name = name;
        this.description = description;
        this.owner = owner;
        this.lastChange = lastchange;
        this.accessRestriction = AccessRestrictionType.NONE;
        this.authorizationControl = AuthorizationControl.NAMED;
        this.federationSets = new ArrayList<String>();
        this.federationStrategy = FederationStrategy.FEDERATE_THIS;    
        this.projectPath = StringUtils.getFirstPathElement(name);
        this.owners = new ArrayList<String>();
        addOwner(owner);
    }
    
    public List<String> getLocalBranches() {
@@ -161,7 +165,10 @@
    }
    
    public boolean isOwner(String username) {
        return owner != null && username != null && owner.equalsIgnoreCase(username);
        if (StringUtils.isEmpty(username) || ArrayUtils.isEmpty(owners)) {
            return false;
        }
        return owners.contains(username.toLowerCase());
    }
    
    public boolean isPersonalRepository() {
@@ -174,6 +181,10 @@
    
    public boolean allowAnonymousView() {
        return !accessRestriction.atLeast(AccessRestrictionType.VIEW);
    }
    public boolean isSparkleshared() {
        return !StringUtils.isEmpty(sparkleshareId);
    }
    
    public RepositoryModel cloneAs(String cloneName) {
@@ -193,6 +204,40 @@
        clone.useTickets = useTickets;
        clone.skipSizeCalculation = skipSizeCalculation;
        clone.skipSummaryMetrics = skipSummaryMetrics;
        clone.sparkleshareId = sparkleshareId;
        return clone;
    }
}
    public void addOwner(String username) {
        if (!StringUtils.isEmpty(username)) {
            String name = username.toLowerCase();
            // a set would be more efficient, but this complicates JSON
            // deserialization so we enforce uniqueness with an arraylist
            if (!owners.contains(name)) {
                owners.add(name);
            }
        }
    }
    public void removeOwner(String username) {
        if (!StringUtils.isEmpty(username)) {
            owners.remove(username.toLowerCase());
        }
    }
    public void addOwners(Collection<String> usernames) {
        if (!ArrayUtils.isEmpty(usernames)) {
            for (String username : usernames) {
                addOwner(username);
            }
        }
    }
    public void removeOwners(Collection<String> usernames) {
        if (!ArrayUtils.isEmpty(owners)) {
            for (String username : usernames) {
                removeOwner(username);
            }
        }
    }
}
src/com/gitblit/models/UserModel.java
@@ -29,6 +29,7 @@
import com.gitblit.Constants.AccessPermission;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.PermissionType;
import com.gitblit.Constants.RegistrantType;
@@ -73,15 +74,22 @@
    // non-persisted fields
    public boolean isAuthenticated;
    public AccountType accountType;
    
    public UserModel(String username) {
        this.username = username;
        this.isAuthenticated = true;
        this.accountType = AccountType.LOCAL;
    }
    private UserModel() {
        this.username = "$anonymous";
        this.isAuthenticated = false;
        this.accountType = AccountType.LOCAL;
    }
    public boolean isLocalAccount() {
        return accountType.isLocal();
    }
    /**
@@ -100,8 +108,7 @@
    @Deprecated
    @Unused
    public boolean canAccessRepository(RepositoryModel repository) {
        boolean isOwner = !StringUtils.isEmpty(repository.owner)
                && repository.owner.equals(username);
        boolean isOwner = repository.isOwner(username);
        boolean allowAuthenticated = isAuthenticated && AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl);
        return canAdmin() || isOwner || repositories.contains(repository.name.toLowerCase())
                || hasTeamAccess(repository.name) || allowAuthenticated;
src/com/gitblit/utils/ActivityUtils.java
@@ -36,9 +36,9 @@
import com.gitblit.GitBlit;
import com.gitblit.models.Activity;
import com.gitblit.models.Activity.RepositoryCommit;
import com.gitblit.models.GravatarProfile;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
import com.google.gson.reflect.TypeToken;
src/com/gitblit/utils/ArrayUtils.java
@@ -15,7 +15,9 @@
 */
package com.gitblit.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
@@ -41,4 +43,32 @@
    public static boolean isEmpty(Collection<?> collection) {
        return collection == null || collection.size() == 0;
    }
    public static String toString(Collection<?> collection) {
        if (isEmpty(collection)) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (Object o : collection) {
            sb.append(o.toString()).append(", ");
        }
        // trim trailing comma-space
        sb.setLength(sb.length() - 2);
        return sb.toString();
    }
    public static Collection<String> fromString(String value) {
        if (StringUtils.isEmpty(value)) {
            value = "";
        }
        List<String> list = new ArrayList<String>();
        String [] values = value.split(",|;");
        for (String v : values) {
            String string = v.trim();
            if (!StringUtils.isEmpty(string)) {
                list.add(string);
            }
        }
        return list;
    }
}
src/com/gitblit/utils/FileUtils.java
@@ -176,19 +176,17 @@
    public static long folderSize(File directory) {
        if (directory == null || !directory.exists()) {
            return -1;
        }
        if (directory.isFile()) {
            return directory.length();
        }
        long length = 0;
        for (File file : directory.listFiles()) {
            if (file.isFile()) {
                length += file.length();
            } else {
        }
        if (directory.isDirectory()) {
            long length = 0;
            for (File file : directory.listFiles()) {
                length += folderSize(file);
            }
            return length;
        } else if (directory.isFile()) {
            return directory.length();
        }
        return length;
        return 0;
    }
    /**
@@ -276,4 +274,19 @@
            return path.getAbsoluteFile();
        }
    }
    public static File resolveParameter(String parameter, File aFolder, String path) {
        if (aFolder == null) {
            // strip any parameter reference
            path = path.replace(parameter, "").trim();
            if (path.length() > 0 && path.charAt(0) == '/') {
                // strip leading /
                path = path.substring(1);
            }
        } else if (path.contains(parameter)) {
            // replace parameter with path
            path = path.replace(parameter, aFolder.getAbsolutePath());
        }
        return new File(path);
    }
}
src/com/gitblit/utils/IssueUtils.java
@@ -76,9 +76,9 @@
        public abstract boolean accept(IssueModel issue);
    }
    public static final String GB_ISSUES = "refs/heads/gb-issues";
    public static final String GB_ISSUES = "refs/gitblit/issues";
    static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
    static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.class);
    /**
     * Log an error message and exception.
@@ -111,7 +111,13 @@
     * @return a refmodel for the gb-issues branch or null
     */
    public static RefModel getIssuesBranch(Repository repository) {
        return JGitUtils.getBranch(repository, "gb-issues");
        List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
        for (RefModel ref : refs) {
            if (ref.reference.getName().equals(GB_ISSUES)) {
                return ref;
            }
        }
        return null;
    }
    /**
@@ -374,7 +380,7 @@
        String issuePath = getIssuePath(issueId);
        RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();
        byte[] content = JGitUtils
                .getByteContent(repository, tree, issuePath + "/" + attachment.id);
                .getByteContent(repository, tree, issuePath + "/" + attachment.id, false);
        attachment.content = content;
        attachment.size = content.length;
        return attachment;
@@ -394,7 +400,7 @@
    public static IssueModel createIssue(Repository repository, Change change) {
        RefModel issuesBranch = getIssuesBranch(repository);
        if (issuesBranch == null) {
            JGitUtils.createOrphanBranch(repository, "gb-issues", null);
            JGitUtils.createOrphanBranch(repository, GB_ISSUES, null);
        }
        if (StringUtils.isEmpty(change.author)) {
@@ -471,7 +477,7 @@
        RefModel issuesBranch = getIssuesBranch(repository);
        if (issuesBranch == null) {
            throw new RuntimeException("gb-issues branch does not exist!");
            throw new RuntimeException(GB_ISSUES + " does not exist!");
        }
        if (StringUtils.isEmpty(issueId)) {
src/com/gitblit/utils/JGitUtils.java
@@ -537,7 +537,7 @@
     * @param path
     * @return content as a byte []
     */
    public static byte[] getByteContent(Repository repository, RevTree tree, final String path) {
    public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) {
        RevWalk rw = new RevWalk(repository);
        TreeWalk tw = new TreeWalk(repository);
        tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
@@ -572,7 +572,9 @@
                }
            }
        } catch (Throwable t) {
            error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name());
            if (throwError) {
                error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name());
            }
        } finally {
            rw.dispose();
            tw.release();
@@ -591,7 +593,7 @@
     * @return UTF-8 string content
     */
    public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) {
        byte[] content = getByteContent(repository, tree, blobPath);
        byte[] content = getByteContent(repository, tree, blobPath, true);
        if (content == null) {
            return null;
        }
@@ -741,11 +743,7 @@
                df.setDetectRenames(true);
                List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
                for (DiffEntry diff : diffs) {
                    String objectId = null;
                    if (FileMode.GITLINK.equals(diff.getNewMode())) {
                        objectId = diff.getNewId().name();
                    }
                    String objectId = diff.getNewId().name();
                    if (diff.getChangeType().equals(ChangeType.DELETE)) {
                        list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
                                .getNewMode().getBits(), objectId, commit.getId().getName(), diff
@@ -1457,6 +1455,20 @@
            int maxCount) {
        return getRefs(repository, Constants.R_NOTES, fullName, maxCount);
    }
    /**
     * Returns the list of refs in the specified base ref. If repository does
     * not exist or is empty, an empty list is returned.
     *
     * @param repository
     * @param fullName
     *            if true, /refs/yadayadayada is returned. If false,
     *            yadayadayada is returned.
     * @return list of refs
     */
    public static List<RefModel> getRefs(Repository repository, String baseRef) {
        return getRefs(repository, baseRef, true, -1);
    }
    /**
     * Returns a list of references in the repository matching "refs". If the
@@ -1570,7 +1582,7 @@
     */
    public static List<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
        List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
        byte [] blob = getByteContent(repository, tree, ".gitmodules");
        byte [] blob = getByteContent(repository, tree, ".gitmodules", false);
        if (blob == null) {
            return list;
        }
@@ -1603,6 +1615,32 @@
            }
        }
        return null;
    }
    public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) {
        String commitId = null;
        RevWalk rw = new RevWalk(repository);
        TreeWalk tw = new TreeWalk(repository);
        tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
        try {
            tw.reset(commit.getTree());
            while (tw.next()) {
                if (tw.isSubtree() && !path.equals(tw.getPathString())) {
                    tw.enterSubtree();
                    continue;
                }
                if (FileMode.GITLINK == tw.getFileMode(0)) {
                    commitId = tw.getObjectId(0).getName();
                    break;
                }
            }
        } catch (Throwable t) {
            error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name());
        } finally {
            rw.dispose();
            tw.release();
        }
        return commitId;
    }
    /**
@@ -1720,4 +1758,18 @@
        }
        return success;
    }
    /**
     * Reads the sparkleshare id, if present, from the repository.
     *
     * @param repository
     * @return an id or null
     */
    public static String getSparkleshareId(Repository repository) {
        byte[] content = getByteContent(repository, null, ".sparkleshare", false);
        if (content == null) {
            return null;
        }
        return StringUtils.decodeString(content);
    }
}
src/com/gitblit/utils/MetricUtils.java
@@ -210,6 +210,7 @@
                            p = rev.getAuthorIdent().getEmailAddress().toLowerCase();
                        }
                    }
                    p = p.replace('\n',' ').replace('\r',  ' ').trim();
                    if (!metricMap.containsKey(p)) {
                        metricMap.put(p, new Metric(p));
                    }
src/com/gitblit/utils/PushLogUtils.java
New file
@@ -0,0 +1,344 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.utils;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.PushLogEntry;
import com.gitblit.models.RefModel;
import com.gitblit.models.UserModel;
/**
 * Utility class for maintaining a pushlog within a git repository on an
 * orphan branch.
 *
 * @author James Moger
 *
 */
public class PushLogUtils {
    public static final String GB_PUSHES = "refs/gitblit/pushes";
    static final Logger LOGGER = LoggerFactory.getLogger(PushLogUtils.class);
    /**
     * Log an error message and exception.
     *
     * @param t
     * @param repository
     *            if repository is not null it MUST be the {0} parameter in the
     *            pattern.
     * @param pattern
     * @param objects
     */
    private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
        List<Object> parameters = new ArrayList<Object>();
        if (objects != null && objects.length > 0) {
            for (Object o : objects) {
                parameters.add(o);
            }
        }
        if (repository != null) {
            parameters.add(0, repository.getDirectory().getAbsolutePath());
        }
        LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
    }
    /**
     * Returns a RefModel for the gb-pushes branch in the repository. If the
     * branch can not be found, null is returned.
     *
     * @param repository
     * @return a refmodel for the gb-pushes branch or null
     */
    public static RefModel getPushLogBranch(Repository repository) {
        List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
        for (RefModel ref : refs) {
            if (ref.reference.getName().equals(GB_PUSHES)) {
                return ref;
            }
        }
        return null;
    }
    /**
     * Updates a push log.
     *
     * @param user
     * @param repository
     * @param commands
     * @return true, if the update was successful
     */
    public static boolean updatePushLog(UserModel user, Repository repository,
            Collection<ReceiveCommand> commands) {
        RefModel pushlogBranch = getPushLogBranch(repository);
        if (pushlogBranch == null) {
            JGitUtils.createOrphanBranch(repository, GB_PUSHES, null);
        }
        boolean success = false;
        String message = "push";
        try {
            ObjectId headId = repository.resolve(GB_PUSHES + "^{commit}");
            ObjectInserter odi = repository.newObjectInserter();
            try {
                // Create the in-memory index of the push log entry
                DirCache index = createIndex(repository, headId, commands);
                ObjectId indexTreeId = index.writeTree(odi);
                PersonIdent ident = new PersonIdent(user.getDisplayName(),
                        user.emailAddress == null ? user.username:user.emailAddress);
                // Create a commit object
                CommitBuilder commit = new CommitBuilder();
                commit.setAuthor(ident);
                commit.setCommitter(ident);
                commit.setEncoding(Constants.CHARACTER_ENCODING);
                commit.setMessage(message);
                commit.setParentId(headId);
                commit.setTreeId(indexTreeId);
                // Insert the commit into the repository
                ObjectId commitId = odi.insert(commit);
                odi.flush();
                RevWalk revWalk = new RevWalk(repository);
                try {
                    RevCommit revCommit = revWalk.parseCommit(commitId);
                    RefUpdate ru = repository.updateRef(GB_PUSHES);
                    ru.setNewObjectId(commitId);
                    ru.setExpectedOldObjectId(headId);
                    ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
                    Result rc = ru.forceUpdate();
                    switch (rc) {
                    case NEW:
                    case FORCED:
                    case FAST_FORWARD:
                        success = true;
                        break;
                    case REJECTED:
                    case LOCK_FAILURE:
                        throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
                                ru.getRef(), rc);
                    default:
                        throw new JGitInternalException(MessageFormat.format(
                                JGitText.get().updatingRefFailed, GB_PUSHES, commitId.toString(),
                                rc));
                    }
                } finally {
                    revWalk.release();
                }
            } finally {
                odi.release();
            }
        } catch (Throwable t) {
            error(t, repository, "Failed to commit pushlog entry to {0}");
        }
        return success;
    }
    /**
     * Creates an in-memory index of the push log entry.
     *
     * @param repo
     * @param headId
     * @param commands
     * @return an in-memory index
     * @throws IOException
     */
    private static DirCache createIndex(Repository repo, ObjectId headId,
            Collection<ReceiveCommand> commands) throws IOException {
        DirCache inCoreIndex = DirCache.newInCore();
        DirCacheBuilder dcBuilder = inCoreIndex.builder();
        ObjectInserter inserter = repo.newObjectInserter();
        long now = System.currentTimeMillis();
        Set<String> ignorePaths = new TreeSet<String>();
        try {
            // add receive commands to the temporary index
            for (ReceiveCommand command : commands) {
                // use the ref names as the path names
                String path = command.getRefName();
                ignorePaths.add(path);
                StringBuilder change = new StringBuilder();
                change.append(command.getType().name()).append(' ');
                switch (command.getType()) {
                case CREATE:
                    change.append(ObjectId.zeroId().getName());
                    change.append(' ');
                    change.append(command.getNewId().getName());
                    break;
                case UPDATE:
                case UPDATE_NONFASTFORWARD:
                    change.append(command.getOldId().getName());
                    change.append(' ');
                    change.append(command.getNewId().getName());
                    break;
                case DELETE:
                    change = null;
                    break;
                }
                if (change == null) {
                    // ref deleted
                    continue;
                }
                String content = change.toString();
                // create an index entry for this attachment
                final DirCacheEntry dcEntry = new DirCacheEntry(path);
                dcEntry.setLength(content.length());
                dcEntry.setLastModified(now);
                dcEntry.setFileMode(FileMode.REGULAR_FILE);
                // insert object
                dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")));
                // add to temporary in-core index
                dcBuilder.add(dcEntry);
            }
            // Traverse HEAD to add all other paths
            TreeWalk treeWalk = new TreeWalk(repo);
            int hIdx = -1;
            if (headId != null)
                hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
            treeWalk.setRecursive(true);
            while (treeWalk.next()) {
                String path = treeWalk.getPathString();
                CanonicalTreeParser hTree = null;
                if (hIdx != -1)
                    hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
                if (!ignorePaths.contains(path)) {
                    // add entries from HEAD for all other paths
                    if (hTree != null) {
                        // create a new DirCacheEntry with data retrieved from
                        // HEAD
                        final DirCacheEntry dcEntry = new DirCacheEntry(path);
                        dcEntry.setObjectId(hTree.getEntryObjectId());
                        dcEntry.setFileMode(hTree.getEntryFileMode());
                        // add to temporary in-core index
                        dcBuilder.add(dcEntry);
                    }
                }
            }
            // release the treewalk
            treeWalk.release();
            // finish temporary in-core index used for this commit
            dcBuilder.finish();
        } finally {
            inserter.release();
        }
        return inCoreIndex;
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository) {
        return getPushLog(repositoryName, repository, null, -1);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int maxCount) {
        return getPushLog(repositoryName, repository, null, maxCount);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate) {
        return getPushLog(repositoryName, repository, minimumDate, -1);
    }
    public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate, int maxCount) {
        List<PushLogEntry> list = new ArrayList<PushLogEntry>();
        RefModel ref = getPushLogBranch(repository);
        if (ref == null) {
            return list;
        }
        List<RevCommit> pushes;
        if (minimumDate == null) {
            pushes = JGitUtils.getRevLog(repository, GB_PUSHES, 0, maxCount);
        } else {
            pushes = JGitUtils.getRevLog(repository, GB_PUSHES, minimumDate);
        }
        for (RevCommit push : pushes) {
            if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
                // skip gitblit/internal commits
                continue;
            }
            Date date = push.getAuthorIdent().getWhen();
            UserModel user = new UserModel(push.getAuthorIdent().getEmailAddress());
            user.displayName = push.getAuthorIdent().getName();
            PushLogEntry log = new PushLogEntry(repositoryName, date, user);
            list.add(log);
            List<PathChangeModel> changedRefs = JGitUtils.getFilesInCommit(repository, push);
            for (PathChangeModel change : changedRefs) {
                switch (change.changeType) {
                case DELETE:
                    log.updateRef(change.path, ReceiveCommand.Type.DELETE);
                    break;
                case ADD:
                    log.updateRef(change.path, ReceiveCommand.Type.CREATE);
                default:
                    String content = JGitUtils.getStringContent(repository, push.getTree(), change.path);
                    String [] fields = content.split(" ");
                    log.updateRef(change.path, ReceiveCommand.Type.valueOf(fields[0]));
                    String oldId = fields[1];
                    String newId = fields[2];
                    List<RevCommit> pushedCommits = JGitUtils.getRevLog(repository, oldId, newId);
                    for (RevCommit pushedCommit : pushedCommits) {
                        log.addCommit(change.path, pushedCommit);
                    }
                }
            }
        }
        Collections.sort(list);
        return list;
    }
}
src/com/gitblit/utils/StringUtils.java
@@ -719,4 +719,18 @@
        Matcher m = p.matcher(input);
        return m.matches();
    }
    /**
     * Removes new line and carriage return chars from a string.
     * If input value is null an empty string is returned.
     *
     * @param input
     * @return a sanitized or empty string
     */
    public static String removeNewlines(String input) {
        if (input == null) {
            return "";
        }
        return input.replace('\n',' ').replace('\r',  ' ').trim();
    }
}
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -440,4 +440,6 @@
gb.validity = validity
gb.siteName = site name
gb.siteNameDescription = short, descriptive name of your server 
gb.excludeFromActivity = exclude from activity page
gb.excludeFromActivity = exclude from activity page
gb.isSparkleshared = repository is Sparkleshared
gb.owners = owners
src/com/gitblit/wicket/GitBlitWebApp_es.properties
@@ -163,7 +163,7 @@
gb.lastLogin = \u00DAltimo acceso
gb.skipSizeCalculation = Saltar comprobaciones de tama\u00F1o
gb.skipSizeCalculationDescription = No calcular el tama\u00F1o del repositorio (Reduce tiempo de carga de la p\u00E1gina)
gb.skipSummaryMetrics = Saltar el resumen de estad\u00EDsticas
gb.skipSummaryMetrics = Saltar resumen de estad\u00EDsticas
gb.skipSummaryMetricsDescription = No calcular estad\u00EDsticas (Reduce tiempo de carga de la p\u00E1gina)
gb.accessLevel = Nivel de acceso
gb.default = Predeterminado
@@ -341,8 +341,103 @@
gb.canCreate = Puede crear
gb.canCreateDescription = Puede crear repositorios personales
gb.illegalPersonalRepositoryLocation = Tu repositorio personal debe estar ubicado en \"{0}\"
gb.verifyCommitter = Acreditar consignador
gb.verifyCommitterDescription = Require que la acreditaci\u00F3n del consignador coincida con la de la cuenta del usuario en Gitblt (es necesario "--no-ff" al empujar para que consignador se acredite)
gb.verifyCommitter = Consignador acreditado
gb.verifyCommitterDescription = Require que la acreditaci\u00F3n del consignador coincida con la de la cuenta del usuario en Gitblt
gb.verifyCommitterNote = es obligatorio "--no-ff" al empujar para que el consignador se acredite
gb.repositoryPermissions = Permisos del repositorio
gb.userPermissions = Permisos de usuarios
gb.teamPermissions = Permisos de equipos
gb.add = A\u00F1adir
gb.noPermission = BORRAR ESTE PERMISO
gb.excludePermission = {0} (excluir)
gb.viewPermission = {0} (ver)
gb.clonePermission = {0} (clonar)
gb.pushPermission = {0} (empujar)
gb.createPermission = {0} (empujar, ref creaci\u00F3n)
gb.deletePermission = {0} (empujar, ref creaci\u00F3n+borrado)
gb.rewindPermission = {0} (empujar, ref creaci\u00F3n+borrado+supresi\u00F3n)
gb.permission = Permisos
gb.regexPermission = Estos permisos se ajustan desde la expresi\u00F3n regulare \"{0}\"
gb.accessDenied = Acceso denegado
gb.busyCollectingGarbage = Perd\u00F3n, Gitblit est\u00E1 ocupado quitando basura de {0}
gb.gcPeriod = Periodo para GC
gb.gcPeriodDescription = Duraci\u00F3n entre periodos de limpieza
gb.gcThreshold = L\u00EDmites para GC
gb.gcThresholdDescription = Tama\u00F1o m\u00EDnimo total de objetos sueltos para activar la recolecci\u00F3n inmediata de basura
gb.ownerPermission = Propietario del repositorio
gb.administrator = Admin
gb.administratorPermission = Administrador de Gitblit
gb.team = Equipo
gb.teamPermission = Permisos ajustados para \"{0}\" mienbros de equipo
gb.missing = \u00A1Omitido!
gb.missingPermission = \u00A1Falta el repositorio de este permiso!
gb.mutable = Alterables
gb.specified = Espec\u00EDficos
gb.effective = Efectivos
gb.organizationalUnit = Unidad de organizaci\u00F3n
gb.organization = Organizaci\u00F3n
gb.locality = Localidad
gb.stateProvince = Estado o provincia
gb.countryCode = C\u00F3digo postal
gb.properties = Propiedades
gb.issued = Publicado
gb.expires = Expira
gb.expired = Expirado
gb.expiring = Concluido
gb.revoked = Revocado
gb.serialNumber = N\u00FAmero de serie
gb.certificates = Certificados
gb.newCertificate = Nuevo certificado
gb.revokeCertificate = Revocar certificado
gb.sendEmail = Enviar correo
gb.passwordHint = Recordatorio de contrase\u00F1a
gb.ok = ok
gb.invalidExpirationDate = \u00A1La fecha de expiraci\u00F3n no es v\u00E1lida!
gb.passwordHintRequired = \u00A1Se requiere una pista para la contrase\u00F1a!
gb.viewCertificate = Ver certificado
gb.subject = Asunto
gb.issuer = Emisor
gb.validFrom = V\u00E1lido desde
gb.validUntil = V\u00E1lido hasta
gb.publicKey = Clave p\u00FAblica
gb.signatureAlgorithm = Algoritmo de firma
gb.sha1FingerPrint = Huella digital SHA-1
gb.md5FingerPrint = Huella digital MD5
gb.reason = Motivo
gb.revokeCertificateReason = Por favor, selecciona un motivo por el que revocas el certificado
gb.unspecified = Sin especificar
gb.keyCompromise = Clave de compromiso
gb.caCompromise = Compromiso CA
gb.affiliationChanged = Afiliaci\u00F3n cambiada
gb.superseded = Sustituida
gb.cessationOfOperation = Cese de operaci\u00F3n
gb.privilegeWithdrawn = Privilegios retirados
gb.time.inMinutes = en {0} mints
gb.time.inHours = en {0} horas
gb.time.inDays = en {0} d\u00EDas
gb.hostname = Nombre de host
gb.hostnameRequired = Por favor, introduzca un nombre de host
gb.newSSLCertificate = Nuevo certificado SSL del servidor
gb.newCertificateDefaults = Nuevo certificado predeterminado
gb.duration = Duraci\u00F3n
gb.certificateRevoked = El cretificado {0,n\u00FAmero,0} ha sido revocado
gb.clientCertificateGenerated = Nuevo certificado de cliente generado correctamente para {0}
gb.sslCertificateGenerated = Nuevo certificado de SSL generado correctamente para {0}
gb.newClientCertificateMessage = AVISO:\nLa 'contrase\u00F1a' no es la contrase\u00F1a del usuario, es la contrase\u00F1a para proteger el almac\u00E9n de claves del usuario. Esta contrase\u00F1a no se guarda por lo que tambi\u00E9n debe introducirse una "pista" que ser\u00E1 incluida en las instrucciones LEEME del usuario.
gb.certificate = Certificado
gb.emailCertificateBundle = Correo del cliente para el paquete del certificado
gb.pleaseGenerateClientCertificate = Por favor, genera un certificado de cliente para {0}
gb.clientCertificateBundleSent = Paquete de certificado de cliente {0} enviado
gb.enterKeystorePassword =  Por favor, introduzca la contrase\u00F1a del almac\u00E9n de claves de Gitblit
gb.warning = Advertencia
gb.jceWarning = Tu entorno de trabajo JAVA no contiene los archivos \"JCE Unlimited Strength Jurisdiction Policy\".\nEsto limita la longitud de la contrase\u00F1a que puedes usuar para cifrar el almac\u00E9n de claves a 7 caracteres.\nEstos archivos opcionales puedes descargarlos desde Oracle.\n\n\u00BFQuieres continuar y generar la infraestructura de certificados de todos modos?\n\nSi respondes No tu navegador te dirigir\u00E1 a la p\u00E1gina de descarga de Oracle para que pueda descargar dichos archivos.
gb.maxActivityCommits = Actividad m\u00E1xima de consignas
gb.maxActivityCommitsDescription = N\u00FAmero m\u00E1ximo de consignas a incluir en la p\u00E1gina de actividad
gb.noMaximum = Sin m\u00E1ximos
gb.attributes = Atributos
gb.serveCertificate = Servidor https con este certificado
gb.sslCertificateGeneratedRestart = Certificado SSL generado correctamente para  {0}.\nDebes reiniciar Gitblit para usar el nuevo certificado.\n\nSi lo has iniciado con la opci\u00F3n  '--alias' deber\u00E1s ajustar dicha opci\u00F3n a ''--alias {0}''.
gb.validity = Vigencia
gb.siteName = Nombre del sitio
gb.siteNameDescription = Nombre corto y descriptivo de tu servidor
gb.excludeFromActivity = Excluir de la p\u00E1gina de actividad
src/com/gitblit/wicket/GitBlitWebApp_ko.properties
@@ -237,7 +237,7 @@
gb.passwordChangeAborted = \uD328\uC2A4\uC6CC\uB4DC \uBCC0\uACBD \uCDE8\uC18C\uB428.
gb.pleaseSetRepositoryName = \uC800\uC7A5\uC18C \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694!
gb.illegalLeadingSlash = \uC800\uC7A5\uC18C \uC774\uB984 \uB610\uB294 \uD3F4\uB354\uB294 (/) \uB85C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
gb.illegalRelativeSlash = \uC0C1\uB300 \uACBD\uB85C \uC9C0\uC815 (../) \uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
gb.illegalRelativeSlash = \uC0C1\uB300 \uACBD\uB85C \uC9C0\uC815 (../) \uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4..
gb.illegalCharacterRepositoryName = \uBB38\uC790 ''{0}'' \uC800\uC7A5\uC18C \uC774\uB984\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4\uC694!
gb.selectAccessRestriction = \uC811\uC18D \uAD8C\uD55C\uC744 \uC120\uD0DD\uD558\uC138\uC694!
gb.selectFederationStrategy = \uD398\uB354\uB808\uC774\uC158 \uC815\uCC45\uC744 \uC120\uD0DD\uD558\uC138\uC694!
@@ -287,7 +287,7 @@
gb.line = \uB77C\uC778
gb.content = \uB0B4\uC6A9
gb.empty = empty
gb.inherited = inherited
gb.inherited = \uC0C1\uC18D
gb.deleteRepository = \"{0}\" \uC800\uC7A5\uC18C\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
gb.repositoryDeleted = ''{0}'' \uC800\uC7A5\uC18C \uC0AD\uC81C\uB428.
gb.repositoryDeleteFailed = ''{0}'' \uC800\uC7A5\uC18C \uC0AD\uC81C \uC2E4\uD328!
@@ -315,3 +315,129 @@
gb.allowNamedDescription = \uC774\uB984\uC73C\uB85C \uC720\uC800\uB098 \uD300\uC5D0\uAC8C \uAD8C\uD55C \uBD80\uC5EC
gb.markdownFailure = \uB9C8\uD06C\uB2E4\uC6B4 \uCEE8\uD150\uD2B8 \uD30C\uC2F1 \uC624\uB958!
gb.clearCache = \uCE90\uC2DC \uC9C0\uC6B0\uAE30
gb.projects = \uD504\uB85C\uC81D\uD2B8\uB4E4
gb.project = \uD504\uB85C\uC81D\uD2B8
gb.allProjects = \uBAA8\uB4E0 \uD504\uB85C\uC81D\uD2B8
gb.copyToClipboard = \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC
gb.fork = \uD3EC\uD06C
gb.forks = \uD3EC\uD06C
gb.forkRepository = fork {0}?
gb.repositoryForked = {0} \uD3EC\uD06C\uB428
gb.repositoryForkFailed= \uD3EC\uD06C\uC2E4\uD328
gb.personalRepositories = \uAC1C\uC778 \uC800\uC7A5\uC18C
gb.allowForks = \uD3EC\uD06C \uD5C8\uC6A9
gb.allowForksDescription = \uC774 \uC800\uC7A5\uC18C\uB97C \uC778\uC99D\uB41C \uC720\uC800\uC5D0\uAC70 \uD3EC\uD06C \uD5C8\uC6A9
gb.forkedFrom = forked from
gb.canFork = \uD3EC\uD06C \uAC00\uB2A5
gb.canForkDescription = \uD5C8\uC6A9\uB41C \uC800\uC7A5\uC18C\uB97C \uAC1C\uC778 \uC800\uC7A5\uC18C\uC5D0 \uD3EC\uD06C\uD560 \uC218 \uC788\uC74C
gb.myFork = \uB0B4 \uD3EC\uD06C \uBCF4\uAE30
gb.forksProhibited = \uD3EC\uD06C \uCC28\uB2E8\uB428
gb.forksProhibitedWarning = \uC774 \uC800\uC7A5\uC18C\uB294 \uD3EC\uD06C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC74C
gb.noForks = {0} \uB294 \uD3EC\uD06C \uC5C6\uC74C
gb.forkNotAuthorized = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. {0} \uD3EC\uD06C\uC5D0 \uC811\uC18D \uC778\uC99D\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
gb.forkInProgress = \uD504\uD06C \uC9C4\uD589 \uC911
gb.preparingFork = \uD3EC\uD06C \uC900\uBE44 \uC911...
gb.isFork = \uD3EC\uD06C\uD55C
gb.canCreate = \uC0DD\uC131 \uAC00\uB2A5
gb.canCreateDescription = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB97C \uB9CC\uB4E4 \uC218 \uC788\uC74C
gb.illegalPersonalRepositoryLocation = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB294 \uBC18\uB4DC\uC2DC \"{0}\" \uC5D0 \uC704\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.
gb.verifyCommitter = \uCEE4\uBBF8\uD130 \uD655\uC778
gb.verifyCommitterDescription = \uCEE4\uBBF8\uD130 ID \uB294 Gitblit ID \uC640 \uB9E4\uCE58\uB418\uC5B4\uC57C \uD568
gb.verifyCommitterNote = \uBAA8\uB4E0 \uBA38\uC9C0\uB294 \uCEE4\uBBF8\uD130 ID \uB97C \uC801\uC6A9\uD558\uAE30 \uC704\uD574 "--no-ff" \uC635\uC158 \uD544\uC694
gb.repositoryPermissions = \uC800\uC7A5\uC18C \uAD8C\uD55C
gb.userPermissions = \uC720\uC800 \uAD8C\uD55C
gb.teamPermissions = \uD300 \uAD8C\uD55C
gb.add = \uCD94\uAC00
gb.noPermission = \uC774 \uAD8C\uD55C \uC0AD\uC81C
gb.excludePermission = {0} (\uC81C\uC678)
gb.viewPermission = {0} (\uBCF4\uAE30)
gb.clonePermission = {0} (\uD074\uB860)
gb.pushPermission = {0} (\uD478\uC2DC)
gb.createPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131)
gb.deletePermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C)
gb.rewindPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C+\uB418\uB3CC\uB9AC\uAE30)
gb.permission = \uAD8C\uD55C
gb.regexPermission = \uC774 \uAD8C\uD55C\uC740 \uC815\uADDC\uC2DD \"{0}\" \uB85C\uBD80\uD130 \uC124\uC815\uB428
gb.accessDenied = \uC811\uC18D \uAC70\uBD80
gb.busyCollectingGarbage = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. Gitblit \uC740 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158 \uC911\uC785\uB2C8\uB2E4. {0}
gb.gcPeriod = GC \uC8FC\uAE30
gb.gcPeriodDescription = \uAC00\uBE44\uC9C0 \uD074\uB809\uC158\uAC04\uC758 \uC2DC\uAC04 \uAC04\uACA9
gb.gcThreshold = GC \uAE30\uC900\uC810
gb.gcThresholdDescription = \uC870\uAE30 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158\uC744 \uBC1C\uC0DD\uC2DC\uD0A4\uAE30 \uC704\uD55C \uC624\uBE0C\uC81D\uD2B8\uB4E4\uC758 \uCD5C\uC18C \uC804\uCCB4 \uD06C\uAE30
gb.ownerPermission = \uC800\uC7A5\uC18C \uC624\uB108
gb.administrator = \uAD00\uB9AC\uC790
gb.administratorPermission = Gitblit \uAD00\uB9AC\uC790
gb.team = \uD300
gb.teamPermission = \"{0}\" \uD300 \uBA64\uBC84\uC5D0 \uAD8C\uD55C \uC124\uC815\uB428
gb.missing = \uB204\uB77D!
gb.missingPermission = \uC774 \uAD8C\uD55C\uC744 \uC704\uD55C \uC800\uC7A5\uC18C \uB204\uB77D!
gb.mutable = \uAC00\uBCC0
gb.specified = \uC9C0\uC815\uB41C
gb.effective = \uD6A8\uACFC\uC801
gb.organizationalUnit = \uC870\uC9C1
gb.organization = \uAE30\uAD00
gb.locality = \uC704\uCE58
gb.stateProvince = \uB3C4 \uB610\uB294 \uC8FC
gb.countryCode = \uAD6D\uAC00\uCF54\uB4DC
gb.properties = \uC18D\uC131
gb.issued = \uBC1C\uAE09\uB428
gb.expires = \uB9CC\uB8CC
gb.expired = \uB9CC\uB8CC\uB428
gb.expiring = \uB9CC\uB8CC\uC911
gb.revoked = \uD3D0\uAE30\uB428
gb.serialNumber = \uC77C\uB828\uBC88\uD638
gb.certificates = \uC778\uC99D\uC11C
gb.newCertificate = \uC0C8 \uC778\uC99D\uC11C
gb.revokeCertificate = \uC778\uC99D\uC11C \uD3D0\uAE30
gb.sendEmail = \uBA54\uC77C \uBCF4\uB0B4\uAE30
gb.passwordHint = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8
gb.ok = ok
gb.invalidExpirationDate = \uB9D0\uB8CC\uC77C\uC790 \uC624\uB958!
gb.passwordHintRequired = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8 \uD544\uC218!
gb.viewCertificate = \uC778\uC99D\uC11C \uBCF4\uAE30
gb.subject = \uC774\uB984
gb.issuer = \uBC1C\uAE09\uC790
gb.validFrom = \uC720\uD6A8\uAE30\uAC04 (\uC2DC\uC791)
gb.validUntil = \uC720\uD6A8\uAE30\uAC04 (\uB05D)
gb.publicKey = \uACF5\uAC1C\uD0A4
gb.signatureAlgorithm = \uC11C\uBA85 \uC54C\uACE0\uB9AC\uC998
gb.sha1FingerPrint = SHA-1 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
gb.md5FingerPrint = MD5 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
gb.reason = \uC774\uC720
gb.revokeCertificateReason = \uC778\uC99D\uC11C \uD574\uC9C0\uC774\uC720\uB97C \uC120\uD0DD\uD558\uC138\uC694
gb.unspecified = \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC74C
gb.keyCompromise = \uD0A4 \uC190\uC0C1
gb.caCompromise = CA \uC190\uC0C1
gb.affiliationChanged = \uAD00\uACC4 \uBCC0\uACBD\uB428
gb.superseded = \uB300\uCCB4\uB428
gb.cessationOfOperation = \uC6B4\uC601 \uC911\uC9C0
gb.privilegeWithdrawn = \uAD8C\uD55C \uCCA0\uD68C\uB428
gb.time.inMinutes = {0} \uBD84
gb.time.inHours = {0} \uC2DC\uAC04
gb.time.inDays = {0} \uC77C
gb.hostname = \uD638\uC2A4\uD2B8\uBA85
gb.hostnameRequired = \uD638\uC2A4\uD2B8\uBA85\uC744 \uC785\uB825\uD558\uC138\uC694
gb.newSSLCertificate = \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C
gb.newCertificateDefaults = \uC0C8 \uC778\uC99D\uC11C \uAE30\uBCF8
gb.duration = \uAE30\uAC04
gb.certificateRevoked = \uC778\uC99D\uC11C {0,number,0} \uB294 \uD3D0\uAE30\uB418\uC5C8\uC2B5\uB2C8\uB2E4
gb.clientCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
gb.sslCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uC11C\uBC84 SSL \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
gb.newClientCertificateMessage = \uB178\uD2B8:\n'\uD328\uC2A4\uC6CC\uB4DC' \uB294 \uC720\uC800\uC758 \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uC544\uB2C8\uB77C \uC720\uC800\uC758 \uD0A4\uC2A4\uD1A0\uC5B4 \uB97C \uBCF4\uD638\uD558\uAE30 \uC704\uD55C \uAC83\uC785\uB2C8\uB2E4. \uC774 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uC800\uC7A5\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uC0AC\uC6A9\uC790 README \uC9C0\uCE68\uC5D0 \uD3EC\uD568\uB420 '\uD78C\uD2B8' \uB97C \uBC18\uB4DC\uC2DC \uC785\uB825\uD574\uC57C \uD569\uB2C8\uB2E4.
gb.certificate = \uC778\uC99D\uC11C
gb.emailCertificateBundle = \uC774\uBA54\uC77C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4
gb.pleaseGenerateClientCertificate = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C\uB97C \uC0DD\uC131\uD558\uC138\uC694
gb.clientCertificateBundleSent = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4 \uBC1C\uC1A1\uB428
gb.enterKeystorePassword = Gitblit \uD0A4\uC2A4\uD1A0\uC5B4 \uD328\uC2A4\uC6CC\uB4DC\uB97C \uC785\uB825\uD558\uC138\uC694
gb.warning = \uACBD\uACE0
gb.jceWarning = \uC790\uBC14 \uC2E4\uD589\uD658\uACBD\uC5D0 \"JCE Unlimited Strength Jurisdiction Policy\" \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC774\uAC83\uC740 \uD0A4\uC800\uC7A5\uC18C \uC554\uD638\uD654\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uD328\uC2A4\uC6CC\uB4DC\uC758 \uAE38\uC774\uB294 7\uC790\uB85C \uC81C\uD55C\uD569\uB2C8\uB2E4.\n\uC774 \uC815\uCC45 \uD30C\uC77C\uC740 Oracle \uC5D0\uC11C \uC120\uD0DD\uC801\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD574\uC57C \uD569\uB2C8\uB2E4.\n\n\uBB34\uC2DC\uD558\uACE0 \uC778\uC99D\uC11C \uC778\uD504\uB77C\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?\n\n\uC544\uB2C8\uC624(No) \uB77C\uACE0 \uB2F5\uD558\uBA74 \uC815\uCC45\uD30C\uC77C\uC744 \uB2E4\uC6B4\uBC1B\uC744 \uC218 \uC788\uB294 Oracle \uB2E4\uC6B4\uB85C\uB4DC \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC548\uB0B4\uD560 \uAC83\uC785\uB2C8\uB2E4.
gb.maxActivityCommits = \uCD5C\uB300 \uC561\uD2F0\uBE44\uD2F0 \uCEE4\uBC0B
gb.maxActivityCommitsDescription = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0 \uD45C\uC2DC\uD560 \uCD5C\uB300 \uCEE4\uBC0B \uC218
gb.noMaximum = \uBB34\uC81C\uD55C
gb.attributes = \uC18D\uC131
gb.serveCertificate = \uC774 \uC778\uC99D\uC11C\uB85C https \uC81C\uACF5
gb.sslCertificateGeneratedRestart = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C\uB97C \uC131\uACF5\uC801\uC73C\uB85C \uC0DD\uC131\uD558\uC600\uC2B5\uB2C8\uB2E4. \n\uC0C8 \uC778\uC99D\uC11C\uB97C \uC0AC\uC6A9\uD558\uB824\uBA74 Gitblit \uC744 \uC7AC\uC2DC\uC791 \uD574\uC57C \uD569\uB2C8\uB2E4.\n\n'--alias' \uD30C\uB77C\uBBF8\uD130\uB85C \uC2E4\uD589\uD55C\uB2E4\uBA74 ''--alias {0}'' \uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.
gb.validity = \uC720\uD6A8\uC131
gb.siteName = \uC0AC\uC774\uD2B8 \uC774\uB984
gb.siteNameDescription = \uC11C\uBC84\uC758 \uC9E6\uC740 \uC124\uBA85\uC774 \uD3EC\uD568\uB41C \uC774\uB984
gb.excludeFromActivity = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0\uC11C \uC81C\uC678
src/com/gitblit/wicket/GitBlitWebApp_nl.properties
New file
@@ -0,0 +1,443 @@
gb.repository = repositorie
gb.owner = eigenaar
gb.description = omschrijving
gb.lastChange = laatste wijziging
gb.refs = refs
gb.tag = tag
gb.tags = tags
gb.author = auteur
gb.committer = committer
gb.commit = commit
gb.tree = tree
gb.parent = parent
gb.url = URL
gb.history = historie
gb.raw = raw
gb.object = object
gb.ticketId = ticket id
gb.ticketAssigned = toegewezen
gb.ticketOpenDate = open datum
gb.ticketState = status
gb.ticketComments = commentaar
gb.view = view
gb.local = local
gb.remote = remote
gb.branches = branches
gb.patch = patch
gb.diff = diff
gb.log = log
gb.moreLogs = meer commits...
gb.allTags = alle tags...
gb.allBranches = alle branches...
gb.summary = samenvatting
gb.ticket = ticket
gb.newRepository = nieuwe repositorie
gb.newUser = nieuwe gebruiker
gb.commitdiff = commitdiff
gb.tickets = tickets
gb.pageFirst = eerste
gb.pagePrevious = vorige
gb.pageNext = volgende
gb.head = HEAD
gb.blame = blame
gb.login = aanmelden
gb.logout = afmelden
gb.username = gebruikersnaam
gb.password = wachtwoord
gb.tagger = tagger
gb.moreHistory = meer historie...
gb.difftocurrent = diff naar current
gb.search = zoeken
gb.searchForAuthor = Zoeken naar commits authored door
gb.searchForCommitter = Zoeken naar commits committed door
gb.addition = additie
gb.modification = wijziging
gb.deletion = verwijdering
gb.rename = hernoem
gb.metrics = metrieken
gb.stats = stats
gb.markdown = markdown
gb.changedFiles = gewijzigde bestanden
gb.filesAdded = {0} bestanden toegevoegd
gb.filesModified = {0} bestanden gewijzigd
gb.filesDeleted = {0} bestanden verwijderd
gb.filesCopied = {0} bestanden gekopieerd
gb.filesRenamed = {0} bestanden hernoemd
gb.missingUsername = Ontbrekende Gebruikersnaam
gb.edit = edit
gb.searchTypeTooltip = Selecteer Zoek Type
gb.searchTooltip = Zoek {0}
gb.delete = verwijder
gb.docs = docs
gb.accessRestriction = toegangsbeperking
gb.name = naam
gb.enableTickets = enable tickets
gb.enableDocs = enable docs
gb.save = opslaan
gb.showRemoteBranches = toon remote branches
gb.editUsers = wijzig gebruikers
gb.confirmPassword = bevestig wachtwoord
gb.restrictedRepositories = restricted repositories
gb.canAdmin = kan beheren
gb.notRestricted = anoniem view, clone, & push
gb.pushRestricted = geauthenticeerde push
gb.cloneRestricted = geauthenticeerde clone & push
gb.viewRestricted = geauthenticeerde view, clone, & push
gb.useTicketsDescription = readonly, gedistribueerde Ticgit issues
gb.useDocsDescription = enumereer Markdown documentatie in repositorie
gb.showRemoteBranchesDescription = toon remote branches
gb.canAdminDescription = kan Gitblit server beheren
gb.permittedUsers = toegestane gebruikers
gb.isFrozen = is bevroren
gb.isFrozenDescription = weiger push operaties
gb.zip = zip
gb.showReadme = toon readme
gb.showReadmeDescription = toon een \"readme\" Markdown bestand in de samenvattingspagina
gb.nameDescription = gebruik '/' voor het groeperen van repositories.  bijv. libraries/mycoollib.git
gb.ownerDescription = de eigenaar mag repository instellingen wijzigen
gb.blob = blob
gb.commitActivityTrend = commit activiteit trend
gb.commitActivityDOW = commit activiteit per dag van de week
gb.commitActivityAuthors = primaire auteurs op basis van commit activiteit
gb.feed = feed
gb.cancel = afbreken
gb.changePassword = wijzig wachtwoord
gb.isFederated = is gefedereerd
gb.federateThis = federeer deze repositorie
gb.federateOrigin = federeer deze origin
gb.excludeFromFederation = uitsluiten van federatie
gb.excludeFromFederationDescription = sluit gefedereerde Gitblit instances uit van het pullen van dit account
gb.tokens = federatie tokens
gb.tokenAllDescription = alle repositories, gebruikers, & instellingen
gb.tokenUnrDescription = alle repositories & gebruikers
gb.tokenJurDescription = alle repositories
gb.federatedRepositoryDefinitions = repositorie definities
gb.federatedUserDefinitions = gebruikersdefinities
gb.federatedSettingDefinitions = instellingendefinities
gb.proposals = federatie voorstellen
gb.received = ontvangen
gb.type = type
gb.token = token
gb.repositories = repositories
gb.proposal = voorstel
gb.frequency = frequentie
gb.folder = map
gb.lastPull = laatste pull
gb.nextPull = volgende pull
gb.inclusions = inclusies
gb.exclusions = exclusies
gb.registration = registratie
gb.registrations = federatie registraties
gb.sendProposal = voorstel
gb.status = status
gb.origin = origin
gb.headRef = default branch (HEAD)
gb.headRefDescription = wijzig de ref waar HEAD naar linkt naar bijv. refs/heads/master
gb.federationStrategy = federatie strategie
gb.federationRegistration = federatie registratie
gb.federationResults = federatie pull resultaten
gb.federationSets = federatie sets
gb.message = melding
gb.myUrlDescription = de publiek toegankelijke url voor uw Gitblit instantie
gb.destinationUrl = zend naar
gb.destinationUrlDescription = de url van de Gitblit instantie voor het verzenden van uw voorstel
gb.users = gebruikers
gb.federation = federatie
gb.error = fout
gb.refresh = ververs
gb.browse = blader
gb.clone = clone
gb.filter = filter
gb.create = maak
gb.servers = servers
gb.recent = recent
gb.available = beschikbaar
gb.selected = geselecteerd
gb.size = grootte
gb.downloading = downloading
gb.loading = laden
gb.starting = starten
gb.general = algemeen
gb.settings = instellingen
gb.manage = beheer
gb.lastLogin = laatste login
gb.skipSizeCalculation = geen berekening van de omvang
gb.skipSizeCalculationDescription = geen berekening van de repositoriegrootte (beperkt laadtijd pagina)
gb.skipSummaryMetrics = geen metrieken samenvatting
gb.skipSummaryMetricsDescription = geen berekening van metrieken op de samenvattingspagina (beperkt laadtijd pagina)
gb.accessLevel = toegangsniveau
gb.default = standaard
gb.setDefault = instellen als standaard
gb.since = sinds
gb.status = status
gb.bootDate = boot datum
gb.servletContainer = servlet container
gb.heapMaximum = maximum heap
gb.heapAllocated = toegewezen heap
gb.heapUsed = gebruikte heap
gb.free = beschikbaar
gb.version = versie
gb.releaseDate = release datum
gb.date = datum
gb.activity = activiteit
gb.subscribe = aboneer
gb.branch = branch
gb.maxHits = max hits
gb.recentActivity = recente activiteit
gb.recentActivityStats = laatste {0} dagen / {1} commits door {2} auteurs
gb.recentActivityNone = laatste {0} dagen / geen
gb.dailyActivity = dagelijkse activiteit
gb.activeRepositories = actieve repositories
gb.activeAuthors = actieve auteurs
gb.commits = commits
gb.teams = teams
gb.teamName = teamnaam
gb.teamMembers = teamleden
gb.teamMemberships = teamlidmaatschappen
gb.newTeam = nieuw team
gb.permittedTeams = toegestane teams
gb.emptyRepository = lege repositorie
gb.repositoryUrl = repositorie url
gb.mailingLists = mailing lijsten
gb.preReceiveScripts = pre-receive scripts
gb.postReceiveScripts = post-receive scripts
gb.hookScripts = hook scripts
gb.customFields = custom velden
gb.customFieldsDescription = custom velden beschikbaar voor Groovy hooks
gb.accessPermissions = toegangsrechten
gb.filters = filters
gb.generalDescription = algemene instellingen
gb.accessPermissionsDescription = beperk toegang voor gebruikers en teams
gb.accessPermissionsForUserDescription = stel teamlidmaatschappen in of geef toegang tot specifieke besloten repositories
gb.accessPermissionsForTeamDescription = stel teamlidmaatschappen in en geef toegang tot specifieke besloten repositories
gb.federationRepositoryDescription = deel deze repositorie met andere Gitblit servers
gb.hookScriptsDescription = run Groovy scripts bij pushes naar deze Gitblit server
gb.reset = reset
gb.pages = paginas
gb.workingCopy = werkkopie
gb.workingCopyWarning = deze repositorie heeft een werkkopie en kan geen pushes ontvangen
gb.query = query
gb.queryHelp = Standaard query syntax wordt ondersteund.<p/><p/>Zie aub <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> voor informatie.
gb.queryResults = resultaten {0} - {1} ({2} hits)
gb.noHits = geen hits
gb.authored = authored
gb.committed = committed
gb.indexedBranches = geïndexeerde branches
gb.indexedBranchesDescription = kies de branches voor opname in uw Lucene index
gb.noIndexedRepositoriesWarning = geen van uw repositories is geconfigureerd voor Lucene indexering
gb.undefinedQueryWarning = query is niet gedefinieerd!
gb.noSelectedRepositoriesWarning = kies aub één of meerdere repositories!
gb.luceneDisabled = Lucene indexering staat uit
gb.failedtoRead = Lezen is mislukt
gb.isNotValidFile = is geen valide bestand
gb.failedToReadMessage = Het lezen van de standaard boodschap van {0} is mislukt!
gb.passwordsDoNotMatch = Wachtwoorden komen niet overeen!
gb.passwordTooShort = Wachtwoord is te kort. Minimum lengte is {0} karakters.
gb.passwordChanged = Wachtwoord succesvol gewijzigd.
gb.passwordChangeAborted = Wijziging wachtwoord afgebroken.
gb.pleaseSetRepositoryName = Vul aub een repositorie naam in!
gb.illegalLeadingSlash = Leidende root folder referenties (/) zijn niet toegestaan.
gb.illegalRelativeSlash = Relatieve folder referenties (../) zijn niet toegestaan.
gb.illegalCharacterRepositoryName = Illegaal karakter ''{0}'' in repositorie naam!
gb.selectAccessRestriction = Stel aub een toegangsbeperking in!
gb.selectFederationStrategy = Selecteer aub een federatie strategie!
gb.pleaseSetTeamName = Vul aub een teamnaam in!
gb.teamNameUnavailable = Teamnaam ''{0}'' is niet beschikbaar.
gb.teamMustSpecifyRepository = Een team moet minimaal één repositorie specificeren.
gb.teamCreated = Nieuw team ''{0}'' successvol aangemaakt.
gb.pleaseSetUsername = Vul aub een gebruikersnaam in!
gb.usernameUnavailable = Gebruikersnaam ''{0}'' is niet beschikbaar.
gb.combinedMd5Rename = Gitblit is geconfigureerd voor combined-md5 wachtwoord hashing. U moet een nieuw wachtwoord opgeven bij het hernoemen van een account.
gb.userCreated = Nieuwe gebruiker ''{0}'' succesvol aangemaakt.
gb.couldNotFindFederationRegistration = Kon de federatie registratie niet vinden!
gb.failedToFindGravatarProfile = Kon het Gravatar profiel voor {0} niet vinden
gb.branchStats = {0} commits en {1} tags in {2}
gb.repositoryNotSpecified = Repositorie niet gespecificeerd!
gb.repositoryNotSpecifiedFor = Repositorie niet gespecificeerd voor {0}!
gb.canNotLoadRepository = Kan repositorie niet laden
gb.commitIsNull = Commit is null
gb.unauthorizedAccessForRepository = Niet toegestane toegang tot repositorie
gb.failedToFindCommit = Het vinden van commit \"{0}\" in {1} voor {2} pagina is mislukt!
gb.couldNotFindFederationProposal = Kon federatievoorstel niet vinden!
gb.invalidUsernameOrPassword = Onjuiste gebruikersnaam of wachtwoord!
gb.OneProposalToReview = Er is 1 federatie voorstel dat wacht op review.
gb.nFederationProposalsToReview = Er zijn {0} federatie verzoeken die wachten op review.
gb.couldNotFindTag = Kon tag {0} niet vinden
gb.couldNotCreateFederationProposal = Kon geen federatie voorstel maken!
gb.pleaseSetGitblitUrl = Vul aub uw Gitblit url in!
gb.pleaseSetDestinationUrl = Vul aub een bestemmings-url in voor uw voorstel!
gb.proposalReceived = Voorstel correct ontvangen door {0}.
gb.noGitblitFound = Sorry, {0} kon geen Gitblit instance vinden op {1}.
gb.noProposals = Sorry, {0} accepteert geen voorstellen op dit moment.
gb.noFederation = Sorry, {0} is niet geconfigureerd voor het federeren met een Gitblit instance.
gb.proposalFailed = Sorry, {0} ontving geen voorstelgegevens!
gb.proposalError = Sorry, {0} rapporteert dat een onverwachte fout is opgetreden!
gb.failedToSendProposal = Voorstel verzenden is niet gelukt!
gb.userServiceDoesNotPermitAddUser = {0} staat het toevoegen van een gebruikersaccount niet toe!
gb.userServiceDoesNotPermitPasswordChanges = {0} staat wachtwoord wijzigingen niet toe!
gb.displayName = display naam
gb.emailAddress = emailadres
gb.errorAdminLoginRequired = Aanmelden vereist voor beheerwerk
gb.errorOnlyAdminMayCreateRepository = Alleen een beheerder kan een repositorie maken
gb.errorOnlyAdminOrOwnerMayEditRepository = Alleen een beheerder of de eigenaar kan een repositorie wijzigen
gb.errorAdministrationDisabled = Beheer is uitgeschakeld
gb.lastNDays = laatste {0} dagen
gb.completeGravatarProfile = Completeer profiel op Gravatar.com
gb.none = geen
gb.line = regel
gb.content = inhoud
gb.empty = leeg
gb.inherited = geërfd
gb.deleteRepository = Verwijder repositorie \"{0}\"?
gb.repositoryDeleted = Repositorie ''{0}'' verwijderd.
gb.repositoryDeleteFailed = Verwijdering van repositorie ''{0}'' mislukt!
gb.deleteUser = Verwijder gebruiker \"{0}\"?
gb.userDeleted = Gebruiker ''{0}'' verwijderd.
gb.userDeleteFailed = Verwijdering van gebruiker ''{0}'' mislukt!
gb.time.justNow = net
gb.time.today = vandaag
gb.time.yesterday = gisteren
gb.time.minsAgo = {0} minuten geleden
gb.time.hoursAgo = {0} uren geleden
gb.time.daysAgo = {0} dagen geleden
gb.time.weeksAgo = {0} weken geleden
gb.time.monthsAgo = {0} maanden geleden
gb.time.oneYearAgo = 1 jaar geleden
gb.time.yearsAgo = {0} jaren geleden
gb.duration.oneDay = 1 dag
gb.duration.days = {0} dagen
gb.duration.oneMonth = 1 maand
gb.duration.months = {0} maanden
gb.duration.oneYear = 1 jaar
gb.duration.years = {0} jaren
gb.authorizationControl = authorisatiebeheer
gb.allowAuthenticatedDescription = ken RW+ rechten toe aan alle geautoriseerde gebruikers
gb.allowNamedDescription = ken verfijnde rechten toe aan genoemde gebruikers of teams
gb.markdownFailure = Het parsen van Markdown content is mislukt!
gb.clearCache = maak cache leeg
gb.projects = projecten
gb.project = project
gb.allProjects = alle projecten
gb.copyToClipboard = kopieer naar clipboard
gb.fork = fork
gb.forks = forks
gb.forkRepository = fork {0}?
gb.repositoryForked = {0} is geforked
gb.repositoryForkFailed= fork is mislukt
gb.personalRepositories = personlijke repositories
gb.allowForks = sta forks toe
gb.allowForksDescription = sta geauthoriseerde gebruikers toe om deze repositorie te forken
gb.forkedFrom = geforked vanaf
gb.canFork = kan geforked worden
gb.canForkDescription = kan geauthoriseerde repositories forken naar persoonlijke repositories
gb.myFork = toon mijn fork
gb.forksProhibited = forks niet toegestaan
gb.forksProhibitedWarning = deze repositorie staat forken niet toe
gb.noForks = {0} heeft geen forks
gb.forkNotAuthorized = sorry, u bent niet geautoriseerd voor het forken van {0}
gb.forkInProgress = bezig met forken
gb.preparingFork = bezig met het maken van uw fork...
gb.isFork = is een fork
gb.canCreate = mag maken
gb.canCreateDescription = mag persoonlijke repositories maken
gb.illegalPersonalRepositoryLocation = uw persoonlijke repositorie moet te vinden zijn op \"{0}\"
gb.verifyCommitter = controleer committer
gb.verifyCommitterDescription = vereis dat committer identiteit overeen komt met pushing Gitblt gebruikersaccount
gb.verifyCommitterNote = alle merges vereisen "--no-ff" om committer identiteit af te dwingen
gb.repositoryPermissions = repository rechten
gb.userPermissions = gebruikersrechten
gb.teamPermissions = teamrechten
gb.add = toevoegen
gb.noPermission = VERWIJDER DIT RECHT
gb.excludePermission = {0} (exclude)
gb.viewPermission = {0} (view)
gb.clonePermission = {0} (clone)
gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creëer)
gb.deletePermission = {0} (push, ref creëer+verwijdering)
gb.rewindPermission = {0} (push, ref creëer+verwijdering+rewind)
gb.permission = recht
gb.regexPermission = dit recht is gezet vanaf de reguliere expressie \"{0}\"
gb.accessDenied = toegang geweigerd
gb.busyCollectingGarbage = sorry, Gitblit is bezig met opruimen in {0}
gb.gcPeriod = opruim periode
gb.gcPeriodDescription = tijdsduur tussen opruimacties
gb.gcThreshold = opruim drempel
gb.gcThresholdDescription = minimum totaalomvang van losse objecten voor het starten van opruimactie
gb.ownerPermission = repositorie eigenaar
gb.administrator = beheer
gb.administratorPermission = Gitblit beheerder
gb.team = team
gb.teamPermission = permissie ingesteld via \"{0}\" teamlidmaatschap
gb.missing = ontbrekend!
gb.missingPermission = de repositorie voor deze permissie ontbreekt!
gb.mutable = te wijzigen
gb.specified = gespecificeerd
gb.effective = geldig
gb.organizationalUnit = organisatie eenheid
gb.organization = organisatie
gb.locality = localiteit
gb.stateProvince = staat of provincie
gb.countryCode = landcode
gb.properties = eigenschappen
gb.issued = uitgegeven
gb.expires = verloopt op
gb.expired = verlopen
gb.expiring = verloopt
gb.revoked = ingetrokken
gb.serialNumber = serie nummer
gb.certificates = certificaten
gb.newCertificate = nieuwe certificaten
gb.revokeCertificate = trek certificaat in
gb.sendEmail = zend email
gb.passwordHint = wachtwoord hint
gb.ok = ok
gb.invalidExpirationDate = ongeldige verloopdatum!
gb.passwordHintRequired = wachtwoord hint vereist!
gb.viewCertificate = toon certificaat
gb.subject = onderwerp
gb.issuer = issuer
gb.validFrom = geldig vanaf
gb.validUntil = geldig tot
gb.publicKey = publieke sleutel
gb.signatureAlgorithm = signature algoritme
gb.sha1FingerPrint = SHA-1 Fingerprint
gb.md5FingerPrint = MD5 Fingerprint
gb.reason = reden
gb.revokeCertificateReason = Kies aub een reden voor het intrekken van het certificaat
gb.unspecified = niet gespecificeerd
gb.keyCompromise = sleutel gecompromitteerd
gb.caCompromise = CA gecompromitteerd
gb.affiliationChanged = affiliatie gewijzigd
gb.superseded = opgevolgd
gb.cessationOfOperation = gestaakt
gb.privilegeWithdrawn = privilege ingetrokken
gb.time.inMinutes = in {0} minuten
gb.time.inHours = in {0} uren
gb.time.inDays = in {0} dagen
gb.hostname = hostnaam
gb.hostnameRequired = Vul aub een hostnaam in
gb.newSSLCertificate = nieuw server SSL certificaat
gb.newCertificateDefaults = nieuw certificaat defaults
gb.duration = duur
gb.certificateRevoked = Certificaat {0,number,0} is ingetrokken
gb.clientCertificateGenerated =  Nieuw client certificaat voor {0} succesvol gegenereerd
gb.sslCertificateGenerated = Nieuw server SSL certificaat voor {0} succesvol gegenereerd
gb.newClientCertificateMessage = MERK OP:\nHet 'wachtwoord' is niet het wachtwoord van de gebruiker. Het is het wachtwoord voor het afschermen van de sleutelring van de gebruiker.  Dit wachtwoord wordt niet opgeslagen dus moet u ook een 'hint' invullen die zal worden opgenomen in de README instructies van de gebruiker.
gb.certificate = certificaat
gb.emailCertificateBundle = email client certificaat bundel
gb.pleaseGenerateClientCertificate = Genereer aub een client certificaat voor {0}
gb.clientCertificateBundleSent = Client certificaat bundel voor {0} verzonden
gb.enterKeystorePassword = Vul aub het Gitblit keystore wachtwoord in
gb.warning = waarschuwing
gb.jceWarning = Uw Java Runtime Environment heeft geen \"JCE Unlimited Strength Jurisdiction Policy\" bestanden.\nDit zal de lengte van wachtwoorden voor het eventueel versleutelen van uw keystores beperken tot 7 karakters.\nDeze policy bestanden zijn een optionele download van Oracle.\n\nWilt u toch doorgaan en de certificaat infrastructuur genereren?\n\nNee antwoorden zal uw browser doorsturen naar de downloadpagina van Oracle zodat u de policybestanden kunt downloaden.
gb.maxActivityCommits = maximum activiteit commits
gb.maxActivityCommitsDescription = maximum aantal commits om bij te dragen aan de Activiteitspagina
gb.noMaximum = geen maximum
gb.attributes = attributen
gb.serveCertificate = gebruik deze certificaten voor https
gb.sslCertificateGeneratedRestart = Nieuwe SSL certificaten voor {0} succesvol gegenereerd.\nU dient Gitblit te herstarten om de nieuwe certificaten te gebruiken.\n\nAls u opstart met de '--alias' parameter moet u die wijzigen naar ''--alias {0}''.
gb.validity = geldigheid
gb.siteName = site naam
gb.siteNameDescription = korte, verduidelijkende naam van deze server
gb.excludeFromActivity = sluit uit van activiteitspagina
src/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
New file
@@ -0,0 +1,442 @@
gb.repository = repositório
gb.owner = proprietário
gb.description = descrição
gb.lastChange = última alteração
gb.refs = refs
gb.tag = tag
gb.tags = tags
gb.author = autor
gb.committer = committer
gb.commit = commit
gb.tree = árvore
gb.parent = parent
gb.url = URL
gb.history = histórico
gb.raw = raw
gb.object = object
gb.ticketId = ticket id
gb.ticketAssigned = atribuído
gb.ticketOpenDate = data da abertura
gb.ticketState = estado
gb.ticketComments = comentários
gb.view = visualizar
gb.local = local
gb.remote = remote
gb.branches = branches
gb.patch = patch
gb.diff = diff
gb.log = log
gb.moreLogs = mais commits...
gb.allTags = todas as tags...
gb.allBranches = todos os branches...
gb.summary = resumo
gb.ticket = ticket
gb.newRepository = novo repositório
gb.newUser = novo usuário
gb.commitdiff = commitdiff
gb.tickets = tickets
gb.pageFirst = primeira
gb.pagePrevious anterior
gb.pageNext = próxima
gb.head = HEAD
gb.blame = blame
gb.login = login
gb.logout = logout
gb.username = username
gb.password = password
gb.tagger = tagger
gb.moreHistory = mais histórico...
gb.difftocurrent = diff para a atual
gb.search = pesquisar
gb.searchForAuthor = Procurar por commits cujo autor é
gb.searchForCommitter = Procurar por commits commitados por é
gb.addition = adicionados
gb.modification = modificados
gb.deletion = apagados
gb.rename = renomear
gb.metrics = métricas
gb.stats = estatísticas
gb.markdown = markdown
gb.changedFiles = arquivos alterados
gb.filesAdded = {0} arquivos adicionados
gb.filesModified = {0} arquivos modificados
gb.filesDeleted = {0} arquivos deletados
gb.filesCopied = {0} arquivos copiados
gb.filesRenamed = {0} arquivos renomeados
gb.missingUsername = Username desconhecido
gb.edit = editar
gb.searchTypeTooltip = Selecione o Tipo de Pesquisa
gb.searchTooltip = Pesquisar {0}
gb.delete = deletar
gb.docs = documentos
gb.accessRestriction = restrição de acesso
gb.name = nome
gb.enableTickets = ativar tickets
gb.enableDocs = ativar documentação
gb.save = salvar
gb.showRemoteBranches = mostrar branches remotos
gb.editUsers = editar usuários
gb.confirmPassword = confirmar password
gb.restrictedRepositories = repositórios restritos
gb.canAdmin = pode administrar
gb.notRestricted = visualização anônima, clone, & push
gb.pushRestricted = push autênticado
gb.cloneRestricted = clone & push autênticados
gb.viewRestricted = view, clone, & push autênticados
gb.useTicketsDescription = somente leitura, issues do Ticgit distribuídos
gb.useDocsDescription = enumerar documentação Markdown no repositório
gb.showRemoteBranchesDescription = mostrar branches remotos
gb.canAdminDescription = pode administrar o server Gitblit
gb.permittedUsers = usuários autorizados
gb.isFrozen = congelar
gb.isFrozenDescription = proibir fazer push
gb.zip = zip
gb.showReadme = mostrar readme
gb.showReadmeDescription = mostrar um arquivo \"leia-me\" na página de resumo
gb.nameDescription = usar '/' para agrupar repositórios.  e.g. libraries/mycoollib.git
gb.ownerDescription = o proprietário pode editar configurações do repositório
gb.blob = blob
gb.commitActivityTrend = tendência dos commits
gb.commitActivityDOW = commits diários
gb.commitActivityAuthors = principais committers
gb.feed = feed
gb.cancel = cancelar
gb.changePassword = alterar password
gb.isFederated = está federado
gb.federateThis = federar este repositório
gb.federateOrigin = federar o origin
gb.excludeFromFederation = excluir da federação
gb.excludeFromFederationDescription = bloquear instâncias federadas do GitBlit de fazer pull desta conta
gb.tokens = tokens de federação
gb.tokenAllDescription = todos repositórios, usuários & configurações
gb.tokenUnrDescription = todos repositórios & usuários
gb.tokenJurDescription = todos repositórios
gb.federatedRepositoryDefinitions = definições de repositório
gb.federatedUserDefinitions = definições de usuários
gb.federatedSettingDefinitions = definições de configurações
gb.proposals = propostas de federações
gb.received = recebidos
gb.type = tipo
gb.token = token
gb.repositories = repositórios
gb.proposal = propostas
gb.frequency = frequência
gb.folder = pasta
gb.lastPull = último pull
gb.nextPull = próximo pull
gb.inclusions = inclusões
gb.exclusions = excluões
gb.registration = cadastro
gb.registrations = cadastro de federações
gb.sendProposal = enviar proposta
gb.status = status
gb.origin = origin
gb.headRef = default branch (HEAD)
gb.headRefDescription = alterar a ref o qual a HEAD aponta. e.g. refs/heads/master
gb.federationStrategy = estratégia de federação
gb.federationRegistration = cadastro de federações
gb.federationResults = resultados dos pulls de federações
gb.federationSets = ajustes de federações
gb.message = mensagem
gb.myUrlDescription = a url de acesso público para a instância Gitblit
gb.destinationUrl = enviar para
gb.destinationUrlDescription = a url da intância do Gitblit para enviar sua proposta
gb.users = usuários
gb.federation = federação
gb.error = erro
gb.refresh = atualizar
gb.browse = navegar
gb.clone = clonar
gb.filter = filtrar
gb.create = criar
gb.servers = servidores
gb.recent = recente
gb.available = disponível
gb.selected = selecionado
gb.size = tamanho
gb.downloading = downloading
gb.loading = loading
gb.starting = inciando
gb.general = geral
gb.settings = configurações
gb.manage = administrar
gb.lastLogin = último login
gb.skipSizeCalculation = ignorar cálculo do tamanho
gb.skipSizeCalculationDescription = não calcular o tamanho do repositório (reduz o tempo de load da página)
gb.skipSummaryMetrics = ignorar resumo das métricas
gb.skipSummaryMetricsDescription = não calcular métricas na página de resumo
gb.accessLevel = acesso
gb.default = default
gb.setDefault = tornar default
gb.since = desde
gb.status = status
gb.bootDate = data do boot
gb.servletContainer = servlet container
gb.heapMaximum = heap máximo
gb.heapAllocated = alocar heap
gb.heapUsed = usar heap
gb.free = free
gb.version = versão
gb.releaseDate = data de release
gb.date = data
gb.activity = atividade
gb.subscribe = inscrever
gb.branch = branch
gb.maxHits = hits máximos
gb.recentActivity = atividade recente
gb.recentActivityStats = últimos {0} dias / {1} commits por {2} autores
gb.recentActivityNone = últimos {0} dias / nenhum
gb.dailyActivity = atividade diária
gb.activeRepositories = repositórios ativos
gb.activeAuthors = autores ativos
gb.commits = commits
gb.teams = equipes
gb.teamName = nome da equipe
gb.teamMembers = membros
gb.teamMemberships = filiações em equipes
gb.newTeam = nova equipe
gb.permittedTeams = equipes permitidas
gb.emptyRepository = repositório vazio
gb.repositoryUrl = url do repositório
gb.mailingLists = listas de e-mails
gb.preReceiveScripts = pre-receive scripts
gb.postReceiveScripts = post-receive scripts
gb.hookScripts = hook scripts
gb.customFields = campos customizados
gb.customFieldsDescription = campos customizados disponíveis para Groovy hooks
gb.accessPermissions = permissões de acesso
gb.filters = filtros
gb.generalDescription = configurações comuns
gb.accessPermissionsDescription = restringir acesso por usuários e equipes
gb.accessPermissionsForUserDescription = ajustar filiações em equipes ou garantir acesso a repositórios específicos
gb.accessPermissionsForTeamDescription = ajustar membros da equipe e garantir acesso a repositórios específicos
gb.federationRepositoryDescription = compartilhar este repositório com outros servidores Gitblit
gb.hookScriptsDescription = rodar scripts Groovy nos pushes pushes para este servidor Gitblit
gb.reset = reset
gb.pages = páginas
gb.workingCopy = working copy
gb.workingCopyWarning = this repository has a working copy and can not receive pushes
gb.query = query
gb.queryHelp =  Standard query syntax é suportada.<p/><p/>Por favor veja <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> para mais detalhes.
gb.queryResults = resultados {0} - {1} ({2} hits)
gb.noHits = sem hits
gb.authored = foi autor do
gb.committed = committed
gb.indexedBranches = branches indexados
gb.indexedBranchesDescription = selecione os branches para incluir nos seus índices Lucene
gb.noIndexedRepositoriesWarning = nenhum dos seus repositórios foram configurados para indexação do Lucene
gb.undefinedQueryWarning = a query não foi definida!
gb.noSelectedRepositoriesWarning = por favor selecione um ou mais repositórios!
gb.luceneDisabled = indexação do Lucene está desabilitada
gb.failedtoRead = leitura falhou
gb.isNotValidFile = não é um arquivo válido
gb.failedToReadMessage = Falhou em ler mensagens default de {0}!
gb.passwordsDoNotMatch = Passwords não conferem!
gb.passwordTooShort = Password é muito curto. Tamanho mínimo são {0} caracteres.
gb.passwordChanged = Password alterado com sucesso.
gb.passwordChangeAborted = alteração do Password foi abortada.
gb.pleaseSetRepositoryName = Por favor ajuste o nome do repositório!
gb.illegalLeadingSlash = Referências a diretórios raiz começando com (/) são proibidas.
gb.illegalRelativeSlash = Referências a diretórios relativos (../) são proibidas.
gb.illegalCharacterRepositoryName = Caractere ilegal ''{0}'' no nome do repositório!
gb.selectAccessRestriction = Please select access restriction!
gb.selectFederationStrategy = Por favor selecione a estratégia de federação!
gb.pleaseSetTeamName = Por favor insira um nome de equipe!
gb.teamNameUnavailable = O nome de equipe ''{0}'' está indisponível.
gb.teamMustSpecifyRepository = Uma equipe deve  especificar pelo menos um repositório.
gb.teamCreated = Nova equipe ''{0}'' criada com sucesso.
gb.pleaseSetUsername = Por favor entre com um username!
gb.usernameUnavailable = Username ''{0}'' está indisponível.
gb.combinedMd5Rename = Gitblit está configurado para usar um hash combinado-md5. Você deve inserir um novo password ao renamear a conta.
gb.userCreated = Novo usuário ''{0}'' criado com sucesso.
gb.couldNotFindFederationRegistration = Não foi possível localizar o registro da federação!
gb.failedToFindGravatarProfile = Falhou em localizar um perfil Gravatar para {0}
gb.branchStats = {0} commits e {1} tags em {2}
gb.repositoryNotSpecified = Repositório não específicado!
gb.repositoryNotSpecifiedFor = Repositório não específicado para {0}!
gb.canNotLoadRepository = Não foi possível carregar o repositório
gb.commitIsNull = Commit está nulo
gb.unauthorizedAccessForRepository = Acesso não autorizado para o repositório
gb.failedToFindCommit = Não foi possível achar o commit \"{0}\" em {1} para {2} página!
gb.couldNotFindFederationProposal = Não foi possível localizar propostas de federação!
gb.invalidUsernameOrPassword = username ou password inválido!
gb.OneProposalToReview = Existe uma proposta de federação aguardando revisão.
gb.nFederationProposalsToReview = Existem {0} propostas de federação aguardando revisão.
gb.couldNotFindTag = Não foi possível localizar a tag {0}
gb.couldNotCreateFederationProposal = Não foi possível criar uma proposta de federation!
gb.pleaseSetGitblitUrl = Por favor insira sua url do Gitblit!
gb.pleaseSetDestinationUrl = Por favor insira a url de destino para sua proposta!
gb.proposalReceived = Proposta recebida com sucesso por {0}.
gb.noGitblitFound = Desculpe, {0} não localizou uma instância do Gitblit em {1}.
gb.noProposals = Desculpe, {0} não está aceitando propostas agora.
gb.noFederation = Desculpe, {0} não está configurado com nenhuma intância do Gitblit.
gb.proposalFailed = Desculpe, {0} não recebeu nenhum dado de proposta!
gb.proposalError = Desculpe, {0} reportou que um erro inesperado ocorreu!
gb.failedToSendProposal = Não foi possível enviar a proposta!
gb.userServiceDoesNotPermitAddUser = {0} não permite adicionar uma conta de usuário!
gb.userServiceDoesNotPermitPasswordChanges = {0} não permite alterações no password!
gb.displayName = nome
gb.emailAddress = e-mail
gb.errorAdminLoginRequired = Administração requer um login
gb.errorOnlyAdminMayCreateRepository = Somente umadministrador pode criar um repositório
gb.errorOnlyAdminOrOwnerMayEditRepository = Somente umadministrador pode editar um repositório
gb.errorAdministrationDisabled = Administração está desabilitada
gb.lastNDays = últimos {0} dias
gb.completeGravatarProfile = Profile completo em Gravatar.com
gb.none = nenhum
gb.line = linha
gb.content = conteúdo
gb.empty = vazio
gb.inherited = herdado
gb.deleteRepository = Deletar repositório \"{0}\"?
gb.repositoryDeleted = Repositório ''{0}'' deletado.
gb.repositoryDeleteFailed = Não foi possível apagar o repositório ''{0}''!
gb.deleteUser = Deletar usuário \"{0}\"?
gb.userDeleted = Usuário ''{0}'' deletado.
gb.userDeleteFailed = Não foi possível apagar o usuário ''{0}''!
gb.time.justNow = agora mesmo
gb.time.today = hoje
gb.time.yesterday = ontem
gb.time.minsAgo = há {0} minutos
gb.time.hoursAgo = há {0} horas
gb.time.daysAgo = há {0} dias
gb.time.weeksAgo = há {0} semanas
gb.time.monthsAgo = há {0} meses
gb.time.oneYearAgo = há 1 ano
gb.time.yearsAgo = há {0} anos
gb.duration.oneDay = 1 dia
gb.duration.days = {0} dias
gb.duration.oneMonth = 1 mês
gb.duration.months = {0} meses
gb.duration.oneYear = 1 ano
gb.duration.years = {0} anos
gb.authorizationControl = controle de autorização
gb.allowAuthenticatedDescription = conceder permissão RW+ para todos os usuários autênticados
gb.allowNamedDescription = conceder permissões refinadas para usuários escolhidos ou equipes
gb.markdownFailure = Não foi possível converter conteúdo Markdown!
gb.clearCache = limpar o cache
gb.projects = projetos
gb.project = projeto
gb.allProjects = todos projetos
gb.copyToClipboard = copiar para o clipboard
gb.fork = fork
gb.forks = forks
gb.forkRepository = fork {0}?
gb.repositoryForked = fork feito em {0}
gb.repositoryForkFailed= não foi possível fazer fork
gb.personalRepositories = repositórios pessoais
gb.allowForks = permitir forks
gb.allowForksDescription = permitir usuários autorizados a fazer fork deste repositório
gb.forkedFrom = forked de
gb.canFork = pode fazer fork
gb.canForkDescription = pode fazer fork de repositórios autorizados para repositórios pessoais
gb.myFork = visualizar meu fork
gb.forksProhibited = forks proibidos
gb.forksProhibitedWarning = este repositório proíbe forks
gb.noForks = {0} não possui forks
gb.forkNotAuthorized = desculpe, você não está autorizado a fazer fork de {0}
gb.forkInProgress = fork em progresso
gb.preparingFork = preparando seu fork...
gb.isFork = é fork
gb.canCreate = pode criar
gb.canCreateDescription = pode criar repositórios pessoais
gb.illegalPersonalRepositoryLocation = seu repositório pessoal deve estar localizado em \"{0}\"
gb.verifyCommitter = verificar committer
gb.verifyCommitterDescription = requer a identidade do committer para combinar com uma conta do Gitblt
gb.verifyCommitterNote = todos os merges requerem "--no-ff" para impor a identidade do committer
gb.repositoryPermissions = permissões de repositório
gb.userPermissions = permissões de usuário
gb.teamPermissions = permissões de equipe
gb.add = add
gb.noPermission = APAGAR ESTA PERMISSÃO
gb.excludePermission = {0} (excluir)
gb.viewPermission = {0} (visualizar)
gb.clonePermission = {0} (clonar)
gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
gb.permission = permissão
gb.regexPermission = esta permissão foi configurada através da expressão regular \"{0}\"
gb.accessDenied = acesso negado
gb.busyCollectingGarbage = desculpe, o Gitblit está ocupado coletando lixo em {0}
gb.gcPeriod = período do GC
gb.gcPeriodDescription = duração entre as coletas de lixo
gb.gcThreshold = limite do GC
gb.gcThresholdDescription = tamanho total mínimo de objetos \"soltos\" que ativam a coleta de lixo
gb.ownerPermission = proprietário do repositório
gb.administrator = administrador
gb.administratorPermission = administrador do Gitblit
gb.team = equipe
gb.teamPermission = permissão concedida pela filiação a equipe \"{0}\"
gb.missing = faltando!
gb.missingPermission = o repositório para esta permissão está faltando!
gb.mutable = mutável
gb.specified = específico
gb.effective = efetivo
gb.organizationalUnit = unidade organizacional
gb.organization = organização
gb.locality = localidade
gb.stateProvince = estado ou província
gb.countryCode = código do país
gb.properties = propriedades
gb.issued = emitido
gb.expires = expira
gb.expired = expirado
gb.expiring = expirando
gb.revoked = revogado
gb.serialNumber = número serial
gb.certificates = certificados
gb.newCertificate = novo certificado
gb.revokeCertificate = revogar certificado
gb.sendEmail = enviar email
gb.passwordHint = dica de password
gb.ok = ok
gb.invalidExpirationDate = data de expiração inválida!
gb.passwordHintRequired = dica de password requerida!
gb.viewCertificate = visualizar certificado
gb.subject = assunto
gb.issuer = emissor
gb.validFrom = válido a partir de
gb.validUntil = válido até
gb.publicKey = chave pública
gb.signatureAlgorithm = algoritmo de assinatura
gb.sha1FingerPrint = digital SHA-1
gb.md5FingerPrint = digital MD5
gb.reason = razão
gb.revokeCertificateReason = Por selecione a razão da revogação do certificado
gb.unspecified = não específico
gb.keyCompromise = comprometimento de chave
gb.caCompromise = compromisso CA
gb.affiliationChanged = afiliação foi alterada
gb.superseded = substituídas
gb.cessationOfOperation = cessação de funcionamento
gb.privilegeWithdrawn = privilégio retirado
gb.time.inMinutes = em {0} minutos
gb.time.inHours = em {0} horas
gb.time.inDays = em {0} dias
gb.hostname = hostname
gb.hostnameRequired = Por favor insira um hostname
gb.newSSLCertificate = novo servidor de certificado SSL
gb.newCertificateDefaults = novos padrões de certificação
gb.duration = duração
gb.certificateRevoked = Certificado {0, número, 0} foi revogado
gb.clientCertificateGenerated = Novo certificado cliente para {0} foi gerado com sucesso
gb.sslCertificateGenerated = Novo servidor de certificado SSL gerado com sucesso para {0}
gb.newClientCertificateMessage = OBSERVAÇÃO:\nO 'password' não é o password do usuário mas sim o password usado para proteger a keystore.  Este password não será salvo então você também inserir uma dica que será incluída nas instruções de LEIA-ME do usuário.
gb.certificate = certificado
gb.emailCertificateBundle = pacote certificado de cliente de email
gb.pleaseGenerateClientCertificate = Por favor gere um certificado cliente para {0}
gb.clientCertificateBundleSent = Pacote de certificado de cliente para {0} enviada
gb.enterKeystorePassword = Por favor insira uma chave para keystore do Gitblit
gb.warning = warning
gb.jceWarning = Seu Java Runtime Environment não tem os arquivos \"JCE Unlimited Strength Jurisdiction Policy\".\nIsto irá limitar o tamanho dos passwords que você usará para encriptar suas keystores para 7 caracteres.\nEstes arquivos de políticas são um download opcional da Oracle.\n\nVocê gostaria de continuar e gerar os certificados de infraestrutura de qualquer forma?\n\nRespondendo "Não" irá redirecionar o seu browser para a página de downloads da Oracle, de onde você poderá fazer download desses arquivos.
gb.maxActivityCommits = limitar exibição de commits
gb.maxActivityCommitsDescription = quantidade máxima de commits para contribuir para a página de atividade
gb.noMaximum = ilimitado
gb.attributes = atributos
gb.serveCertificate = servir https com este certificado
gb.sslCertificateGeneratedRestart = Novo certificado SSL de servidor gerado com sucesso para {0}.\nVocê deve reiniciar o Gitblit para usar o novo certificado.\n\nSe você estiver executando com o parâmetro '--alias', você precisará alterá-lo para ''--alias {0}''.
gb.validity = validade
gb.siteName = nome do site
gb.siteNameDescription = breve mas um nome descritivo para seu servidor
src/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
New file
@@ -0,0 +1,444 @@
gb.repository = \u7248\u672c\u5e93
gb.owner = \u7ba1\u7406\u5458
gb.description = \u63cf\u8ff0
gb.lastChange = \u6700\u8fd1\u4fee\u6539
gb.refs = refs
gb.tag = \u6807\u7b7e
gb.tags = \u6807\u7b7e
gb.author = \u7528\u6237
gb.committer = \u63d0\u4ea4\u8005
gb.commit = \u63d0\u4ea4
gb.tree = \u76ee\u5f55
gb.parent = parent
gb.url = URL
gb.history = \u5386\u53f2\u4fe1\u606f
gb.raw = raw
gb.object = object
gb.ticketId = ticket id
gb.ticketAssigned = assigned
gb.ticketOpenDate = \u5f00\u542f\u65e5\u671f
gb.ticketState = \u72b6\u6001
gb.ticketComments = \u8bc4\u8bba
gb.view = \u67e5\u770b
gb.local = \u672c\u5730
gb.remote = \u8fdc\u7a0b
gb.branches = \u5206\u652f
gb.patch = patch
gb.diff = \u5bf9\u6bd4
gb.log = \u65e5\u5fd7
gb.moreLogs = \u66f4\u591a\u63d0\u4ea4...
gb.allTags = \u6240\u6709\u6807\u7b7e...
gb.allBranches = \u6240\u6709\u5206\u652f...
gb.summary = \u6982\u51b5
gb.ticket = ticket
gb.newRepository = \u521b\u5efa\u7248\u672c\u5e93
gb.newUser = \u6dfb\u52a0\u7528\u6237
gb.commitdiff = \u5bf9\u6bd4\u63d0\u4ea4\u7684\u5185\u5bb9
gb.tickets = tickets
gb.pageFirst = \u9996\u9875
gb.pagePrevious = \u524d\u4e00\u9875
gb.pageNext = \u4e0b\u4e00\u9875
gb.head = HEAD
gb.blame = blame
gb.login = \u767b\u5f55
gb.logout = \u6ce8\u9500
gb.username = \u7528\u6237\u540d
gb.password = \u5bc6\u7801
gb.tagger = \u6807\u8bb0\u8005
gb.moreHistory = \u66f4\u591a\u7684\u5386\u53f2\u4fe1\u606f...
gb.difftocurrent = \u5bf9\u6bd4\u5f53\u524d
gb.search = \u641c\u7d22
gb.searchForAuthor = \u6309\u4f5c\u8005\u641c\u7d22 commits
gb.searchForCommitter = \u6309\u63d0\u4ea4\u8005\u641c\u7d22 commits
gb.addition = \u6dfb\u52a0
gb.modification = \u4fee\u6539
gb.deletion = \u5220\u9664
gb.rename = \u91cd\u547d\u540d
gb.metrics = metrics
gb.stats = \u7edf\u8ba1
gb.markdown = markdown
gb.changedFiles = \u5df2\u4fee\u6539\u6587\u4ef6
gb.filesAdded = {0}\u4e2a\u6587\u4ef6\u5df2\u6dfb\u52a0
gb.filesModified = {0}\u4e2a\u6587\u4ef6\u5df2\u4fee\u6539
gb.filesDeleted = {0}\u4e2a\u6587\u4ef6\u5df2\u5220\u9664
gb.filesCopied = {0} \u6587\u4ef6\u5df2\u590d\u5236
gb.filesRenamed = {0} \u6587\u4ef6\u5df2\u91cd\u547d\u540d
gb.missingUsername = \u7528\u6237\u540d\u4e0d\u5b58\u5728
gb.edit = \u7f16\u8f91
gb.searchTypeTooltip = \u9009\u62e9\u641c\u7d22\u7c7b\u578b
gb.searchTooltip = \u641c\u7d22 {0}
gb.delete = \u5220\u9664
gb.docs = \u6587\u6863
gb.accessRestriction = \u8bbf\u95ee\u9650\u5236
gb.name = \u540d\u79f0
gb.enableTickets = \u5141\u8bb8 tickets
gb.enableDocs = \u5141\u8bb8\u6587\u6863
gb.save = \u4fdd\u5b58
gb.showRemoteBranches = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
gb.editUsers = \u7f16\u8f91\u7528\u6237
gb.confirmPassword = \u786e\u8ba4\u5bc6\u7801
gb.restrictedRepositories = \u7248\u672c\u5e93\u8bbe\u7f6e
gb.canAdmin = \u7ba1\u7406\u6743\u9650
gb.notRestricted = anonymous view, clone, & push
gb.pushRestricted = authenticated push
gb.cloneRestricted = authenticated clone & push
gb.viewRestricted = authenticated view, clone, & push
gb.useTicketsDescription = distributed Ticgit issues
gb.useDocsDescription = \u5217\u51fa\u7248\u672c\u5e93\u5185\u6240\u6709 Markdown \u6587\u6863
gb.showRemoteBranchesDescription = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
gb.canAdminDescription = Gitblit \u670d\u52a1\u5668\u7ba1\u7406\u5458
gb.permittedUsers = \u5141\u8bb8\u7528\u6237
gb.isFrozen = \u88ab\u51bb\u7ed3
gb.isFrozenDescription = \u7981\u6b62\u63a8\u9001\u64cd\u4f5c
gb.zip = zip
gb.showReadme = \u663e\u793areadme
gb.showReadmeDescription = \u5728\u6982\u51b5\u9875\u9762\u663e\u793a \\"readme\\" Markdown \u6587\u4ef6
gb.nameDescription = \u4f7f\u7528 '/' \u5bf9\u7248\u672c\u5e93\u8fdb\u884c\u5206\u7ec4  \u4f8b\u5982. libraries/mycoollib.git
gb.ownerDescription = \u521b\u5efa\u8005\u53ef\u4ee5\u7f16\u8f91\u7248\u672c\u5e93\u5c5e\u6027
gb.blob = blob
gb.commitActivityTrend = commit \u6d3b\u52a8\u8d8b\u52bf
gb.commitActivityDOW = \u6bcf\u5468 commit \u6d3b\u52a8
gb.commitActivityAuthors = commit \u6d3b\u52a8\u4e3b\u8981\u7528\u6237
gb.feed = feed
gb.cancel = \u53d6\u6d88
gb.changePassword = \u4fee\u6539\u5bc6\u7801
gb.isFederated = is federated
gb.federateThis = federate this repository
gb.federateOrigin = federate the origin
gb.excludeFromFederation = exclude from federation
gb.excludeFromFederationDescription = \u7981\u6b62\u5df2 federated \u7684 Gitblit \u5b9e\u4f8b\u4ece\u672c\u8d26\u6237\u62c9\u53d6
gb.tokens = federation tokens
gb.tokenAllDescription = all repositories, users, & settings
gb.tokenUnrDescription = all repositories & users
gb.tokenJurDescription = all repositories
gb.federatedRepositoryDefinitions = \u7248\u672c\u5e93\u5b9a\u4e49
gb.federatedUserDefinitions = \u7528\u6237\u5b9a\u4e49
gb.federatedSettingDefinitions = \u8bbe\u7f6e\u5b9a\u4e49
gb.proposals = federation proposals
gb.received = \u5df2\u63a5\u53d7
gb.type = type
gb.token = token
gb.repositories = \u7248\u672c\u5e93
gb.proposal = proposal
gb.frequency = \u9891\u7387
gb.folder = \u6587\u4ef6\u5939
gb.lastPull = \u4e0a\u4e00\u6b21\u62c9\u53d6
gb.nextPull = \u4e0b\u4e00\u6b21\u62c9\u53d6
gb.inclusions = \u5305\u542b\u5185\u5bb9
gb.exclusions = \u4f8b\u5916
gb.registration = \u6ce8\u518c
gb.registrations = federation \u6ce8\u518c
gb.sendProposal = propose
gb.status = \u72b6\u6001
gb.origin = origin
gb.headRef = \u9ed8\u8ba4\u5206\u652f (HEAD)
gb.headRefDescription = \u4fee\u6539 HEAD \u6240\u6307\u5411\u7684 ref\u3002 \u4f8b\u5982: refs/heads/master
gb.federationStrategy = federation \u7b56\u7565
gb.federationRegistration = federation \u6ce8\u518c
gb.federationResults = federation \u62c9\u53d6\u7ed3\u679c
gb.federationSets = federation \u96c6
gb.message = \u6d88\u606f
gb.myUrlDescription = \u60a8\u7684 Gitblit \u5b9e\u4f8b\u7684\u516c\u5171\u8bbf\u95ee\u7f51\u5740
gb.destinationUrl = \u53d1\u9001\u81f3
gb.destinationUrlDescription = \u4f60\u6240\u8981\u53d1\u9001proposal\u7684 Gitblit \u5b9e\u4f8b\u7f51\u5740
gb.users = \u7528\u6237
gb.federation = federation
gb.error = \u9519\u8bef
gb.refresh = \u5237\u65b0
gb.browse = \u6d4f\u89c8
gb.clone = \u514b\u9686
gb.filter = \u8fc7\u6ee4
gb.create = \u521b\u5efa
gb.servers = \u670d\u52a1\u5668
gb.recent = \u6700\u8fd1
gb.available = \u53ef\u7528
gb.selected = \u5df2\u9009\u4e2d
gb.size = \u5927\u5c0f
gb.downloading = \u4e0b\u8f7d\u4e2d
gb.loading = \u8f7d\u5165\u4e2d
gb.starting = \u542f\u52a8\u4e2d
gb.general = \u5e38\u89c4
gb.settings = \u8bbe\u7f6e
gb.manage = \u7ba1\u7406
gb.lastLogin = \u4e0a\u6b21\u767b\u5f55
gb.skipSizeCalculation = \u5ffd\u7565\u5927\u5c0f\u4f30\u8ba1
gb.skipSizeCalculationDescription = \u4e0d\u8ba1\u7b97\u7248\u672c\u5e93\u5927\u5c0f\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
gb.skipSummaryMetrics = \u5ffd\u7565\u6982\u51b5\u5904 metrics
gb.skipSummaryMetricsDescription = \u6982\u51b5\u9875\u9762\u4e0d\u8ba1\u7b97metrics\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
gb.accessLevel = \u8bbf\u95ee\u7ea7\u522b
gb.default = \u9ed8\u8ba4
gb.setDefault = \u9ed8\u8ba4\u8bbe\u7f6e
gb.since = \u81ea\u4ece
gb.status = \u72b6\u6001
gb.bootDate = \u542f\u52a8\u65e5\u671f
gb.servletContainer = servlet container
gb.heapMaximum = \u6700\u5927\u5806
gb.heapAllocated = \u5df2\u5206\u914d\u5806
gb.heapUsed = \u5df2\u4f7f\u7528\u5806
gb.free = \u7a7a\u95f2
gb.version = \u7248\u672c
gb.releaseDate = \u53d1\u884c\u65e5\u671f
gb.date = \u65e5\u671f
gb.activity = \u6d3b\u52a8
gb.subscribe = \u8ba2\u9605
gb.branch = \u5206\u652f
gb.maxHits = \u6700\u5927\u547d\u4e2d\u6570
gb.recentActivity = \u6700\u8fd1\u6d3b\u52a8
gb.recentActivityStats = \u6700\u8fd1{0}\u5929 / {2}\u4f4d\u7528\u6237\u505a\u4e86{1}\u6b21\u63d0\u4ea4
gb.recentActivityNone = \u6700\u8fd1{0}\u5929 / \u6ca1\u6709\u6d3b\u52a8
gb.dailyActivity = \u65e5\u5e38\u6d3b\u52a8
gb.activeRepositories = \u6d3b\u8dc3\u7684\u7248\u672c\u5e93
gb.activeAuthors = \u6d3b\u8dc3\u7528\u6237
gb.commits = \u63d0\u4ea4\u6b21\u6570
gb.teams = \u56e2\u961f
gb.teamName = \u56e2\u961f\u540d\u79f0
gb.teamMembers = \u56e2\u961f\u6210\u5458
gb.teamMemberships = \u56e2\u961f\u6210\u5458
gb.newTeam = \u6dfb\u52a0\u56e2\u961f
gb.permittedTeams = \u5141\u8bb8\u56e2\u961f
gb.emptyRepository = \u7a7a\u7248\u672c\u5e93
gb.repositoryUrl = \u7248\u672c\u5e93\u5730\u5740
gb.mailingLists = \u90ae\u4ef6\u5217\u8868
gb.preReceiveScripts = pre-receive \u811a\u672c
gb.postReceiveScripts = post-receive \u811a\u672c
gb.hookScripts = hook \u811a\u672c
gb.customFields = \u81ea\u5b9a\u4e49\u57df
gb.customFieldsDescription = Groovy\u811a\u672c\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u57df
gb.accessPermissions = \u8bbf\u95ee\u6743\u9650
gb.filters = \u8fc7\u6ee4
gb.generalDescription = \u4e00\u822c\u8bbe\u7f6e
gb.accessPermissionsDescription = \u6309\u7167\u7528\u6237\u548c\u56e2\u961f\u9650\u5236\u8bbf\u95ee
gb.accessPermissionsForUserDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u6216\u8005\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
gb.accessPermissionsForTeamDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u5e76\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
gb.federationRepositoryDescription = \u4e0e\u5176\u4ed6Gitblit\u670d\u52a1\u5668\u5206\u4eab\u7248\u672c\u5e93
gb.hookScriptsDescription = \u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884cGroovy\u811a\u672c
gb.reset = \u91cd\u7f6e
gb.pages = \u9875\u9762
gb.workingCopy = \u5de5\u4f5c\u526f\u672c
gb.workingCopyWarning = \u6b64\u7248\u672c\u5e93\u5b58\u5728\u4e00\u4efd\u5de5\u4f5c\u526f\u672c\uff0c\u65e0\u6cd5\u8fdb\u884c\u63a8\u9001
gb.query = \u67e5\u8be2
gb.queryHelp = \u652f\u6301\u6807\u51c6\u67e5\u8be2\u683c\u5f0f.<p/><p/>\u8bf7\u67e5\u770b <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene \u67e5\u8be2\u5904\u7406\u5668\u683c\u5f0f</a> \u4ee5\u83b7\u53d6\u8be6\u7ec6\u5185\u5bb9\u3002
gb.queryResults = \u7ed3\u679c {0} - {1} ({2} \u6b21\u547d\u4e2d)
gb.noHits = \u672a\u547d\u4e2d
gb.authored = authored
gb.committed = committed
gb.indexedBranches = \u5df2\u7d22\u5f15\u5206\u652f
gb.indexedBranchesDescription = \u9009\u62e9\u8981\u653e\u5165\u4f60\u7684 Lucene \u7d22\u5f15\u7684\u5206\u652f
gb.noIndexedRepositoriesWarning = \u60a8\u7684\u6240\u6709\u7248\u672c\u5e93\u90fd\u6ca1\u6709\u7ecf\u8fc7Lucene\u7d22\u5f15
gb.undefinedQueryWarning = \u67e5\u8be2\u672a\u5b9a\u4e49!
gb.noSelectedRepositoriesWarning = \u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u4e2a\u7248\u672c\u5e93!
gb.luceneDisabled = Lucene\u7d22\u5f15\u5df2\u88ab\u7981\u6b62
gb.failedtoRead = \u8bfb\u53d6\u5931\u8d25
gb.isNotValidFile = \u4e0d\u662f\u5408\u6cd5\u6587\u4ef6
gb.failedToReadMessage = \u5728 {0} \u4e2d\u8bfb\u53d6\u9ed8\u8ba4\u6d88\u606f\u5931\u8d25!
gb.passwordsDoNotMatch = \u5bc6\u7801\u4e0d\u5339\u914d!
gb.passwordTooShort = \u5bc6\u7801\u957f\u5ea6\u592a\u77ed\u3002\u6700\u77ed\u957f\u5ea6 {0} \u4e2a\u5b57\u7b26\u3002
gb.passwordChanged = \u5bc6\u7801\u4fee\u6539\u6210\u529f\u3002
gb.passwordChangeAborted = \u5bc6\u7801\u4fee\u6539\u7ec8\u6b62
gb.pleaseSetRepositoryName = \u8bf7\u8bbe\u7f6e\u4e00\u4e2a\u7248\u672c\u5e93\u540d\u79f0!
gb.illegalLeadingSlash = \u7981\u6b62\u4f7f\u7528\u6839\u76ee\u5f55\u5f15\u7528 (/) \u3002
gb.illegalRelativeSlash = \u76f8\u5bf9\u6587\u4ef6\u5939\u8def\u5f84(../)\u7981\u6b62\u4f7f\u7528
gb.illegalCharacterRepositoryName = \u7248\u672c\u5e93\u4e2d\u542b\u6709\u4e0d\u5408\u6cd5\u5b57\u7b26 ''{0}'' !
gb.selectAccessRestriction = \u8bf7\u9009\u62e9\u8bbf\u95ee\u6743\u9650\uff01
gb.selectFederationStrategy = \u8bf7\u9009\u62e9federation\u7b56\u7565!
gb.pleaseSetTeamName = \u8bf7\u8f93\u5165\u4e00\u4e2a\u56e2\u961f\u540d\u79f0\uff01
gb.teamNameUnavailable = \u56e2\u961f\u540d ''{0}'' \u4e0d\u5408\u6cd5.
gb.teamMustSpecifyRepository = \u56e2\u961f\u5fc5\u987b\u62e5\u6709\u81f3\u5c11\u4e00\u4e2a\u7248\u672c\u5e93\u3002
gb.teamCreated = \u6210\u529f\u521b\u5efa\u65b0\u56e2\u961f ''{0}'' .
gb.pleaseSetUsername = \u8bf7\u8f93\u5165\u7528\u6237\u540d\uff01
gb.usernameUnavailable = \u7528\u6237\u540d ''{0}'' \u4e0d\u53ef\u7528..
gb.combinedMd5Rename = Gitblit\u91c7\u7528\u6df7\u5408md5\u5bc6\u7801\u54c8\u5e0c\u3002\u56e0\u6b64\u5fc5\u987b\u5728\u4fee\u6539\u7528\u6237\u540d\u540e\u4fee\u6539\u5bc6\u7801\u3002
gb.userCreated = \u6210\u529f\u521b\u5efa\u65b0\u7528\u6237 \\"{0}\\"\u3002
gb.couldNotFindFederationRegistration = \u65e0\u6cd5\u627e\u5230federation registration!
gb.failedToFindGravatarProfile = \u52a0\u8f7d {0} \u7684Gravatar\u4fe1\u606f\u5931\u8d25
gb.branchStats = {0} \u4e2a\u63d0\u4ea4\u548c {1} \u4e2a\u6807\u7b7e\u5728 {2} \u5185
gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5e93!
gb.repositoryNotSpecifiedFor = \u6ca1\u6709\u4e3a {0} \u8bbe\u7f6e\u7248\u672c\u5e93!
gb.canNotLoadRepository = \u65e0\u6cd5\u8f7d\u5165\u7248\u672c\u5e93
gb.commitIsNull = \u63d0\u4ea4\u5185\u5bb9\u4e3a\u7a7a
gb.unauthorizedAccessForRepository = \u672a\u6388\u6743\u8bbf\u95ee\u7248\u672c\u5e93
gb.failedToFindCommit = \u5728 {1} \u4e2d {2} \u4e2a\u9875\u9762\u5185\u67e5\u627e\u63d0\u4ea4 \\"{0}\\"\u5931\u8d25!
gb.couldNotFindFederationProposal = \u65e0\u6cd5\u627e\u5230federation proposal!
gb.invalidUsernameOrPassword = \u7528\u6237\u540d\u6216\u8005\u5bc6\u7801\u9519\u8bef\uff01
gb.OneProposalToReview = 1\u4e2afederation proposals\u7b49\u5f85\u68c0\u67e5\u3002
gb.nFederationProposalsToReview = {0} \u4e2afederation proposals\u7b49\u5f85\u68c0\u67e5
gb.couldNotFindTag = \u65e0\u6cd5\u627e\u5230\u6807\u7b7e {0}
gb.couldNotCreateFederationProposal = \u65e0\u6cd5\u521b\u5efafederation proposal!
gb.pleaseSetGitblitUrl = \u8bf7\u8f93\u5165\u4f60\u7684Gitblit\u7f51\u5740!
gb.pleaseSetDestinationUrl = \u8bf7\u4e3a\u4f60\u7684proposal\u8f93\u5165\u4e00\u4e2a\u76ee\u6807\u5730\u5740!
gb.proposalReceived = \u6210\u529f\u4ece {0} \u63a5\u6536Proposal.
gb.noGitblitFound = \u62b1\u6b49, {0} \u65e0\u6cd5\u5728{1} \u4e2d\u627e\u5230Gitblit\u5b9e\u4f8b\u3002
gb.noProposals = \u62b1\u6b49, {0} \u5f53\u524d\u4e0d\u63a5\u53d7proposals\u3002
gb.noFederation = \u62b1\u6b49, {0} \u6ca1\u6709\u4e0e\u4efb\u4f55Gitblit\u5b9e\u4f8b\u8bbe\u7f6efederate\u3002.
gb.proposalFailed = \u62b1\u6b49, {0} \u65e0\u6cd5\u63a5\u53d7\u4efb\u4f55proposal\u6570\u636e!
gb.proposalError = \u62b1\u6b49\uff0c{0} \u62a5\u544a\u4e2d\u53d1\u73b0\u672a\u9884\u671f\u7684\u9519\u8bef\uff01
gb.failedToSendProposal = \u53d1\u9001proposal\u5931\u8d25!
gb.userServiceDoesNotPermitAddUser = {0} \u4e0d\u5141\u8bb8\u6dfb\u52a0\u7528\u6237!
gb.userServiceDoesNotPermitPasswordChanges = {0} \u4e0d\u5141\u8bb8\u8fdb\u884c\u5bc6\u7801\u4fee\u6539!
gb.displayName = \u663e\u793a\u540d\u79f0
gb.emailAddress = \u90ae\u7bb1
gb.errorAdminLoginRequired = \u9700\u8981\u7ba1\u7406\u5458\u767b\u9646
gb.errorOnlyAdminMayCreateRepository = \u53ea\u6709\u7ba1\u7406\u5458\u624d\u53ef\u4ee5\u521b\u5efa\u7248\u672c\u5e93
gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u5458\u6216\u8005\u6240\u6709\u8005\u624d\u53ef\u4ee5\u7f16\u8f91\u4ee3\u7801\u5e93
gb.errorAdministrationDisabled = \u7ba1\u7406\u6743\u9650\u88ab\u7981\u6b62\u3002
gb.lastNDays = \u6700\u8fd1 {0} \u5929
gb.completeGravatarProfile = \u5728Gravatar.com\u4e0a\u5b8c\u6210\u4e2a\u4eba\u8bbe\u5b9a
gb.none = \u65e0
gb.line = \u884c
gb.content = \u5185\u5bb9
gb.empty = \u7a7a\u767d\u7248\u672c\u5e93
gb.inherited = \u7ee7\u627f
gb.deleteRepository = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \uff1f
gb.repositoryDeleted = \u7248\u672c\u5e93 ''{0}'' \u5df2\u5220\u9664\u3002
gb.repositoryDeleteFailed = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \u5931\u8d25\uff01
gb.deleteUser = \u5220\u9664\u7528\u6237 \\"{0}\\" \uff1f
gb.userDeleted = \u7528\u6237 ''{0}'' \u5df2\u5220\u9664\uff01
gb.userDeleteFailed = \u5220\u9664\u7528\u6237''{0}''\u5931\u8d25\uff01
gb.time.justNow = \u521a\u521a
gb.time.today = \u4eca\u5929
gb.time.yesterday = \u6628\u5929
gb.time.minsAgo = {0} \u5206\u949f\u4ee5\u524d
gb.time.hoursAgo = {0} \u5c0f\u65f6\u4ee5\u524d
gb.time.daysAgo = {0} \u5929\u4ee5\u524d
gb.time.weeksAgo = {0} \u5468\u4ee5\u524d
gb.time.monthsAgo = {0} \u4e2a\u6708\u4ee5\u524d
gb.time.oneYearAgo = 1 \u5e74\u4ee5\u524d
gb.time.yearsAgo = {0} \u5e74\u4ee5\u524d
gb.duration.oneDay = 1 \u5929
gb.duration.days = {0} \u5929
gb.duration.oneMonth = 1 \u6708
gb.duration.months = {0} \u6708
gb.duration.oneYear = 1 \u5e74
gb.duration.years = {0} \u5e74
gb.authorizationControl = \u6388\u6743\u63a7\u5236
gb.allowAuthenticatedDescription = \u6388\u4e88\u6240\u6709\u8ba4\u8bc1\u7528\u6237\u53d7\u9650\u5236\u7684\u8bbf\u95ee\u6743\u9650
gb.allowNamedDescription = \u6388\u4e88\u6307\u5b9a\u540d\u79f0\u7684\u7528\u6237\u6216\u56e2\u961f\u53d7\u9650\u5236\u7684\u8bbf\u95ee\u6743\u9650
gb.markdownFailure = \u8bfb\u53d6 Markdown \u5185\u5bb9\u5931\u8d25\uff01
gb.clearCache = \u6e05\u9664\u7f13\u5b58
gb.projects = \u9879\u76ee
gb.project = \u9879\u76ee
gb.allProjects = \u6240\u6709\u9879\u76ee
gb.copyToClipboard = \u590d\u5236\u5230\u526a\u8d34\u677f
gb.fork = \u6d3e\u751f
gb.forks = \u6d3e\u751f
gb.forkRepository = \u6d3e\u751f {0} ?
gb.repositoryForked = {0} \u5df2\u88ab\u6d3e\u751f
gb.repositoryForkFailed = \u6d3e\u751f\u5931\u8d25
gb.personalRepositories = \u79c1\u4eba\u7248\u672c\u5e93
gb.allowForks = \u5141\u8bb8\u6d3e\u751f
gb.allowForksDescription = \u5141\u8bb8\u8ba4\u8bc1\u7528\u6237\u6d3e\u751f\u6b64\u7248\u672c\u5e93
gb.forkedFrom = \u6d3e\u751f\u81ea
gb.canFork = \u5141\u8bb8\u6d3e\u751f
gb.canForkDescription = \u5141\u8bb8\u6d3e\u751f\u8ba4\u8bc1\u7248\u672c\u5e93\u5230\u79c1\u4eba\u7248\u672c\u5e93
gb.myFork = \u67e5\u770b\u6211\u7684\u6d3e\u751f
gb.forksProhibited = \u7981\u6b62\u6d3e\u751f
gb.forksProhibitedWarning = \u5f53\u524d\u7248\u672c\u5e93\u7981\u6b62\u6d3e\u751f
gb.noForks = {0} \u6ca1\u6709\u6d3e\u751f
gb.forkNotAuthorized = \u62b1\u6b49\uff0c\u4f60\u65e0\u6743\u6d3e\u751f {0}
gb.forkInProgress = \u6b63\u5728\u6d3e\u751f
gb.preparingFork = \u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u6d3e\u751f...
gb.isFork = \u5df2\u6d3e\u751f
gb.canCreate = \u5141\u8bb8\u521b\u5efa
gb.canCreateDescription = \u5141\u8bb8\u521b\u5efa\u79c1\u4eba\u7248\u672c\u5e93
gb.illegalPersonalRepositoryLocation = \u60a8\u7684\u79c1\u4eba\u7248\u672c\u5e93\u5fc5\u987b\u4f4d\u4e8e \\"{0}\\"
gb.verifyCommitter = \u9a8c\u8bc1\u63d0\u4ea4\u8005
gb.verifyCommitterDescription = \u9700\u8981\u63d0\u4ea4\u8005\u7684\u8eab\u4efd\u4e0e Gitblit \u7528\u6237\u8eab\u4efd\u76f8\u7b26
gb.verifyCommitterNote = \u6240\u6709\u5408\u5e76\u9009\u9879\u9700\u8981\u4f7f\u7528 \\"--no-ff\\" \u6765\u6267\u884c\u63d0\u4ea4\u8005\u9a8c\u8bc1
gb.repositoryPermissions = \u7248\u672c\u5e93\u6743\u9650
gb.userPermissions = \u7528\u6237\u6743\u9650
gb.teamPermissions = \u56e2\u961f\u6743\u9650
gb.add = \u6dfb\u52a0
gb.noPermission = \u5220\u9664\u6b64\u6743\u9650
gb.excludePermission = {0} (exclude)
gb.viewPermission = {0} (view)
gb.clonePermission = {0} (clone)
gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
gb.permission = \u6743\u9650
gb.regexPermission = \u6b64\u6743\u9650\u662f\u901a\u8fc7\u6b63\u5219\u8868\u8fbe\u5f0f \\"{0}\\" \u8bbe\u7f6e
gb.accessDenied = \u8bbf\u95ee\u88ab\u62d2\u7edd
gb.busyCollectingGarbage = \u62b1\u6b49\uff0cGitblit\u6b63\u5728 {0} \u5185\u6e05\u7406\u5783\u573e
gb.gcPeriod = GC \u65f6\u95f4
gb.gcPeriodDescription = \u5783\u573e\u6e05\u7406\u7684\u6301\u7eed\u65f6\u95f4
gb.gcThreshold = GC \u9600\u503c
gb.gcThresholdDescription = \u6fc0\u53d1\u5783\u573e\u6e05\u7406\u7684\u6700\u5c0f objects \u5927\u5c0f
gb.ownerPermission = \u7248\u672c\u5e93\u521b\u5efa\u8005
gb.administrator = \u7ba1\u7406\u5458
gb.administratorPermission = Gitblit \u7ba1\u7406\u5458
gb.team = \u56e2\u961f
gb.teamPermission = \u901a\u8fc7 \\"{0}\\" \u56e2\u961f\u6210\u5458\u8bbe\u7f6e\u6743\u9650
gb.missing = \u4e0d\u5b58\u5728!
gb.missingPermission = \u6b64\u6743\u9650\u7684\u7248\u672c\u5e93\u4e0d\u5b58\u5728!
gb.mutable = mutable
gb.specified = specified
gb.effective = effective
gb.organizationalUnit = \u7ec4\u7ec7\u90e8\u5206
gb.organization = \u7ec4\u7ec7
gb.locality = \u5730\u533a
gb.stateProvince = \u5dde\u6216\u7701
gb.countryCode = \u56fd\u5bb6\u4ee3\u7801
gb.properties = \u5c5e\u6027
gb.issued = issued
gb.expires = \u5230\u671f
gb.expired = \u5df2\u5230\u671f
gb.expiring = \u5373\u5c06\u8fc7\u671f
gb.revoked = \u5df2\u64a4\u9500
gb.serialNumber = \u5e8f\u5217\u53f7
gb.certificates = \u8bc1\u4e66
gb.newCertificate = \u521b\u5efa\u8bc1\u4e66
gb.revokeCertificate = \u64a4\u9500\u8bc1\u4e66
gb.sendEmail = \u53d1\u9001\u90ae\u4ef6
gb.passwordHint = \u5bc6\u7801\u63d0\u793a
gb.ok = \u786e\u5b9a
gb.invalidExpirationDate = \u65e0\u6548\u7684\u8fc7\u671f\u65f6\u95f4!
gb.passwordHintRequired = \u9700\u8981\u586b\u5199\u5bc6\u7801\u63d0\u793a!
gb.viewCertificate = \u67e5\u770b\u8bc1\u4e66
gb.subject = \u4e3b\u9898
gb.issuer = \u63d0\u4ea4\u8005
gb.validFrom = \u6709\u6548\u671f\u5f00\u59cb\u81ea
gb.validUntil = \u6709\u6548\u671f\u622a\u6b62\u4e8e
gb.publicKey = \u516c\u94a5
gb.signatureAlgorithm = \u7b7e\u540d\u7b97\u6cd5
gb.sha1FingerPrint = SHA-1 \u6307\u7eb9\u7b97\u6cd5
gb.md5FingerPrint = MD5 \u6307\u7eb9\u7b97\u6cd5
gb.reason = \u7406\u7531
gb.revokeCertificateReason = \u8bf7\u9009\u62e9\u64a4\u9500\u8bc1\u4e66\u7684\u7406\u7531
gb.unspecified = \u672a\u6307\u5b9a
gb.keyCompromise = key compromise
gb.caCompromise = CA compromise
gb.affiliationChanged = \u96b6\u5c5e\u5173\u7cfb\u5df2\u4fee\u6539
gb.superseded = \u5df2\u53d6\u4ee3
gb.cessationOfOperation = \u505c\u6b62\u64cd\u4f5c
gb.privilegeWithdrawn = \u7279\u6743\u5df2\u64a4\u56de
gb.time.inMinutes = {0} \u5206\u949f\u4e4b\u5185
gb.time.inHours = {0} \u5c0f\u65f6\u4e4b\u5185
gb.time.inDays = {0} \u5929\u4e4b\u5185
gb.hostname = hostname
gb.hostnameRequired = \u8bf7\u8f93\u5165 hostname
gb.newSSLCertificate = \u521b\u5efa\u670d\u52a1\u5668 SSL \u8bc1\u4e66
gb.newCertificateDefaults = \u521b\u5efa\u8bc1\u4e66\u9ed8\u8ba4\u8bbe\u7f6e
gb.duration = \u6301\u7eed\u65f6\u95f4
gb.certificateRevoked = \u8bc1\u4e66 {0,number,0} \u5df2\u88ab\u64a4\u9500
gb.clientCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.sslCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u670d\u52a1\u5668 SSL \u8bc1\u4e66
gb.newClientCertificateMessage = \u6ce8\u610f:\\n\u6b64\u5bc6\u7801\u5e76\u975e\u7528\u6237\u5bc6\u7801, \u8fd9\u662f\u4fdd\u5b58\u7528\u6237 keystore \u7684\u5bc6\u7801\u3002  \u7531\u4e8e\u672c\u5bc6\u7801\u672a\u5b58\u50a8\uff0c\u56e0\u6b64\u4f60\u5fc5\u987b\u4e00\u4e2a\u5bc6\u7801\u63d0\u793a\uff0c\u8fd9\u4e2a\u63d0\u793a\u4f1a\u8bb0\u5f55\u5728\u7528\u6237\u7684 README \u6587\u6863\u5185\u3002
gb.certificate = \u8bc1\u4e66
gb.emailCertificateBundle = \u53d1\u9001\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.pleaseGenerateClientCertificate = \u8bf7\u4e3a {0} \u751f\u6210\u4e00\u4e2a\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.clientCertificateBundleSent = {0} \u7684\u5ba2\u6237\u7aef\u8bc1\u4e66\u5df2\u53d1\u9001
gb.enterKeystorePassword = \u8bf7\u8f93\u5165 Gitblit keystore \u5bc6\u7801
gb.warning = \u8b66\u544a
gb.jceWarning = \u60a8\u7684 JAVA \u8fd0\u884c\u73af\u5883\u4e0d\u5305\u542b \\"JCE Unlimited Strength Jurisdiction Policy\\" \u6587\u4ef6\u3002\\n\u8fd9\u5c06\u5bfc\u81f4\u60a8\u6700\u591a\u53ea\u80fd\u75287\u4e2a\u5b57\u7b26\u7684\u5bc6\u7801\u4fdd\u62a4\u60a8\u7684 keystore\u3002 \\n\u8fd9\u4e9b\u662f\u4e00\u4e9b\u53ef\u9009\u4e0b\u8f7d\u7684\u653f\u7b56\u6587\u4ef6\u3002\\n\\n\u4f60\u662f\u5426\u8981\u7ee7\u7eed\u751f\u6210\u8bc1\u4e66\uff1f\\n\\n\u9009\u62e9\u5426\u7684\u8bdd\uff0c\u5c06\u4f1a\u6253\u5f00\u4e00\u4e2a\u6d4f\u89c8\u5668\u754c\u9762\u4f9b\u60a8\u4e0b\u8f7d\u76f8\u5173\u6587\u4ef6\u3002
gb.maxActivityCommits = \u6700\u5927\u6d3b\u52a8\u63d0\u4ea4\u6570
gb.maxActivityCommitsDescription = \u6d3b\u52a8\u9875\u9762\u663e\u793a\u7684\u6700\u5927\u63d0\u4ea4\u6570
gb.noMaximum = \u65e0\u4e0a\u9650
gb.attributes = \u5c5e\u6027
gb.serveCertificate = \u4f7f\u7528\u6b64\u8bc1\u4e66\u63d0\u4f9b https \u652f\u6301
gb.sslCertificateGeneratedRestart = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684 SSL \u8bc1\u4e66.\\n\u4f60\u5fc5\u987b\u91cd\u65b0\u542f\u52a8 Gitblit \u4ee5\u4f7f\u7528\u6b64\u8bc1\u4e66\u3002\\n\\n\u5982\u679c\u60a8\u4f7f\u7528 '--alias' \u53c2\u6570\u542f\u52a8\uff0c\u4f60\u5fc5\u987b\u4e5f\u8981\u8bbe\u7f6e ''--alias {0}''\u3002
gb.validity = \u5408\u6cd5\u6027
gb.siteName = \u7f51\u7ad9\u540d\u79f0
gb.siteNameDescription = \u60a8\u7684\u670d\u52a1\u5668\u7684\u7b80\u8981\u63cf\u8ff0
gb.excludeFromActivity = \u4ece\u6d3b\u52a8\u9875\u9762\u6392\u9664
gb.isSparkleshared = repository is Sparkleshared
src/com/gitblit/wicket/pages/BasePage.java
@@ -98,6 +98,10 @@
        return GitBlitWebSession.get().getLocale().getLanguage();
    }
    
    protected String getCountryCode() {
        return GitBlitWebSession.get().getLocale().getCountry().toLowerCase();
    }
    protected TimeUtils getTimeUtils() {
        if (timeUtils == null) {
            ResourceBundle bundle;        
@@ -132,7 +136,10 @@
    private void login() {
        GitBlitWebSession session = GitBlitWebSession.get();
        if (session.isLoggedIn() && !session.isSessionInvalidated()) {
            // already have a session
            // already have a session, refresh usermodel to pick up
            // any changes to permissions or roles (issue-186)
            UserModel user = GitBlit.self().getUserModel(session.getUser().username);
            session.setUser(user);
            return;
        }
        
@@ -429,7 +436,7 @@
            GitBlitWebSession session = GitBlitWebSession.get();
            if (session.isLoggedIn()) {                
                UserModel user = session.getUser();
                boolean editCredentials = GitBlit.self().supportsCredentialChanges();
                boolean editCredentials = GitBlit.self().supportsCredentialChanges(user);
                boolean standardLogin = session.authenticationType.isStandard();
                // username, logout, and change password
src/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -51,12 +51,13 @@
            throw new RestartResponseException(getApplication().getHomePage());
        }
        
        if (!GitBlit.self().supportsCredentialChanges()) {
        UserModel user = GitBlitWebSession.get().getUser();
        if (!GitBlit.self().supportsCredentialChanges(user)) {
            error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),
                    GitBlit.getString(Keys.realm.userService, "users.conf")), true);
                    GitBlit.getString(Keys.realm.userService, "${baseFolder}/users.conf")), true);
        }
        
        setupPage(getString("gb.changePassword"), GitBlitWebSession.get().getUsername());
        setupPage(getString("gb.changePassword"), user.username);
        StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {
src/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -15,14 +15,14 @@
 */
package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.text.MessageFormat;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -150,15 +150,12 @@
                // quick links
                if (entry.isSubmodule()) {
                    // submodule
                    item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path)).setEnabled(false));
                    item.add(new ExternalLink("patch", "").setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
                            .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path)).setEnabled(false));
                    item.add(new ExternalLink("blame", "").setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(hasSubmodule));
                            .newPathParameter(repositoryName, entry.commitId, entry.path)));
                } else {
                    // tree or blob
                    item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
src/com/gitblit/wicket/pages/CommitPage.java
@@ -22,6 +22,7 @@
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
@@ -184,16 +185,14 @@
                if (entry.isSubmodule()) {
                    // submodule                    
                    item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(false));
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                    item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
                            .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(false));
                    item.add(new ExternalLink("blame", "").setEnabled(false));
                    item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
                            .newPathParameter(submodulePath, entry.objectId, entry.path))
                            .setEnabled(hasSubmodule));
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
                } else {
                    // tree or blob
                    item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -50,7 +50,7 @@
        <div class="tab-pane" id="permissions">
            <table class="plain">
                <tbody class="settings">
                    <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select class="span2" wicket:id="owner" tabindex="15" /> &nbsp;<span class="help-inline"><wicket:message key="gb.ownerDescription"></wicket:message></span></td></tr>
                    <tr><th><wicket:message key="gb.owners"></wicket:message></th><td class="edit"><span wicket:id="owners" tabindex="15" /> </td></tr>
                    <tr><th colspan="2"><hr/></th></tr>
                    <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="16" /></td></tr>
                    <tr><th colspan="2"><hr/></th></tr>
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -94,7 +94,7 @@
            // personal create permissions, inject personal repository path
            model.name = user.getPersonalPath() + "/";
            model.projectPath = user.getPersonalPath();
            model.owner = user.username;
            model.addOwner(user.username);
            // personal repositories are private by default
            model.accessRestriction = AccessRestrictionType.VIEW;
            model.authorizationControl = AuthorizationControl.NAMED;
@@ -164,6 +164,12 @@
        final RegistrantPermissionsPanel teamsPalette = new RegistrantPermissionsPanel("teams", 
                RegistrantType.TEAM, GitBlit.self().getAllTeamnames(), repositoryTeams, getAccessPermissions());
        // owners palette
        List<String> owners = new ArrayList<String>(repositoryModel.owners);
        List<String> persons = GitBlit.self().getAllUsernames();
        final Palette<String> ownersPalette = new Palette<String>("owners", new ListModel<String>(owners), new CollectionModel<String>(
              persons), new StringChoiceRenderer(), 12, true);
        // indexed local branches palette
        List<String> allLocalBranches = new ArrayList<String>();
        allLocalBranches.add(Constants.DEFAULT_BRANCH);
@@ -326,6 +332,13 @@
                    }
                    repositoryModel.indexedBranches = indexedBranches;
                    // owners
                    repositoryModel.owners.clear();
                    Iterator<String> owners = ownersPalette.getSelectedChoices();
                    while (owners.hasNext()) {
                        repositoryModel.addOwner(owners.next());
                    }
                    // pre-receive scripts
                    List<String> preReceiveScripts = new ArrayList<String>();
                    Iterator<String> pres = preReceivePalette.getSelectedChoices();
@@ -377,8 +390,7 @@
        // field names reflective match RepositoryModel fields
        form.add(new TextField<String>("name").setEnabled(allowEditName));
        form.add(new TextField<String>("description"));
        form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames())
                .setEnabled(GitBlitWebSession.get().canAdmin() && !repositoryModel.isPersonalRepository()));
        form.add(ownersPalette);
        form.add(new CheckBox("allowForks").setEnabled(GitBlit.getBoolean(Keys.web.allowForking, true)));
        DropDownChoice<AccessRestrictionType> accessRestriction = new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
                .asList(AccessRestrictionType.values()), new AccessRestrictionRenderer());
@@ -559,7 +571,7 @@
                        isAdmin = true;
                        return;
                    } else {
                        if (!model.owner.equalsIgnoreCase(user.username)) {
                        if (!model.isOwner(user.username)) {
                            // User is not an Admin nor Owner
                            error(getString("gb.errorOnlyAdminOrOwnerMayEditRepository"), true);
                        }
src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -212,7 +212,7 @@
        form.add(new SimpleAttributeModifier("autocomplete", "off"));
        // not all user services support manipulating team memberships
        boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges();
        boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges(null);
        
        // field names reflective match TeamModel fields
        form.add(new TextField<String>("name"));
src/com/gitblit/wicket/pages/EditUserPage.java
@@ -55,9 +55,9 @@
    public EditUserPage() {
        // create constructor
        super();
        if (!GitBlit.self().supportsCredentialChanges()) {
        if (!GitBlit.self().supportsAddUser()) {
            error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),
                    GitBlit.getString(Keys.realm.userService, "users.conf")), true);
                    GitBlit.getString(Keys.realm.userService, "${baseFolder}/users.conf")), true);
        }
        isCreate = true;
        setupPage(new UserModel(""));
@@ -134,7 +134,7 @@
                }
                boolean rename = !StringUtils.isEmpty(oldName)
                        && !oldName.equalsIgnoreCase(username);
                if (GitBlit.self().supportsCredentialChanges()) {
                if (GitBlit.self().supportsCredentialChanges(userModel)) {
                    if (!userModel.password.equals(confirmPassword.getObject())) {
                        error(getString("gb.passwordsDoNotMatch"));
                        return;
@@ -210,16 +210,16 @@
        form.add(new SimpleAttributeModifier("autocomplete", "off"));
        
        // not all user services support manipulating username and password
        boolean editCredentials = GitBlit.self().supportsCredentialChanges();
        boolean editCredentials = GitBlit.self().supportsCredentialChanges(userModel);
        
        // not all user services support manipulating display name
        boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges();
        boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges(userModel);
        // not all user services support manipulating email address
        boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges();
        boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges(userModel);
        // not all user services support manipulating team memberships
        boolean editTeams = GitBlit.self().supportsTeamMembershipChanges();
        boolean editTeams = GitBlit.self().supportsTeamMembershipChanges(userModel);
        // field names reflective match UserModel fields
        form.add(new TextField<String>("username").setEnabled(editCredentials));
src/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
New file
@@ -0,0 +1,53 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="nl"
      lang="nl">
<body>
<wicket:extend>
    <h2>Empty Repository</h2>
    <p></p>
        <div class="row">
            <div class="span10">
                <div class="alert alert-success">
                    <span wicket:id="repository" style="font-weight: bold;">[repository]</span> is een lege repositorie en kan niet bekeken worden door Gitblit.
                    <p></p>
                    Push aub een paar commitsome commits naar <span wicket:id="pushurl"></span>
                    <p></p>
                    <hr/>
                    Nadat u een paar commits gepushed hebt kunt u deze pagina <b>verversen</b> om de repository te bekijken.
                </div>
            </div>
        </div>
        <h3>Git Command-Line Syntax</h3>
        <span style="padding-bottom:5px;">Als u geen lokale Git repositorie heeft, kunt u deze repository clonen, er een paar bestanden naar committen en deze commits teug pushen naar Gitblit.</span>
        <p></p>
        <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
        <p></p>
        <span style="padding-bottom:5px;">Als u al een lokale Git repositorie heeft met commits kunt u deze repository als een remote toevoegen en er naar toe pushen.</span>
        <p></p>
        <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <h3>Learn Git</h3>
        Als u niet goed weet wat u met deze informatie aan moet raden we aan om het <a href="http://book.git-scm.com">Git Community Book</a> of <a href="http://progit.org/book" target="_blank">Pro Git</a> te bestuderen voor een betere begrip van hoe u Git kunt gebruiken.
        <p></p>
        <h4>Open Source Git Clients</h4>
        <ul>
            <li><a href="http://git-scm.com">Git</a> - de officiele, command-line Git</li>
            <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Windows bestandsverkenner ingetratie (officiele command-line Git is wel nodig)</li>
            <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git voor de Eclipse IDE (gebaseerd op JGit, zoals Gitblit)</li>
            <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# frontend voor Git met Windows Explorer en Visual Studio integratie</li>
            <li><a href="http://gitx.laullon.com/">GitX (L)</a> - een Mac OS X Git client</li>
        </ul>
        <p></p>
        <h4>Commercial/Closed-Source Git Clients</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Een Java Git, Mercurial, en SVN client applicatie (officiele command-line Git is wel nodig)</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - Een gratis Mac Client voor Git, Mercurial, en SVN</li>
        </ul>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
New file
@@ -0,0 +1,53 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="pt-br"
      lang="pt-br">
<body>
<wicket:extend>
    <h2>Repositório Vazio</h2>
    <p></p>
        <div class="row">
            <div class="span10">
                <div class="alert alert-success">
                    <span wicket:id="repository" style="font-weight: bold;">[repository]</span> é um repositório vazio e não pode ser visualizado pelo Gitblit.
                    <p></p>
                    Por favor faça o push de alguns commits para <span wicket:id="pushurl"></span>
                    <p></p>
                    <hr/>
                    Depois de ter feito push você poderá <b>atualizar</b> esta página para visualizar seu repositório.
                </div>
            </div>
        </div>
        <h3>Sintaxe dos comandos do Git</h3>
        <span style="padding-bottom:5px;">Se você ainda não tem um repositório local do Git, então você deve primeiro clonar este repositório, fazer commit de alguns arquivos e então fazer push desses commits para o Gitblit.</span>
        <p></p>
        <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
        <p></p>
        <span style="padding-bottom:5px;">Se você já tem um repositório Git local com alguns commits, então você deve adicionar este repositório como uma referência remota e então fazer push.</span>
        <p></p>
        <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <h3>Aprenda Git</h3>
        Se você estiver com dúvidas sobre como ultilizar essas informações, uma sugestão seria dar uma olhada no livro <a href="http://book.git-scm.com">Git Community Book</a> ou <a href="http://progit.org/book" target="_blank">Pro Git</a> para entender melhor como usar o Git.
        <p></p>
        <h4>Alguns clients do Git que são Open Source</h4>
        <ul>
            <li><a href="http://git-scm.com">Git</a> - o Git oficial através de linhas de comando</li>
            <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Faz integração do Explorer do Windows com o Git (por isso requer o Git Oficial)</li>
            <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git para a IDE Eclipse (baseada no JGit, como o Gitblit)</li>
            <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - Interface (em C#) para o Git cuja a característica é a integração com o Windows Explorer e o Visual Studio</li>
            <li><a href="http://gitx.laullon.com/">GitX (L)</a> - um Cliente do Git para Mac OS X</li>
        </ul>
        <p></p>
        <h4>Clients do Git proprietários ou com Código Fechado</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Aplicação Client (em Java) para Git, Mercurial, e SVN (por isso requer o Git Oficial)</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - Client gratuito para o Mac que suporta Git, Mercurial e SVN</li>
        </ul>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
New file
@@ -0,0 +1,55 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="zh-CN"
      lang="zh-CN">
<body>
<wicket:extend>
    <h2>空版本库</h2>
    <p></p>
        <div class="row">
            <div class="span10">
                <div class="alert alert-success">
                    <span wicket:id="repository" style="font-weight: bold;">[repository]</span> 版本库目前为空。
                    Gitblit 无法查看。
                    <p></p>
                    请往此网址进行推送 <span wicket:id="pushurl"></span>
                    <p></p>
                    <hr/>
                    当你推送完毕后你可以 <b>刷新</b> 此页面重新查看您的版本库。
                </div>
            </div>
        </div>
        <h3>Git 命令行格式</h3>
        <span style="padding-bottom:5px;">如果您没有本地 Git 版本库, 您可以克隆此版本库, 提交一些文件, 然后将您的提交推送回Gitblit。</span>
        <p></p>
        <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
        <p></p>
        <span style="padding-bottom:5px;">如果您已经有一个本地的提交过的版本库, 那么您可以将此版本库加为远程
        版本库,并进行推送。</span>
        <p></p>
        <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <h3>学习 Git</h3>
        如果您不明白这些信息什么意思, 您可以参考 <a href="http://book.git-scm.com">Git Community Book</a> 或者 <a href="http://progit.org/book" target="_blank">Pro Git</a> 去更加深入的学习 Git 的用法。
        <p></p>
        <h4>开源 Git 客户端</h4>
        <ul>
            <li><a href="http://git-scm.com">Git</a> - 官方, 命令行版本 Git</li>
            <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - 与 Windows 资源管理器集成 (需要官方, 命令行 Git 的支持)</li>
            <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git for the Eclipse IDE (基于 JGit, 类似 Gitblit)</li>
            <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# 版本的 Git 前端,与 Windows 资源管理器和 Visual Studio 集成</li>
            <li><a href="http://gitx.laullon.com/">GitX (L)</a> - Mac OS X Git 客户端</li>
        </ul>
        <p></p>
        <h4>商业/闭源 Git 客户端</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Java 版本的支持 Git, Mercurial 和 SVN 客户端应用 (需要官方, 命令行 Git 的支持)</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - 免费的 Mac Git Mercurial 以及 SVN 客户端, Mercurial, and SVN</li>
        </ul>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/ForksPage.java
@@ -58,7 +58,7 @@
                
                if (repository.isPersonalRepository()) {
                    UserModel user = GitBlit.self().getUserModel(repository.projectPath.substring(1));
                    PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
                    PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
                    item.add(new GravatarImage("anAvatar", ident, 20));
                    if (pageRepository.equals(repository)) {
                        // do not link to self
src/com/gitblit/wicket/pages/ProjectPage.java
@@ -15,9 +15,6 @@
 */
package com.gitblit.wicket.pages;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -37,7 +34,6 @@
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.Constants;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
@@ -46,7 +42,6 @@
import com.gitblit.models.Metric;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ActivityUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
@@ -111,23 +106,14 @@
        add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(project.getDisplayName(),
                null), feedLink));
        
        final String projectPath;
        if (project.isRoot) {
            projectPath = "";
        } else {
            projectPath = projectName + "/";
        }
        // project markdown message
        File pmkd = new File(GitBlit.getRepositoriesFolder(),  projectPath + "project.mkd");
        String pmessage = readMarkdown(projectName, pmkd);
        String pmessage = transformMarkdown(project.projectMarkdown);
        Component projectMessage = new Label("projectMessage", pmessage)
                .setEscapeModelStrings(false).setVisible(pmessage.length() > 0);
        add(projectMessage);
        // markdown message above repositories list
        File rmkd = new File(GitBlit.getRepositoriesFolder(),  projectPath + "repositories.mkd");
        String rmessage = readMarkdown(projectName, rmkd);
        String rmessage = transformMarkdown(project.repositoriesMarkdown);
        Component repositoriesMessage = new Label("repositoriesMessage", rmessage)
                .setEscapeModelStrings(false).setVisible(rmessage.length() > 0);
        add(repositoriesMessage);
@@ -300,8 +286,8 @@
    @Override
    protected List<ProjectModel> getProjectModels() {
        if (projectModels.isEmpty()) {
            final UserModel user = GitBlitWebSession.get().getUser();
            List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
            List<RepositoryModel> repositories = getRepositoryModels();
            List<ProjectModel> projects = GitBlit.self().getProjectModels(repositories, false);
            projectModels.addAll(projects);
        }
        return projectModels;
@@ -352,20 +338,15 @@
        }
        return menu;
    }
    private String readMarkdown(String projectName, File projectMessage) {
    private String transformMarkdown(String markdown) {
        String message = "";
        if (projectMessage.exists()) {
        if (!StringUtils.isEmpty(markdown)) {
            // Read user-supplied message
            try {
                FileInputStream fis = new FileInputStream(projectMessage);
                InputStreamReader reader = new InputStreamReader(fis,
                        Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                message = MarkdownUtils.transformMarkdown(markdown);
            } catch (Throwable t) {
                message = getString("gb.failedToRead") + " " + projectMessage;
                message = getString("gb.failedToRead") + " " + markdown;
                warn(message, t);
            }
        }
src/com/gitblit/wicket/pages/ProjectsPage.java
@@ -36,7 +36,6 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
@@ -47,8 +46,6 @@
import com.gitblit.wicket.panels.LinkPanel;
public class ProjectsPage extends RootPage {
    List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
    public ProjectsPage() {
        super();
@@ -67,9 +64,7 @@
    
    @Override
    protected List<ProjectModel> getProjectModels() {
        final UserModel user = GitBlitWebSession.get().getUser();
        List<ProjectModel> projects = GitBlit.self().getProjectModels(user, false);
        return projects;
        return GitBlit.self().getProjectModels(getRepositoryModels(), false);
    }
    private void setup(PageParameters params) {
@@ -194,39 +189,47 @@
    }
    private String readDefaultMarkdown(String file) {
        String content = readDefaultMarkdown(file, getLanguageCode());
        if (StringUtils.isEmpty(content)) {
            content = readDefaultMarkdown(file, null);
        }
        return content;
    }
        String base = file.substring(0, file.lastIndexOf('.'));
        String ext = file.substring(file.lastIndexOf('.'));
        String lc = getLanguageCode();
        String cc = getCountryCode();
    private String readDefaultMarkdown(String file, String lc) {
        // try to read file_en-us.ext, file_en.ext, file.ext
        List<String> files = new ArrayList<String>();
        if (!StringUtils.isEmpty(lc)) {
            // convert to file_lc.mkd
            file = file.substring(0, file.lastIndexOf('.')) + "_" + lc
                    + file.substring(file.lastIndexOf('.'));
            if (!StringUtils.isEmpty(cc)) {
                files.add(base + "_" + lc + "-" + cc + ext);
                files.add(base + "_" + lc + "_" + cc + ext);
            }
            files.add(base + "_" + lc + ext);
        }
        String message;
        try {
            ContextRelativeResource res = WicketUtils.getResource(file);
            InputStream is = res.getResourceStream().getInputStream();
            InputStreamReader reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
            message = MarkdownUtils.transformMarkdown(reader);
            reader.close();
        } catch (ResourceStreamNotFoundException t) {
            if (lc == null) {
                // could not find default language resource
        files.add(file);
        for (String name : files) {
            String message;
            InputStreamReader reader = null;
            try {
                ContextRelativeResource res = WicketUtils.getResource(name);
                InputStream is = res.getResourceStream().getInputStream();
                reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                return message;
            } catch (ResourceStreamNotFoundException t) {
                continue;
            } catch (Throwable t) {
                message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
                error(message, t, false);
            } else {
                // ignore so we can try default language resource
                message = null;
            }
        } catch (Throwable t) {
            message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
            error(message, t, false);
                return message;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
            }
        }
        return message;
        return MessageFormat.format(getString("gb.failedToReadMessage"), file);
    }
}
src/com/gitblit/wicket/pages/RawPage.java
@@ -109,7 +109,7 @@
                        switch (type) {
                        case 2:
                            // image blobs
                            byte[] image = JGitUtils.getByteContent(r, commit.getTree(), blobPath);
                            byte[] image = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
                            response.setContentType("image/" + extension.toLowerCase());
                            response.setContentLength(image.length);
                            try {
@@ -120,7 +120,7 @@
                            break;
                        case 3:
                            // binary blobs (download)
                            byte[] binary = JGitUtils.getByteContent(r, commit.getTree(), blobPath);
                            byte[] binary = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
                            response.setContentLength(binary.length);
                            response.setContentType("application/octet-stream");
                            response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
src/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -20,6 +20,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Component;
@@ -118,7 +119,7 @@
        } else {
            // Read user-supplied message
            if (!StringUtils.isEmpty(messageSource)) {
                File file = new File(messageSource);
                File file = GitBlit.getFileOrFolder(messageSource);
                if (file.exists()) {
                    try {
                        FileInputStream fis = new FileInputStream(file);
@@ -139,37 +140,47 @@
    }
    private String readDefaultMarkdown(String file) {
        String content = readDefaultMarkdown(file, getLanguageCode());
        if (StringUtils.isEmpty(content)) {
            content = readDefaultMarkdown(file, null);
        }
        return content;
    }
    private String readDefaultMarkdown(String file, String lc) {
        String base = file.substring(0, file.lastIndexOf('.'));
        String ext = file.substring(file.lastIndexOf('.'));
        String lc = getLanguageCode();
        String cc = getCountryCode();
        // try to read file_en-us.ext, file_en.ext, file.ext
        List<String> files = new ArrayList<String>();
        if (!StringUtils.isEmpty(lc)) {
            // convert to file_lc.mkd
            file = file.substring(0, file.lastIndexOf('.')) + "_" + lc + file.substring(file.lastIndexOf('.'));
            if (!StringUtils.isEmpty(cc)) {
                files.add(base + "_" + lc + "-" + cc + ext);
                files.add(base + "_" + lc + "_" + cc + ext);
            }
            files.add(base + "_" + lc + ext);
        }
        String message;
        try {
            InputStream is = GitBlit.self().getResourceAsStream(file);
            InputStreamReader reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
            message = MarkdownUtils.transformMarkdown(reader);
            reader.close();
        } catch (ResourceStreamNotFoundException t) {
            if (lc == null) {
                // could not find default language resource
        files.add(file);
        for (String name : files) {
            String message;
            InputStreamReader reader = null;
            try {
                ContextRelativeResource res = WicketUtils.getResource(name);
                InputStream is = res.getResourceStream().getInputStream();
                reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
                message = MarkdownUtils.transformMarkdown(reader);
                reader.close();
                return message;
            } catch (ResourceStreamNotFoundException t) {
                continue;
            } catch (Throwable t) {
                message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
                error(message, t, false);
            } else {
                // ignore so we can try default language resource
                message = null;
            }
        } catch (Throwable t) {
            message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
            error(message, t, false);
                return message;
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (Exception e) {
                    }
                }
            }
        }
        return message;
        return MessageFormat.format(getString("gb.failedToReadMessage"), file);
    }
}
src/com/gitblit/wicket/pages/RepositoryPage.html
@@ -52,7 +52,7 @@
                        </div>
                    </div>
                    <div class="span7">
                        <div><span class="project" wicket:id="projectTitle">[project title]</span>/<span class="repository" wicket:id="repositoryName">[repository name]</span> <span class="hidden-phone"><span wicket:id="pageName">[page name]</span></span></div>
                        <div><span class="project" wicket:id="projectTitle">[project title]</span>/<img wicket:id="repositoryIcon" style="padding-left: 10px;"></img><span class="repository" wicket:id="repositoryName">[repository name]</span> <span class="hidden-phone"><span wicket:id="pageName">[page name]</span></span></div>
                        <span wicket:id="originRepository">[origin repository]</span>
                    </div>
                </div>
src/com/gitblit/wicket/pages/RepositoryPage.java
@@ -184,7 +184,7 @@
            showAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, false);
        }
        isOwner = GitBlitWebSession.get().isLoggedIn()
                && (model.owner != null && model.owner.equalsIgnoreCase(GitBlitWebSession.get()
                && (model.isOwner(GitBlitWebSession.get()
                        .getUsername()));
        if (showAdmin || isOwner) {
            pages.put("edit", new PageRegistration("gb.edit", EditRepositoryPage.class, params));
@@ -244,6 +244,14 @@
                        SummaryPage.class, WicketUtils.newRepositoryParameter(model.originRepository)));
                add(forkFrag);
            }
        }
        // show sparkleshare folder icon
        if (model.isSparkleshared()) {
            add(WicketUtils.newImage("repositoryIcon", "folder_star_32x32.png",
                    getString("gb.isSparkleshared")));
        } else {
            add(WicketUtils.newClearPixel("repositoryIcon").setVisible(false));
        }
        
        if (getRepositoryModel().isBare) {
@@ -326,7 +334,7 @@
            RepositoryModel model = GitBlit.self().getRepositoryModel(
                    GitBlitWebSession.get().getUser(), repositoryName);
            if (model == null) {
                if (GitBlit.self().hasRepository(repositoryName)) {
                if (GitBlit.self().hasRepository(repositoryName, true)) {
                    // has repository, but unauthorized
                    authenticationError(getString("gb.unauthorizedAccessForRepository") + " " + repositoryName);
                } else {
@@ -357,10 +365,6 @@
                submodules.put(model.path, model);
            }
        }
        return submodules;
    }
    protected Map<String, SubmoduleModel> getSubmodules() {
        return submodules;
    }
    
@@ -450,6 +454,8 @@
            Constants.SearchType searchType) {
        String name = identity == null ? "" : identity.getName();
        String address = identity == null ? "" : identity.getEmailAddress();
        name = StringUtils.removeNewlines(name);
        address = StringUtils.removeNewlines(address);
        boolean showEmail = GitBlit.getBoolean(Keys.web.showEmailAddresses, false);
        if (!showEmail || StringUtils.isEmpty(name) || StringUtils.isEmpty(address)) {
            String value = name;
src/com/gitblit/wicket/pages/SummaryPage.html
@@ -16,7 +16,7 @@
        <div class="hidden-phone" style="padding-bottom: 10px;"> 
            <table class="plain">
                <tr><th><wicket:message key="gb.description">[description]</wicket:message></th><td><span wicket:id="repositoryDescription">[repository description]</span></td></tr>
                <tr><th><wicket:message key="gb.owner">[owner]</wicket:message></th><td><span wicket:id="repositoryOwner">[repository owner]</span></td></tr>
                <tr><th><wicket:message key="gb.owners">[owner]</wicket:message></th><td><span wicket:id="repositoryOwners"><span wicket:id="owner"></span><span wicket:id="comma"></span></span></td></tr>
                <tr><th><wicket:message key="gb.lastChange">[last change]</wicket:message></th><td><span wicket:id="repositoryLastChange">[repository last change]</span></td></tr>
                <tr><th><wicket:message key="gb.stats">[stats]</wicket:message></th><td><span wicket:id="branchStats">[branch stats]</span> <span class="link"><a wicket:id="metrics"><wicket:message key="gb.metrics">[metrics]</wicket:message></a></span></td></tr>
                <tr><th style="vertical-align:top;"><wicket:message key="gb.repositoryUrl">[URL]</wicket:message>&nbsp;<img style="vertical-align: top;padding-left:3px;" wicket:id="accessRestrictionIcon" /></th><td><span wicket:id="repositoryCloneUrl">[repository clone url]</span><div wicket:id="otherUrls"></div></td></tr>
@@ -44,7 +44,11 @@
        <div style="border:1px solid #ddd;border-radius: 0 0 3px 3px;padding: 20px;">
            <div wicket:id="readmeContent" class="markdown"></div>
        </div>
    </wicket:fragment>
    </wicket:fragment>
    <wicket:fragment wicket:id="ownersFragment">
    </wicket:fragment>
</wicket:extend>    
</body>
</html>
src/com/gitblit/wicket/pages/SummaryPage.java
@@ -27,6 +27,9 @@
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.wicketstuff.googlecharts.Chart;
@@ -82,18 +85,29 @@
        // repository description
        add(new Label("repositoryDescription", getRepositoryModel().description));
        String owner = getRepositoryModel().owner;
        if (StringUtils.isEmpty(owner)) {
            add(new Label("repositoryOwner").setVisible(false));
        } else {
            UserModel ownerModel = GitBlit.self().getUserModel(owner);
            if (ownerModel != null) {
                add(new LinkPanel("repositoryOwner", null, ownerModel.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(owner)));
            } else {
                add(new Label("repositoryOwner", owner));
        // owner links
        final List<String> owners = new ArrayList<String>(getRepositoryModel().owners);
        ListDataProvider<String> ownersDp = new ListDataProvider<String>(owners);
        DataView<String> ownersView = new DataView<String>("repositoryOwners", ownersDp) {
            private static final long serialVersionUID = 1L;
            int counter = 0;
            public void populateItem(final Item<String> item) {
                UserModel ownerModel = GitBlit.self().getUserModel(item.getModelObject());
                if (ownerModel != null) {
                    item.add(new LinkPanel("owner", null, ownerModel.getDisplayName(), UserPage.class,
                            WicketUtils.newUsernameParameter(ownerModel.username)).setRenderBodyOnly(true));
                } else {
                    item.add(new Label("owner").setVisible(false));
                }
                counter++;
                item.add(new Label("comma", ",").setVisible(counter < owners.size()));
                item.setRenderBodyOnly(true);
            }
        }
        };
        ownersView.setRenderBodyOnly(true);
        add(ownersView);
        add(WicketUtils.createTimestampLabel("repositoryLastChange",
                JGitUtils.getLastChange(r), getTimeZone(), getTimeUtils()));
        if (metricsTotal == null) {
src/com/gitblit/wicket/pages/TreePage.java
@@ -137,8 +137,8 @@
                                WicketUtils.newPathParameter(submodulePath, submoduleId,
                                        "")).setEnabled(hasSubmodule));
                        links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
                                WicketUtils.newPathParameter(submodulePath, submoduleId,
                                        "")).setEnabled(hasSubmodule));
                                WicketUtils.newPathParameter(repositoryName, entry.commitId,
                                        entry.path)));
                        links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
                                submodulePath, submoduleId, "").setEnabled(hasSubmodule));
                        item.add(links);                        
src/com/gitblit/wicket/pages/UserPage.java
@@ -97,7 +97,7 @@
        email.setRenderBodyOnly(true);
        add(email.setVisible(GitBlit.getBoolean(Keys.web.showEmailAddresses, true) && !StringUtils.isEmpty(user.emailAddress)));
        
        PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress);
        PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
        add(new GravatarImage("gravatar", person, 210));
        
        UserModel sessionUser = GitBlitWebSession.get().getUser();
src/com/gitblit/wicket/panels/ActivityPanel.java
@@ -27,7 +27,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.Activity;
import com.gitblit.models.Activity.RepositoryCommit;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.CommitDiffPage;
src/com/gitblit/wicket/panels/HistoryPanel.java
@@ -15,10 +15,14 @@
 */
package com.gitblit.wicket.panels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
@@ -38,6 +42,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.PathModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.RefModel;
import com.gitblit.utils.JGitUtils;
@@ -69,6 +74,11 @@
        RevCommit commit = JGitUtils.getCommit(r, objectId);
        List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, commit);
        Map<String, SubmoduleModel> submodules = new HashMap<String, SubmoduleModel>();
        for (SubmoduleModel model : JGitUtils.getSubmodules(r, commit.getTree())) {
            submodules.put(model.path, model);
        }
        PathModel matchingPath = null;
        for (PathModel p : paths) {
            if (p.path.equals(path)) {
@@ -99,7 +109,20 @@
        }
        
        final boolean isTree = matchingPath == null ? true : matchingPath.isTree();
        final boolean isSubmodule = matchingPath == null ? true : matchingPath.isSubmodule();
        // submodule
        SubmoduleModel submodule = getSubmodule(submodules, repositoryName, matchingPath.path);
        final String submodulePath;
        final boolean hasSubmodule;
        if (submodule != null) {
            submodulePath = submodule.gitblitPath;
            hasSubmodule = submodule.hasSubmodule;
        } else {
            submodulePath = "";
            hasSubmodule = false;
        }
        final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
        List<RevCommit> commits;
        if (pageResults) {
@@ -179,6 +202,23 @@
                    links.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
                            WicketUtils.newObjectParameter(repositoryName, entry.getName())));
                    item.add(links);
                } else if (isSubmodule) {
                    // submodule
                    item.add(new Label("hashLabel", submodulePath + "@"));
                    Repository repository = GitBlit.self().getRepository(repositoryName);
                    String submoduleId = JGitUtils.getSubmoduleCommitId(repository, path, entry);
                    repository.close();
                    LinkPanel commitHash = new LinkPanel("hashLink", null, submoduleId.substring(0, hashLen),
                            TreePage.class, WicketUtils.newObjectParameter(
                                    submodulePath, submoduleId));
                    WicketUtils.setCssClass(commitHash, "shortsha1");
                    WicketUtils.setHtmlTooltip(commitHash, submoduleId);
                    item.add(commitHash.setEnabled(hasSubmodule));
                    Fragment links = new Fragment("historyLinks", "treeLinks", this);
                    links.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
                            WicketUtils.newObjectParameter(repositoryName, entry.getName())));
                    item.add(links);
                } else {                    
                    // commit
                    item.add(new Label("hashLabel", getString("gb.blob") + "@"));
@@ -230,4 +270,66 @@
    public boolean hasMore() {
        return hasMore;
    }
    protected SubmoduleModel getSubmodule(Map<String, SubmoduleModel> submodules, String repositoryName, String path) {
        SubmoduleModel model = submodules.get(path);
        if (model == null) {
            // undefined submodule?!
            model = new SubmoduleModel(path.substring(path.lastIndexOf('/') + 1), path, path);
            model.hasSubmodule = false;
            model.gitblitPath = model.name;
            return model;
        } else {
            // extract the repository name from the clone url
            List<String> patterns = GitBlit.getStrings(Keys.git.submoduleUrlPatterns);
            String submoduleName = StringUtils.extractRepositoryPath(model.url, patterns.toArray(new String[0]));
            // determine the current path for constructing paths relative
            // to the current repository
            String currentPath = "";
            if (repositoryName.indexOf('/') > -1) {
                currentPath = repositoryName.substring(0, repositoryName.lastIndexOf('/') + 1);
            }
            // try to locate the submodule repository
            // prefer bare to non-bare names
            List<String> candidates = new ArrayList<String>();
            // relative
            candidates.add(currentPath + StringUtils.stripDotGit(submoduleName));
            candidates.add(candidates.get(candidates.size() - 1) + ".git");
            // relative, no subfolder
            if (submoduleName.lastIndexOf('/') > -1) {
                String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
                candidates.add(currentPath + StringUtils.stripDotGit(name));
                candidates.add(currentPath + candidates.get(candidates.size() - 1) + ".git");
            }
            // absolute
            candidates.add(StringUtils.stripDotGit(submoduleName));
            candidates.add(candidates.get(candidates.size() - 1) + ".git");
            // absolute, no subfolder
            if (submoduleName.lastIndexOf('/') > -1) {
                String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
                candidates.add(StringUtils.stripDotGit(name));
                candidates.add(candidates.get(candidates.size() - 1) + ".git");
            }
            // create a unique, ordered set of candidate paths
            Set<String> paths = new LinkedHashSet<String>(candidates);
            for (String candidate : paths) {
                if (GitBlit.self().hasRepository(candidate)) {
                    model.hasSubmodule = true;
                    model.gitblitPath = candidate;
                    return model;
                }
            }
            // we do not have a copy of the submodule, but we need a path
            model.gitblitPath = candidates.get(0);
            return model;
        }
    }
}
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -38,6 +38,7 @@
            <div class="pull-right" style="text-align:right;padding-right:15px;">
                <span wicket:id="repositoryLinks"></span>
                <div>
                    <img class="inlineIcon" wicket:id="sparkleshareIcon" />
                    <img class="inlineIcon" wicket:id="frozenIcon" />
                    <img class="inlineIcon" wicket:id="federatedIcon" />
                                
src/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -87,6 +87,12 @@
            add(forkFrag);
        }
        if (entry.isSparkleshared()) {
            add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png", localizer.getString("gb.isSparkleshared", parent)));
        } else {
            add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
        }
        add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
        add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
@@ -121,16 +127,24 @@
            add(WicketUtils.newBlankImage("accessRestrictionIcon"));
        }
        if (StringUtils.isEmpty(entry.owner)) {
        if (ArrayUtils.isEmpty(entry.owners)) {
            add(new Label("repositoryOwner").setVisible(false));
        } else {
            UserModel ownerModel = GitBlit.self().getUserModel(entry.owner);
            String owner = entry.owner;
            if (ownerModel != null) {
                owner = ownerModel.getDisplayName();
            String owner = "";
            for (String username : entry.owners) {
                UserModel ownerModel = GitBlit.self().getUserModel(username);
                if (ownerModel != null) {
                    owner = ownerModel.getDisplayName();
                }
            }
            add(new Label("repositoryOwner", owner + " (" +
            if (entry.owners.size() > 1) {
                owner += ", ...";
            }
            Label ownerLabel = (new Label("repositoryOwner", owner + " (" +
                    localizer.getString("gb.owner", parent) + ")"));
            WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
            add(ownerLabel);
        }
        UserModel user = GitBlitWebSession.get().getUser();
src/com/gitblit/wicket/panels/RefsPanel.java
@@ -129,8 +129,14 @@
                    name = name.substring(Constants.R_TAGS.length());
                    cssClass = "tagRef";
                } else if (name.startsWith(Constants.R_NOTES)) {
                    // codereview refs
                    linkClass = CommitPage.class;
                    cssClass = "otherRef";
                } else if (name.startsWith(com.gitblit.Constants.R_GITBLIT)) {
                    // gitblit refs
                    linkClass = LogPage.class;
                    cssClass = "otherRef";
                    name = name.substring(com.gitblit.Constants.R_GITBLIT.length());
                }
                Component c = new LinkPanel("refName", null, name, linkClass,
src/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java
@@ -138,10 +138,10 @@
                    }                    
                } else if (RegistrantType.USER.equals(entry.registrantType)) {
                    // user
                    PersonIdent ident = new PersonIdent(entry.registrant, null);
                    PersonIdent ident = new PersonIdent(entry.registrant, "");
                    UserModel user = GitBlit.self().getUserModel(entry.registrant);
                    if (user != null) {
                        ident = new PersonIdent(user.getDisplayName(), user.emailAddress);
                        ident = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
                    }
                    Fragment userFragment = new Fragment("registrant", "userRegistrant", RegistrantPermissionsPanel.this);
src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -89,7 +89,7 @@
        <td class="left" style="padding-left:3px;" ><b><span class="repositorySwatch" wicket:id="repositorySwatch"></span></b> <span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span></td>
        <td class="hidden-phone"><span class="list" wicket:id="repositoryDescription">[repository description]</span></td>
        <td class="hidden-tablet hidden-phone author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
        <td class="hidden-phone" style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="forkIcon" /><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="federatedIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
        <td class="hidden-phone" style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="sparkleshareIcon" /><img class="inlineIcon" wicket:id="forkIcon" /><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="federatedIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
        <td><span wicket:id="repositoryLastChange">[last change]</span></td>
        <td class="hidden-phone" style="text-align: right;padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span></td>
        <td class="rightAlign">
src/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -49,6 +49,7 @@
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
@@ -123,22 +124,18 @@
            if (rootRepositories.size() > 0) {
                // inject the root repositories at the top of the page
                String rootPath = GitBlit.getString(Keys.web.repositoryRootGroupName, " ");
                roots.add(0, rootPath);
                groups.put(rootPath, rootRepositories);
                roots.add(0, "");
                groups.put("", rootRepositories);
            }
                        
            Map<String, ProjectModel> projects = new HashMap<String, ProjectModel>();
            for (ProjectModel project : GitBlit.self().getProjectModels(user, true)) {
                projects.put(project.name, project);
            }
            List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>();
            for (String root : roots) {
                List<RepositoryModel> subModels = groups.get(root);
                GroupRepositoryModel group = new GroupRepositoryModel(root, subModels.size());
                if (projects.containsKey(root)) {
                    group.title = projects.get(root).title;
                    group.description = projects.get(root).description;
                ProjectModel project = GitBlit.self().getProjectModel(root);
                GroupRepositoryModel group = new GroupRepositoryModel(project.name, subModels.size());
                if (project != null) {
                    group.title = project.title;
                    group.description = project.description;
                }
                groupedModels.add(group);
                Collections.sort(subModels);
@@ -237,6 +234,13 @@
                            .setEscapeModelStrings(false));
                }
                if (entry.isSparkleshared()) {
                    row.add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png",
                            getString("gb.isSparkleshared")));
                } else {
                    row.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
                }
                if (entry.isFork()) {
                    row.add(WicketUtils.newImage("forkIcon", "commit_divide_16x16.png",
                            getString("gb.isFork")));
@@ -291,14 +295,23 @@
                    row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
                }
                String owner = entry.owner;
                if (!StringUtils.isEmpty(owner)) {
                    UserModel ownerModel = GitBlit.self().getUserModel(owner);
                    if (ownerModel != null) {
                        owner = ownerModel.getDisplayName();
                String owner = "";
                if (!ArrayUtils.isEmpty(entry.owners)) {
                    // display first owner
                    for (String username : entry.owners) {
                        UserModel ownerModel = GitBlit.self().getUserModel(username);
                        if (ownerModel != null) {
                            owner = ownerModel.getDisplayName();
                            break;
                        }
                    }
                    if (entry.owners.size() > 1) {
                        owner += ", ...";
                    }
                }
                row.add(new Label("repositoryOwner", owner));
                Label ownerLabel = new Label("repositoryOwner", owner);
                WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
                row.add(ownerLabel);
                String lastChange;
                if (entry.lastChange.getTime() == 0) {
@@ -519,10 +532,12 @@
                Collections.sort(list, new Comparator<RepositoryModel>() {
                    @Override
                    public int compare(RepositoryModel o1, RepositoryModel o2) {
                        String own1 = ArrayUtils.toString(o1.owners);
                        String own2 = ArrayUtils.toString(o2.owners);
                        if (asc) {
                            return o1.owner.compareTo(o2.owner);
                            return own1.compareTo(own2);
                        }
                        return o2.owner.compareTo(o1.owner);
                        return own2.compareTo(own1);
                    }
                });
            } else if (prop.equals(SortBy.description.name())) {
src/com/gitblit/wicket/panels/TeamsPanel.java
@@ -40,7 +40,7 @@
        Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
        adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));
        add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges()));
        add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges(null)));
        final List<TeamModel> teams = GitBlit.self().getAllTeams();
        DataView<TeamModel> teamsView = new DataView<TeamModel>("teamRow",
src/com/gitblit/wicket/panels/UsersPanel.html
@@ -17,7 +17,7 @@
            </th>
            <th class="hidden-phone hidden-tablet left"><wicket:message key="gb.displayName">[display name]</wicket:message></th>
            <th class="hidden-phone hidden-tablet left"><wicket:message key="gb.emailAddress">[email address]</wicket:message></th>
            <th class="hidden-phone" style="width:120px;"><wicket:message key="gb.accessLevel">[access level]</wicket:message></th>
            <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.type">[type]</wicket:message></th>
            <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.teamMemberships">[team memberships]</wicket:message></th>
            <th class="hidden-phone" style="width:100px;"><wicket:message key="gb.repositories">[repositories]</wicket:message></th>
            <th style="width:80px;" class="right"></th>
@@ -27,7 +27,7 @@
                   <td class="left" ><span class="list" wicket:id="username">[username]</span></td>
                   <td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="displayName">[display name]</span></td>
                   <td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="emailAddress">[email address]</span></td>
                   <td class="hidden-phone left" ><span class="list" wicket:id="accesslevel">[access level]</span></td>
                   <td class="hidden-phone left" ><span style="font-size: 0.8em;" wicket:id="accountType">[account type]</span></td>
                   <td class="hidden-phone left" ><span class="list" wicket:id="teams">[team memberships]</span></td>
                   <td class="hidden-phone left" ><span class="list" wicket:id="repositories">[repositories]</span></td>
                   <td class="rightAlign"><span wicket:id="userLinks"></span></td>                  
src/com/gitblit/wicket/panels/UsersPanel.java
@@ -41,7 +41,7 @@
        Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
        adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)
                .setVisible(GitBlit.self().supportsCredentialChanges()));
                .setVisible(GitBlit.self().supportsAddUser()));
        add(adminLinks.setVisible(showAdmin));
        final List<UserModel> users = GitBlit.self().getAllUsers();
@@ -81,7 +81,7 @@
                    item.add(editLink);
                }
                item.add(new Label("accesslevel", entry.canAdmin() ? "administrator" : ""));
                item.add(new Label("accountType", entry.accountType.name() + (entry.canAdmin() ? ", admin":"")));
                item.add(new Label("teams", entry.teams.size() > 0 ? ("" + entry.teams.size()) : ""));
                item.add(new Label("repositories",
                        entry.permissions.size() > 0 ? ("" + entry.permissions.size()) : ""));
test-gitblit.properties
@@ -2,10 +2,10 @@
# Gitblit Unit Testing properties
#
git.repositoriesFolder = git
git.repositoriesFolder = ${baseFolder}/git
git.searchRepositoriesSubfolders = true
git.enableGitServlet = true
groovy.scriptsFolder = groovy
groovy.scriptsFolder = ${baseFolder}/groovy
groovy.preReceiveScripts = blockpush
groovy.postReceiveScripts = sendmail
web.authenticateViewPages = false
@@ -77,7 +77,7 @@
#federation.example1.mirror = true 
#federation.example1.mergeAccounts = true
server.tempFolder = temp
server.tempFolder = ${baseFolder}/temp
server.useNio = true
server.contextPath = /
server.httpPort = 0
test-ui-gitblit.properties
New file
@@ -0,0 +1,1203 @@
#
# Git Servlet Settings
#
# Base folder for repositories.
# This folder may contain bare and non-bare repositories but Gitblit will only
# allow you to push to bare repositories.
# Use forward slashes even on Windows!!
# e.g. c:/gitrepos
#
# SINCE 0.5.0
# RESTART REQUIRED
git.repositoriesFolder = ${baseFolder}/git
# Build the available repository list at startup and cache this list for reuse.
# This reduces disk io when presenting the repositories page, responding to rpcs,
# etc, but it means that  Gitblit will not automatically identify repositories
# added or deleted by external tools.
#
# For this case you can use curl, wget, etc to issue an rpc request to clear the
# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE)
#
# SINCE 1.1.0
git.cacheRepositoryList = true
# Search the repositories folder subfolders for other repositories.
# Repositories MAY NOT be nested (i.e. one repository within another)
# but they may be grouped together in subfolders.
# e.g. c:/gitrepos/libraries/mylibrary.git
#      c:/gitrepos/libraries/myotherlibrary.git
#
# SINCE 0.5.0
git.searchRepositoriesSubfolders = true
# Maximum number of folders to recurse into when searching for repositories.
# The default value, -1, disables depth limits.
#
# SINCE 1.1.0
git.searchRecursionDepth = -1
# List of regex exclusion patterns to match against folders found in
# *git.repositoriesFolder*.
# Use forward slashes even on Windows!!
# e.g. test/jgit\.git
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 1.1.0
git.searchExclusions =
# List of regex url patterns for extracting a repository name when locating
# submodules.
#   e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract
#   *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git*
# If no matches are found then the submodule repository name is assumed to be
# whatever trails the last / character. (e.g. gitblit.git).
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 1.1.0
git.submoduleUrlPatterns = .*?://github.com/(.*)
# Allow push/pull over http/https with JGit servlet.
# If you do NOT want to allow Git clients to clone/push to Gitblit set this
# to false.  You might want to do this if you are only using ssh:// or git://.
# If you set this false, consider changing the *web.otherUrls* setting to
# indicate your clone/push urls.
#
# SINCE 0.5.0
git.enableGitServlet = true
# If you want to restrict all git servlet access to those with valid X509 client
# certificates then set this value to true.
#
# SINCE 1.2.0
git.requiresClientCertificate = false
# Enforce date checks on client certificates to ensure that they are not being
# used prematurely and that they have not expired.
#
# SINCE 1.2.0
git.enforceCertificateValidity = true
# List of OIDs to extract from a client certificate DN to map a certificate to
# an account username.
#
# e.g. git.certificateUsernameOIDs = CN
# e.g. git.certificateUsernameOIDs = FirstName LastName
#
# SPACE-DELIMITED
# SINCE 1.2.0
git.certificateUsernameOIDs = CN
# Only serve/display bare repositories.
# If there are non-bare repositories in git.repositoriesFolder and this setting
# is true, they will be excluded from the ui.
#
# SINCE 0.9.0
git.onlyAccessBareRepositories = false
# Allow an authenticated user to create a destination repository on a push if
# the repository does not already exist.
#
# Administrator accounts can create a repository in any project.
# These repositories are created with the default access restriction and authorization
# control values.  The pushing account is set as the owner.
#
# Non-administrator accounts with the CREATE role may create personal repositories.
# These repositories are created as VIEW restricted for NAMED users.
# The pushing account is set as the owner.
#
# SINCE 1.2.0
git.allowCreateOnPush = true
# The default access restriction for new repositories.
# Valid values are NONE, PUSH, CLONE, VIEW
#  NONE = anonymous view, clone, & push
#  PUSH = anonymous view & clone and authenticated push
#  CLONE = anonymous view, authenticated clone & push
#  VIEW = authenticated view, clone, & push
#
# SINCE 1.0.0
git.defaultAccessRestriction = NONE
# The default authorization control for new repositories.
# Valid values are AUTHENTICATED and NAMED
#  AUTHENTICATED = any authenticated user is granted restricted access
#  NAMED = only named users/teams are granted restricted access
#
# SINCE 1.1.0
git.defaultAuthorizationControl = NAMED
# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!)
#
# USE AT YOUR OWN RISK!
#
# If enabled, the garbage collection executor scans all repositories once a day
# at the hour of your choosing.  The GC executor will take each repository "offline",
# one-at-a-time, to check if the repository satisfies it's GC trigger requirements.
#
# While the repository is offline it will be inaccessible from the web UI or from
# any of the other services (git, rpc, rss, etc).
#
# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block,
# especially on Windows systems, so if you are using other tools please coordinate
# their usage with your GC Executor schedule or do not use this feature.
#
# The GC algorithm complex and the JGit team advises caution when using their
# young implementation of GC.
#
# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics
#
# EXPERIMENTAL
# SINCE 1.2.0
# RESTART REQUIRED
git.enableGarbageCollection = false
# Hour of the day for the GC Executor to scan repositories.
# This value is in 24-hour time.
#
# SINCE 1.2.0
git.garbageCollectionHour = 0
# The default minimum total filesize of loose objects to trigger early garbage
# collection.
#
# You may specify a custom threshold for a repository in the repository's settings.
# Common unit suffixes of k, m, or g are supported.
#
# SINCE 1.2.0
git.defaultGarbageCollectionThreshold = 500k
# The default period, in days, between GCs for a repository.  If the total filesize
# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's
# custom threshold, this period will be short-circuited.
#
# e.g. if a repository collects 100KB of loose objects every day with a 500KB
# threshold and a period of 7 days, it will take 5 days for the loose objects to
# be collected, packed, and pruned.
#
# OR
#
# if a repository collects 10KB of loose objects every day with a 500KB threshold
# and a period of 7 days, it will take the full 7 days for the loose objects to be
# collected, packed, and pruned.
#
# You may specify a custom period for a repository in the repository's settings.
#
# The minimum value is 1 day since the GC Executor only runs once a day.
#
# SINCE 1.2.0
git.defaultGarbageCollectionPeriod = 7
# Number of bytes of a pack file to load into memory in a single read operation.
# This is the "page size" of the JGit buffer cache, used for all pack access
# operations. All disk IO occurs as single window reads. Setting this too large
# may cause the process to load more data than is required; setting this too small
# may increase the frequency of read() system calls.
#
# Default on JGit is 8 KiB on all platforms.
#
# Common unit suffixes of k, m, or g are supported.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.packedGitWindowSize = 8k
# Maximum number of bytes to load and cache in memory from pack files. If JGit
# needs to access more than this many bytes it will unload less frequently used
# windows to reclaim memory space within the process. As this buffer must be shared
# with the rest of the JVM heap, it should be a fraction of the total memory available.
#
# The JGit team recommends setting this value larger than the size of your biggest
# repository. This ensures you can serve most requests from memory.
#
# Default on JGit is 10 MiB on all platforms.
#
# Common unit suffixes of k, m, or g are supported.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.packedGitLimit = 10m
# Maximum number of bytes to reserve for caching base objects that multiple deltafied
# objects reference. By storing the entire decompressed base object in a cache Git
# is able to avoid unpacking and decompressing frequently used base objects multiple times.
#
# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust
# this value.
#
# Common unit suffixes of k, m, or g are supported.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.deltaBaseCacheLimit = 10m
# Maximum number of pack files to have open at once. A pack file must be opened
# in order for any of its data to be available in a cached window.
#
# If you increase this to a larger setting you may need to also adjust the ulimit
# on file descriptors for the host JVM, as Gitblit needs additional file descriptors
# available for network sockets and other repository data manipulation.
#
# Default on JGit is 128 file descriptors on all platforms.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.packedGitOpenFiles = 128
# Largest object size, in bytes, that JGit will allocate as a contiguous byte
# array. Any file revision larger than this threshold will have to be streamed,
# typically requiring the use of temporary files under $GIT_DIR/objects to implement
# psuedo-random access during delta decompression.
#
# Servers with very high traffic should set this to be larger than the size of
# their common big files. For example a server managing the Android platform
# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable
# setting in that environment. Setting this too high may cause the JVM to run out
# of heap space when handling very big binary files, such as device firmware or
# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly.
#
# Default is 50 MiB on all platforms.
#
# Common unit suffixes of k, m, or g are supported.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.streamFileThreshold = 50m
# When true, JGit will use mmap() rather than malloc()+read() to load data from
# pack files.  The use of mmap can be problematic on some JVMs as the garbage
# collector must deduce that a memory mapped segment is no longer in use before
# a call to munmap() can be made by the JVM native code.
#
# In server applications (such as Gitblit) that need to access many pack files,
# setting this to true risks artificially running out of virtual address space,
# as the garbage collector cannot reclaim unused mapped spaces fast enough.
#
# Default on JGit is false. Although potentially slower, it yields much more
# predictable behavior.
# Documentation courtesy of the Gerrit project.
#
# SINCE 1.0.0
# RESTART REQUIRED
git.packedGitMmap = false
#
# Groovy Integration
#
# Location of Groovy scripts to use for Pre and Post receive hooks.
# Use forward slashes even on Windows!!
# e.g. c:/groovy
#
# RESTART REQUIRED
# SINCE 0.8.0
groovy.scriptsFolder = ${baseFolder}/groovy
# Specify the directory Grape uses for downloading libraries.
# http://groovy.codehaus.org/Grape
#
# RESTART REQUIRED
# SINCE 1.0.0
groovy.grapeFolder = ${baseFolder}/groovy/grape
# Scripts to execute on Pre-Receive.
#
# These scripts execute after an incoming push has been parsed and validated
# but BEFORE the changes are applied to the repository.  You might reject a
# push in this script based on the repository and branch the push is attempting
# to change.
#
# Script names are case-sensitive on case-sensitive file systems.  You may omit
# the traditional ".groovy" from this list if your file extension is ".groovy"
#
# NOTE:
# These scripts are only executed when pushing to *Gitblit*, not to other Git
# tooling you may be using.  Also note that these scripts are shared between
# repositories. These are NOT repository-specific scripts!  Within the script
# you may customize the control-flow for a specific repository by checking the
# *repository* variable.
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.8.0
groovy.preReceiveScripts =
# Scripts to execute on Post-Receive.
#
# These scripts execute AFTER an incoming push has been applied to a repository.
# You might trigger a continuous-integration build here or send a notification.
#
# Script names are case-sensitive on case-sensitive file systems.  You may omit
# the traditional ".groovy" from this list if your file extension is ".groovy"
#
# NOTE:
# These scripts are only executed when pushing to *Gitblit*, not to other Git
# tooling you may be using.  Also note that these scripts are shared between
# repositories. These are NOT repository-specific scripts!  Within the script
# you may customize the control-flow for a specific repository by checking the
# *repository* variable.
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.8.0
groovy.postReceiveScripts =
# Repository custom fields for Groovy Hook mechanism
#
# List of key=label pairs of custom fields to prompt for in the Edit Repository
# page.  These keys are stored in the repository's git config file in the
# section [gitblit "customFields"].  Key names are alphanumeric only.  These
# fields are intended to be used for the Groovy hook mechanism where a script
# can adjust it's execution based on the custom fields stored in the repository
# config.
#
# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another
#
# SPACE-DELIMITED
# SINCE 1.0.0
groovy.customFields =
#
# Authentication Settings
#
# Require authentication to see everything but the admin pages
#
# SINCE 0.5.0
# RESTART REQUIRED
web.authenticateViewPages = false
# Require admin authentication for the admin functions and pages
#
# SINCE 0.5.0
# RESTART REQUIRED
web.authenticateAdminPages = true
# Allow Gitblit to store a cookie in the user's browser for automatic
# authentication.  The cookie is generated by the user service.
#
# SINCE 0.5.0
web.allowCookieAuthentication = true
# Config file for storing project metadata
#
# SINCE 1.2.0
web.projectsFile = ${baseFolder}/projects.conf
# Either the full path to a user config file (users.conf)
# OR the full path to a simple user properties file (users.properties)
# OR a fully qualified class name that implements the IUserService interface.
#
# Alternative user services:
#    com.gitblit.LdapUserService
#    com.gitblit.RedmineUserService
#
# Any custom user service implementation must have a public default constructor.
#
# SINCE 0.5.0
# RESTART REQUIRED
realm.userService = test-ui-users.conf
# How to store passwords.
# Valid values are plain, md5, or combined-md5.  md5 is the hash of password.
# combined-md5 is the hash of username.toLowerCase()+password.
# Default is md5.
#
# SINCE 0.5.0
realm.passwordStorage = md5
# Minimum valid length for a plain text password.
# Default value is 5.  Absolute minimum is 4.
#
# SINCE 0.5.0
realm.minPasswordLength = 5
#
# Gitblit Web Settings
#
# If blank Gitblit is displayed.
#
# SINCE 0.5.0
web.siteName =
# If *web.authenticateAdminPages*=true, users with "admin" role can create
# repositories, create users, and edit repository metadata.
#
# If *web.authenticateAdminPages*=false, any user can execute the aforementioned
# functions.
#
# SINCE 0.5.0
web.allowAdministration = true
# Allows rpc clients to list repositories and possibly manage or administer the
# Gitblit server, if the authenticated account has administrator permissions.
# See *web.enableRpcManagement* and *web.enableRpcAdministration*.
#
# SINCE 0.7.0
web.enableRpcServlet = true
# Allows rpc clients to manage repositories and users of the Gitblit instance,
# if the authenticated account has administrator permissions.
# Requires *web.enableRpcServlet=true*.
#
# SINCE 0.7.0
web.enableRpcManagement = false
# Allows rpc clients to control the server settings and monitor the health of this
# this Gitblit instance, if the authenticated account has administrator permissions.
# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*.
#
# SINCE 0.7.0
web.enableRpcAdministration = false
# Full path to a configurable robots.txt file.  With this file you can control
# what parts of your Gitblit server respectable robots are allowed to traverse.
# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html
#
# SINCE 1.0.0
web.robots.txt =
# If true, the web ui layout will respond and adapt to the browser's dimensions.
# if false, the web ui will use a 940px fixed-width layout.
# http://twitter.github.com/bootstrap/scaffolding.html#responsive
#
# SINCE 1.0.0
web.useResponsiveLayout = true
# Allow Gravatar images to be displayed in Gitblit pages.
#
# SINCE 0.8.0
web.allowGravatar = true
# Allow dynamic zip downloads.
#
# SINCE 0.5.0
web.allowZipDownloads = true
# If *web.allowZipDownloads=true* the following formats will be displayed for
# download compressed archive links:
#
# zip   = standard .zip
# tar   = standard tar format (preserves *nix permissions and symlinks)
# gz    = gz-compressed tar
# xz    = xz-compressed tar
# bzip2 = bzip2-compressed tar
#
# SPACE-DELIMITED
# SINCE 1.2.0
web.compressedDownloads = zip gz
# Allow optional Lucene integration. Lucene indexing is an opt-in feature.
# A repository may specify branches to index with Lucene instead of using Git
# commit traversal. There are scenarios where you may want to completely disable
# Lucene indexing despite a repository specifying indexed branches.  One such
# scenario is on a resource-constrained federated Gitblit mirror.
#
# SINCE 0.9.0
web.allowLuceneIndexing = false
# Allows an authenticated user to create forks of a repository
#
# set this to false if you want to disable all fork controls on the web site
#
web.allowForking = true
# Controls the length of shortened commit hash ids
#
# SINCE 1.2.0
web.shortCommitIdLength = 6
# Use Clippy (Flash solution) to provide a copy-to-clipboard button.
# If false, a button with a more primitive JavaScript-based prompt box will
# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative.
#
# SINCE 0.8.0
web.allowFlashCopyToClipboard = true
# Default maximum number of commits that a repository may contribute to the
# activity page, regardless of the selected duration.  This setting may be valuable
# for an extremely busy server.  This value may also be configed per-repository
# in Edit Repository. 0 disables this throttle.
#
# SINCE 1.2.0
web.maxActivityCommits = 0
# Default number of entries to include in RSS Syndication links
#
# SINCE 0.5.0
web.syndicationEntries = 25
# Show the size of each repository on the repositories page.
# This requires recursive traversal of each repository folder.  This may be
# non-performant on some operating systems and/or filesystems.
#
# SINCE 0.5.2
web.showRepositorySizes = true
# List of custom regex expressions that can be displayed in the Filters menu
# of the Repositories and Activity pages.  Keep them very simple because you
# are likely to run into encoding issues if they are too complex.
#
# Use !!! to separate the filters
#
# SINCE 0.8.0
web.customFilters =
# Show federation registrations (without token) and the current pull status
# to non-administrator users.
#
# SINCE 0.6.0
web.showFederationRegistrations = false
# This is the message displayed when *web.authenticateViewPages=true*.
# This can point to a file with Markdown content.
# Specifying "gitblit" uses the internal login message.
#
# SINCE 0.7.0
web.loginMessage = gitblit
# This is the message displayed above the repositories table.
# This can point to a file with Markdown content.
# Specifying "gitblit" uses the internal welcome message.
#
# SINCE 0.5.0
web.repositoriesMessage = gitblit
# Ordered list of charsets/encodings to use when trying to display a blob.
# If empty, UTF-8 and ISO-8859-1 are used.  The server's default charset
# is always appended to the encoding list.  If all encodings fail to cleanly
# decode the blob content, UTF-8 will be used with the standard malformed
# input/unmappable character replacement strings.
#
# SPACE-DELIMITED
# SINCE 1.0.0
web.blobEncodings = UTF-8 ISO-8859-1
# Manually set the default timezone to be used by Gitblit for display in the
# web ui.  This value is independent of the JVM timezone.  Specifying a blank
# value will default to the JVM timezone.
# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin
#
# SINCE 0.9.0
# RESTART REQUIRED
web.timezone =
# Use the client timezone when formatting dates.
# 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.
#
# SINCE 0.5.0
# RESTART REQUIRED
web.useClientTimezone = false
# Time format
# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
#
# SINCE 0.8.0
web.timeFormat = HH:mm
# Short date format
# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
#
# SINCE 0.5.0
web.datestampShortFormat = yyyy-MM-dd
# Long date format
#
# SINCE 0.8.0
web.datestampLongFormat = EEEE, MMMM d, yyyy
# Long timestamp format
# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
#
# SINCE 0.5.0
web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z
# Mount URL parameters
# This setting controls if pretty or parameter URLs are used.
# i.e.
# if true:
#     http://localhost/commit/myrepo/abcdef
# if false:
#     http://localhost/commit/?r=myrepo&h=abcdef
#
# SINCE 0.5.0
# RESTART REQUIRED
web.mountParameters = true
# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding
# in URLs as a security precaution for proxies.  This setting tells Gitblit
# to preemptively replace '/' with '*' or '!' for url string parameters.
#
# <https://issues.apache.org/jira/browse/WICKET-1303>
# <http://tomcat.apache.org/security-6.html#Fixed_in_Apache_Tomcat_6.0.10>
# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your
# *CATALINA_OPTS* or to your JVM launch parameters
#
# SINCE 0.5.2
web.forwardSlashCharacter = /
# Show other URLs on the summary page for accessing your git repositories
# Use spaces to separate urls. {0} is the token for the repository name.
# e.g.
# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0}
#
# SPACE-DELIMITED
# SINCE 0.5.0
web.otherUrls =
# Choose how to present the repositories list.
#   grouped = group nested/subfolder repositories together (no sorting)
#   flat = flat list of repositories (sorting allowed)
#
# SINCE 0.5.0
web.repositoryListType = grouped
# If using a grouped repository list and there are repositories at the
# root level of your repositories folder, you may specify the displayed
# group name with this setting.  This value is only used for web presentation.
#
# SINCE 0.5.0
web.repositoryRootGroupName = main
# Display the repository swatch color next to the repository name link in the
# repositories list.
#
# SINCE 0.8.0
web.repositoryListSwatches = true
# Choose the diff presentation style: gitblt, gitweb, or plain
#
# SINCE 0.5.0
web.diffStyle = gitblit
# Control if email addresses are shown in web ui
#
# SINCE 0.5.0
web.showEmailAddresses = true
# Shows a combobox in the page links header with commit, committer, and author
# search selection.  Default search is commit.
#
# SINCE 0.5.0
web.showSearchTypeSelection = false
# Generates a line graph of repository activity over time on the Summary page.
# This uses the Google Charts API.
#
# SINCE 0.5.0
web.generateActivityGraph = true
# The number of days to show on the activity page.
# Value must exceed 0 else default of 14 is used
#
# SINCE 0.8.0
web.activityDuration = 14
# The number of commits to display on the summary page
# Value must exceed 0 else default of 20 is used
#
# SINCE 0.5.0
web.summaryCommitCount = 16
# The number of tags/branches to display on the summary page.
# -1 = all tags/branches
# 0 = hide tags/branches
# N = N tags/branches
#
# SINCE 0.5.0
web.summaryRefsCount = 5
# The number of items to show on a page before showing the first, prev, next
# pagination links.  A default if 50 is used for any invalid value.
#
# SINCE 0.5.0
web.itemsPerPage = 50
# Registered file extensions to ignore during Lucene indexing
#
# SPACE-DELIMITED
# SINCE 0.9.0
web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip
# Registered extensions for google-code-prettify
#
# SPACE-DELIMITED
# SINCE 0.5.0
web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb
# Registered extensions for markdown transformation
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.5.0
web.markdownExtensions = md mkd markdown MD MKD
# Image extensions
#
# SPACE-DELIMITED
# SINCE 0.5.0
web.imageExtensions = bmp jpg gif png
# Registered extensions for binary blobs
#
# SPACE-DELIMITED
# SINCE 0.5.0
web.binaryExtensions = jar pdf tar.gz zip
# Aggressive heap management will run the garbage collector on every generated
# page.  This slows down page generation a little but improves heap consumption.
#
# SINCE 0.5.0
web.aggressiveHeapManagement = false
# Run the webapp in debug mode
#
# SINCE 0.5.0
# RESTART REQUIRED
web.debugMode = false
# Enable/disable global regex substitutions (i.e. shared across repositories)
#
# SINCE 0.5.0
regex.global = true
# Example global regex substitutions
# Use !!! to separate the search pattern and the replace pattern
# searchpattern!!!replacepattern
# SINCE 0.5.0
regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://somehost/bug/$3">Bug-Id: $3</a>
# SINCE 0.5.0
regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!<a href="http://somehost/changeid/$2">Change-Id: $2</a>
# Example per-repository regex substitutions overrides global
# SINCE 0.5.0
regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://elsewhere/bug/$3">Bug-Id: $3</a>
#
# Mail Settings
# SINCE 0.6.0
#
# Mail settings are used to notify administrators of received federation proposals
#
# ip or hostname of smtp server
#
# SINCE 0.6.0
mail.server =
# port to use for smtp requests
#
# SINCE 0.6.0
mail.port = 25
# debug the mail executor
#
# SINCE 0.6.0
mail.debug = false
# if your smtp server requires authentication, supply the credentials here
#
# SINCE 0.6.0
mail.username =
# SINCE 0.6.0
mail.password =
# from address for generated emails
#
# SINCE 0.6.0
mail.fromAddress =
# List of email addresses for the Gitblit administrators
#
# SPACE-DELIMITED
# SINCE 0.6.0
mail.adminAddresses =
# List of email addresses for sending push email notifications.
#
# This key currently requires use of the sendemail.groovy hook script.
# If you set sendemail.groovy in *groovy.postReceiveScripts* then email
# notifications for all repositories (regardless of access restrictions!)
# will be sent to these addresses.
#
# SPACE-DELIMITED
# SINCE 0.8.0
mail.mailingLists =
#
# Federation Settings
# SINCE 0.6.0
#
# A Gitblit federation is a way to backup one Gitblit instance to another.
#
# *git.enableGitServlet* must be true to use this feature.
# Your federation name is used for federation status acknowledgments.  If it is
# unset, and you elect to send a status acknowledgment, your Gitblit instance
# will be identified by its hostname, if available, else your internal ip address.
# The source Gitblit instance will also append your external IP address to your
# identification to differentiate multiple pulling systems behind a single proxy.
#
# SINCE 0.6.0
federation.name =
# Specify the passphrase of this Gitblit instance.
#
# An unspecified (empty) passphrase disables processing federation requests.
#
# This value can be anything you want: an integer, a sentence, an haiku, etc.
# Keep the value simple, though, to avoid Java properties file encoding issues.
#
# Changing your passphrase will break any registrations you have established with other
# Gitblit instances.
#
# CASE-SENSITIVE
# SINCE 0.6.0
# RESTART REQUIRED *(only to enable or disable federation)*
federation.passphrase =
# Control whether or not this Gitblit instance can receive federation proposals
# from another Gitblit instance.  Registering a federated Gitblit is a manual
# process.  Proposals help to simplify that process by allowing a remote Gitblit
# instance to send your Gitblit instance the federation pull data.
#
# SINCE 0.6.0
federation.allowProposals = false
# The destination folder for cached federation proposals.
# Use forward slashes even on Windows!!
#
# SINCE 0.6.0
federation.proposalsFolder = ${baseFolder}/proposals
# The default pull frequency if frequency is unspecified on a registration
#
# SINCE 0.6.0
federation.defaultFrequency = 60 mins
# Federation Sets are named groups of repositories.  The Federation Sets are
# available for selection in the repository settings page.  You can assign a
# repository to one or more sets and then distribute the token for the set.
# This allows you to grant federation pull access to a subset of your available
# repositories.  Tokens for federation sets only grant repository pull access.
#
# SPACE-DELIMITED
# CASE-SENSITIVE
# SINCE 0.6.0
federation.sets =
# Federation pull registrations
# Registrations are read once, at startup.
#
# RESTART REQUIRED
#
# frequency:
#   The shortest frequency allowed is every 5 minutes
#   Decimal frequency values are cast to integers
#   Frequency values may be specified in mins, hours, or days
#   Values that can not be parsed or are unspecified default to *federation.defaultFrequency*
#
# folder:
#   if unspecified, the folder is *git.repositoriesFolder*
#   if specified, the folder is relative to *git.repositoriesFolder*
#
# bare:
#   if true, each repository will be created as a *bare* repository and will not
#   have a working directory.
#
#   if false, each repository will be created as a normal repository suitable
#   for local work.
#
# mirror:
#   if true, each repository HEAD is reset to *origin/master* after each pull.
#   The repository will be flagged *isFrozen* after the initial clone.
#
#   if false, each repository HEAD will point to the FETCH_HEAD of the initial
#   clone from the origin until pushed to or otherwise manipulated.
#
# mergeAccounts:
#   if true, remote accounts and their permissions are merged into your
#   users.properties file
#
# notifyOnError:
#   if true and the mail configuration is properly set, administrators will be
#   notified by email of pull failures
#
# include and exclude:
#   Space-delimited list of repositories to include or exclude from pull
#   may be * wildcard to include or exclude all
#   may use fuzzy match (e.g. org.eclipse.*)
#
# (Nearly) Perfect Mirror example
#
#federation.example1.url = https://go.gitblit.com
#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
#federation.example1.frequency = 120 mins
#federation.example1.folder =
#federation.example1.bare = true
#federation.example1.mirror = true
#federation.example1.mergeAccounts = true
#
# Advanced Realm Settings
#
# URL of the LDAP server.
# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to
# send StartTLS command.
#
# SINCE 1.0.0
realm.ldap.server = ldap://localhost
# Login username for LDAP searches.
# If this value is unspecified, anonymous LDAP login will be used.
#
# e.g. mydomain\\username
#
# SINCE 1.0.0
realm.ldap.username = cn=Directory Manager
# Login password for LDAP searches.
#
# SINCE 1.0.0
realm.ldap.password = password
# The LdapUserService must be backed by another user service for standard user
# and team management.
# default: users.conf
#
# SINCE 1.0.0
# RESTART REQUIRED
realm.ldap.backingUserService = test-ui-users.conf
# Delegate team membership control to LDAP.
#
# If true, team user memberships will be specified by LDAP groups.  This will
# disable team selection in Edit User and user selection in Edit Team.
#
# If false, LDAP will only be used for authentication and Gitblit will maintain
# team memberships with the *realm.ldap.backingUserService*.
#
# SINCE 1.0.0
realm.ldap.maintainTeams = false
# Root node for all LDAP users
#
# This is the root node from which subtree user searches will begin.
# If blank, Gitblit will search ALL nodes.
#
# SINCE 1.0.0
realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
# Filter criteria for LDAP users
#
# Query pattern to use when searching for a user account. This may be any valid
# LDAP query expression, including the standard (&) and (|) operators.
#
# Variables may be injected via the ${variableName} syntax.
# Recognized variables are:
#    ${username} - The text entered as the user name
#
# SINCE 1.0.0
realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username}))
# Root node for all LDAP groups to be used as Gitblit Teams
#
# This is the root node from which subtree team searches will begin.
# If blank, Gitblit will search ALL nodes.
#
# SINCE 1.0.0
realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
# Filter criteria for LDAP groups
#
# Query pattern to use when searching for a team. This may be any valid
# LDAP query expression, including the standard (&) and (|) operators.
#
# Variables may be injected via the ${variableName} syntax.
# Recognized variables are:
#    ${username} - The text entered as the user name
#    ${dn} - The Distinguished Name of the user logged in
#
# All attributes from the LDAP User record are available. For example, if a user
# has an attribute "fullName" set to "John", "(fn=${fullName})" will be
# translated to "(fn=John)".
#
# SINCE 1.0.0
realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn}))
# LDAP users or groups that should be given administrator privileges.
#
# Teams are specified with a leading '@' character.  Groups with spaces in the
# name can be entered as "@team name".
#
# e.g. realm.ldap.admins = john @git_admins "@git admins"
#
# SPACE-DELIMITED
# SINCE 1.0.0
realm.ldap.admins = @Git_Admins
# Attribute(s) on the USER record that indicate their display (or full) name.
# Leave blank for no mapping available in LDAP.
#
# This may be a single attribute, or a string of multiple attributes.  Examples:
#  displayName - Uses the attribute 'displayName' on the user record
#  ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3
#       attributes together, with a '.' after personalTitle
#
# SINCE 1.0.0
realm.ldap.displayName = displayName
# Attribute(s) on the USER record that indicate their email address.
# Leave blank for no mapping available in LDAP.
#
# This may be a single attribute, or a string of multiple attributes.  Examples:
#  email - Uses the attribute 'email' on the user record
#  ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes
#       together with a '.' and '@' creating something like first.last@gitblit.com
#
# SINCE 1.0.0
realm.ldap.email = email
# The RedmineUserService must be backed by another user service for standard user
# and team management.
# default: users.conf
#
# RESTART REQUIRED
realm.redmine.backingUserService = test-ui-users.conf
# URL of the Redmine.
realm.redmine.url = http://example.com/redmine
#
# Server Settings
#
# The temporary folder to decompress the embedded gitblit webapp.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.tempFolder = ${baseFolder}/temp
# Use Jetty NIO connectors.  If false, Jetty Socket connectors will be used.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.useNio = true
# Context path for the GO application.  You might want to change the context
# path if running Gitblit behind a proxy layer such as mod_proxy.
#
# SINCE 0.7.0
# RESTART REQUIRED
server.contextPath = /
# Standard http port to serve.  <= 0 disables this connector.
# On Unix/Linux systems, ports < 1024 require root permissions.
# Recommended value: 80 or 8080
#
# SINCE 0.5.0
# RESTART REQUIRED
server.httpPort = 0
# Secure/SSL https port to serve. <= 0 disables this connector.
# On Unix/Linux systems, ports < 1024 require root permissions.
# Recommended value: 443 or 8443
#
# SINCE 0.5.0
# RESTART REQUIRED
server.httpsPort = 8443
# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating
# Gitblit GO into an Apache HTTP server setup.  <= 0 disables this connector.
# Recommended value: 8009
#
# SINCE 0.9.0
# RESTART REQUIRED
server.ajpPort = 0
# Specify the interface for Jetty to bind the standard connector.
# You may specify an ip or an empty value to bind to all interfaces.
# Specifying localhost will result in Gitblit ONLY listening to requests to
# localhost.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.httpBindInterface = localhost
# Specify the interface for Jetty to bind the secure connector.
# You may specify an ip or an empty value to bind to all interfaces.
# Specifying localhost will result in Gitblit ONLY listening to requests to
# localhost.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.httpsBindInterface = localhost
# Specify the interface for Jetty to bind the AJP connector.
# You may specify an ip or an empty value to bind to all interfaces.
# Specifying localhost will result in Gitblit ONLY listening to requests to
# localhost.
#
# SINCE 0.9.0
# RESTART REQUIRED
server.ajpBindInterface = localhost
# Alias of certificate to use for https/SSL serving.  If blank the first
# certificate found in the keystore will be used.
#
# SINCE 1.2.0
# RESTART REQUIRED
server.certificateAlias = localhost
# Password for SSL keystore.
# Keystore password and certificate password must match.
# This is provided for convenience, its probably more secure to set this value
# using the --storePassword command line parameter.
#
# If you are using the official JRE or JDK from Oracle you may not have the
# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM.  Because
# of this, your store/key password can not exceed 7 characters.  If you require
# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction
# Policy files from Oracle.
#
# http://www.oracle.com/technetwork/java/javase/downloads/index.html
#
# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited
# Strength encryption is available.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.storePassword = gitblit
# If serving over https (recommended) you might consider requiring clients to
# authenticate with ssl certificates.  If enabled, only https clients with the
# a valid client certificate will be able to access Gitblit.
#
# If disabled, client certificate authentication is optional and will be tried
# first before falling-back to form authentication or basic authentication.
#
# Requiring client certificates to access any of Gitblit may be too extreme,
# consider this carefully.
#
# SINCE 1.2.0
# RESTART REQUIRED
server.requireClientCertificates = false
# Port for shutdown monitor to listen on.
#
# SINCE 0.5.0
# RESTART REQUIRED
server.shutdownPort = 8081
test-ui-users.conf
New file
@@ -0,0 +1,44 @@
[user "admin"]
    password = admin
    cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
    role = "#admin"
    role = "#notfederated"
[user "userthree"]
    password = StoredInLDAP
    cookie = d7d3894fc517612aa6c595555b6e1ab8e147e597
    displayName = User Three
    emailAddress = userthree@gitblit.com
    role = "#admin"
[user "userone"]
    password = StoredInLDAP
    cookie = c97cd38e50858cd0b389ec61b18fb9a89b4da54c
    displayName = User One
    emailAddress = User.One@gitblit.com
    role = "#admin"
[user "usertwo"]
    password = StoredInLDAP
    cookie = 498ca9bd2841d39050fa45d1d737b9f9f767858d
    displayName = User Two
    emailAddress = usertwo@gitblit.com
    role = "#admin"
[user "basic"]
    password = MD5:f17aaabc20bfe045075927934fed52d2
    cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
    role = "#fork"
    repository = RW:~repocreator/shb.git
    repository = V:test/gitective.git
[user "repocreator"]
    password = MD5:b77e53bb561c47368d133b22e285f60b
    cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
    role = "#create"
[team "Git_Admins"]
    role = "#none"
    user = userone
[team "Git_Users"]
    role = "#none"
    user = userone
    user = usertwo
    user = userthree
[team "Git Admins"]
    role = "#none"
    user = usertwo
tests/com/gitblit/tests/FanoutServiceTest.java
New file
@@ -0,0 +1,172 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.tests;
import static org.junit.Assert.assertEquals;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import com.gitblit.fanout.FanoutService;
import com.gitblit.fanout.FanoutClient;
import com.gitblit.fanout.FanoutClient.FanoutAdapter;
import com.gitblit.fanout.FanoutNioService;
import com.gitblit.fanout.FanoutService;
import com.gitblit.fanout.FanoutSocketService;
public class FanoutServiceTest {
    int fanoutPort = FanoutService.DEFAULT_PORT;
    @Test
    public void testNioPubSub() throws Exception {
        testPubSub(new FanoutNioService(fanoutPort));
    }
    @Test
    public void testSocketPubSub() throws Exception {
        testPubSub(new FanoutSocketService(fanoutPort));
    }
    @Test
    public void testNioDisruptionAndRecovery() throws Exception {
        testDisruption(new FanoutNioService(fanoutPort));
    }
    @Test
    public void testSocketDisruptionAndRecovery() throws Exception {
        testDisruption(new FanoutSocketService(fanoutPort));
    }
    protected void testPubSub(FanoutService service) throws Exception {
        System.out.println(MessageFormat.format("\n\n========================================\nPUBSUB TEST {0}\n========================================\n\n", service.toString()));
        service.startSynchronously();
        final Map<String, String> announcementsA = new ConcurrentHashMap<String, String>();
        FanoutClient clientA = new FanoutClient("localhost", fanoutPort);
        clientA.addListener(new FanoutAdapter() {
            @Override
            public void announcement(String channel, String message) {
                announcementsA.put(channel, message);
            }
        });
        clientA.startSynchronously();
        final Map<String, String> announcementsB = new ConcurrentHashMap<String, String>();
        FanoutClient clientB = new FanoutClient("localhost", fanoutPort);
        clientB.addListener(new FanoutAdapter() {
            @Override
            public void announcement(String channel, String message) {
                announcementsB.put(channel, message);
            }
        });
        clientB.startSynchronously();
        // subscribe clients A and B to the channels
        clientA.subscribe("a");
        clientA.subscribe("b");
        clientA.subscribe("c");
        clientB.subscribe("a");
        clientB.subscribe("b");
        clientB.subscribe("c");
        // give async messages a chance to be delivered
        Thread.sleep(1000);
        clientA.announce("a", "apple");
        clientA.announce("b", "banana");
        clientA.announce("c", "cantelope");
        clientB.announce("a", "avocado");
        clientB.announce("b", "beet");
        clientB.announce("c", "carrot");
        // give async messages a chance to be delivered
        Thread.sleep(2000);
        // confirm that client B received client A's announcements
        assertEquals("apple", announcementsB.get("a"));
        assertEquals("banana", announcementsB.get("b"));
        assertEquals("cantelope", announcementsB.get("c"));
        // confirm that client A received client B's announcements
        assertEquals("avocado", announcementsA.get("a"));
        assertEquals("beet", announcementsA.get("b"));
        assertEquals("carrot", announcementsA.get("c"));
        clientA.stop();
        clientB.stop();
        service.stop();
    }
    protected void testDisruption(FanoutService service) throws Exception  {
        System.out.println(MessageFormat.format("\n\n========================================\nDISRUPTION TEST {0}\n========================================\n\n", service.toString()));
        service.startSynchronously();
        final AtomicInteger pongCount = new AtomicInteger(0);
        FanoutClient client = new FanoutClient("localhost", fanoutPort);
        client.addListener(new FanoutAdapter() {
            @Override
            public void pong(Date timestamp) {
                pongCount.incrementAndGet();
            }
        });
        client.startSynchronously();
        // ping and wait for pong
        client.ping();
        Thread.sleep(500);
        // restart client
        client.stop();
        Thread.sleep(1000);
        client.startSynchronously();
        // ping and wait for pong
        client.ping();
        Thread.sleep(500);
        assertEquals(2, pongCount.get());
        // now disrupt service
        service.stop();
        Thread.sleep(2000);
        service.startSynchronously();
        // wait for reconnect
        Thread.sleep(2000);
        // ping and wait for pong
        client.ping();
        Thread.sleep(500);
        // kill all
        client.stop();
        service.stop();
        // confirm expected pong count
        assertEquals(3, pongCount.get());
    }
}
tests/com/gitblit/tests/FederationTests.java
@@ -72,7 +72,7 @@
            model.accessRestriction = AccessRestrictionType.VIEW;
            model.description = "cloneable repository " + i;
            model.lastChange = new Date();
            model.owner = "adminuser";
            model.addOwner("adminuser");
            model.name = "repo" + i + ".git";
            model.size = "5 MB";
            model.hasCommits = true;
tests/com/gitblit/tests/GitBlitSuite.java
@@ -59,10 +59,11 @@
        MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class,
        DiffUtilsTest.class, MetricUtilsTest.class, TicgitUtilsTest.class, X509UtilsTest.class,
        GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class,
        GroovyScriptTest.class, LuceneExecutorTest.class, IssuesTest.class, RepositoryModelTest.class })
        GroovyScriptTest.class, LuceneExecutorTest.class, IssuesTest.class, RepositoryModelTest.class,
        FanoutServiceTest.class })
public class GitBlitSuite {
    public static final File REPOSITORIES = new File("git");
    public static final File REPOSITORIES = new File("data/git");
    static int port = 8280;
    static int shutdownPort = 8281;
@@ -116,7 +117,8 @@
                GitBlitServer.main("--httpPort", "" + port, "--httpsPort", "0", "--shutdownPort",
                        "" + shutdownPort, "--repositoriesFolder",
                        "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService",
                        "test-users.conf", "--settings", "test-gitblit.properties");
                        "test-users.conf", "--settings", "test-gitblit.properties",
                        "--baseFolder", "data");
            }
        });
tests/com/gitblit/tests/GitBlitTest.java
@@ -138,7 +138,7 @@
        assertEquals(5, settings.getInteger("realm.realmFile", 5));
        assertTrue(settings.getBoolean("git.enableGitServlet", false));
        assertEquals("users.conf", settings.getString("realm.userService", null));
        assertEquals("${baseFolder}/users.conf", settings.getString("realm.userService", null));
        assertEquals(5, settings.getInteger("realm.minPasswordLength", 0));
        List<String> mdExtensions = settings.getStrings("web.markdownExtensions");
        assertTrue(mdExtensions.size() > 0);
tests/com/gitblit/tests/GitServletTest.java
@@ -7,9 +7,11 @@
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.api.CloneCommand;
@@ -18,6 +20,7 @@
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileRepository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
@@ -34,9 +37,12 @@
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.PushLogEntry;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.PushLogUtils;
public class GitServletTest {
@@ -88,6 +94,11 @@
    @Test
    public void testClone() throws Exception {
        GitBlitSuite.close(ticgitFolder);
        if (ticgitFolder.exists()) {
            FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
        }
        CloneCommand clone = Git.cloneRepository();
        clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url));
        clone.setDirectory(ticgitFolder);
@@ -187,6 +198,20 @@
    @Test
    public void testAnonymousPush() throws Exception {
        GitBlitSuite.close(ticgitFolder);
        if (ticgitFolder.exists()) {
            FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
        }
        CloneCommand clone = Git.cloneRepository();
        clone.setURI(MessageFormat.format("{0}/git/ticgit.git", url));
        clone.setDirectory(ticgitFolder);
        clone.setBare(false);
        clone.setCloneAllBranches(true);
        clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(account, password));
        GitBlitSuite.close(clone.call());
        assertTrue(true);
        Git git = Git.open(ticgitFolder);
        File file = new File(ticgitFolder, "TODO");
        OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
@@ -201,6 +226,11 @@
    @Test
    public void testSubfolderPush() throws Exception {
        GitBlitSuite.close(jgitFolder);
        if (jgitFolder.exists()) {
            FileUtils.delete(jgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
        }
        CloneCommand clone = Git.cloneRepository();
        clone.setURI(MessageFormat.format("{0}/git/test/jgit.git", url));
        clone.setDirectory(jgitFolder);
@@ -218,6 +248,51 @@
        w.close();
        git.add().addFilepattern(file.getName()).call();
        git.commit().setMessage("test commit").call();
        git.push().setPushAll().call();
        GitBlitSuite.close(git);
    }
    @Test
    public void testPushToFrozenRepo() throws Exception {
        GitBlitSuite.close(jgitFolder);
        if (jgitFolder.exists()) {
            FileUtils.delete(jgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
        }
        CloneCommand clone = Git.cloneRepository();
        clone.setURI(MessageFormat.format("{0}/git/test/jgit.git", url));
        clone.setDirectory(jgitFolder);
        clone.setBare(false);
        clone.setCloneAllBranches(true);
        clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(account, password));
        GitBlitSuite.close(clone.call());
        assertTrue(true);
        // freeze repo
        RepositoryModel model = GitBlit.self().getRepositoryModel("test/jgit.git");
        model.isFrozen = true;
        GitBlit.self().updateRepositoryModel(model.name, model, false);
        Git git = Git.open(jgitFolder);
        File file = new File(jgitFolder, "TODO");
        OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
        BufferedWriter w = new BufferedWriter(os);
        w.write("// " + new Date().toString() + "\n");
        w.close();
        git.add().addFilepattern(file.getName()).call();
        git.commit().setMessage("test commit").call();
        try {
            git.push().setPushAll().call();
            assertTrue(false);
        } catch (Exception e) {
            assertTrue(e.getCause().getMessage().contains("access forbidden"));
        }
        // unfreeze repo
        model.isFrozen = false;
        GitBlit.self().updateRepositoryModel(model.name, model, false);
        git.push().setPushAll().call();
        GitBlitSuite.close(git);
    }
@@ -651,7 +726,7 @@
            
            // confirm default personal repository permissions
            RepositoryModel model = GitBlit.self().getRepositoryModel(MessageFormat.format("~{0}/ticgit.git", user.username));
            assertEquals("Unexpected owner", user.username, model.owner);
            assertEquals("Unexpected owner", user.username, ArrayUtils.toString(model.owners));
            assertEquals("Unexpected authorization control", AuthorizationControl.NAMED, model.authorizationControl);
            assertEquals("Unexpected access restriction", AccessRestrictionType.VIEW, model.accessRestriction);
            
@@ -675,7 +750,7 @@
            
            // confirm default project repository permissions
            RepositoryModel model = GitBlit.self().getRepositoryModel("project/ticgit.git");
            assertEquals("Unexpected owner", user.username, model.owner);
            assertEquals("Unexpected owner", user.username, ArrayUtils.toString(model.owners));
            assertEquals("Unexpected authorization control", AuthorizationControl.fromName(GitBlit.getString(Keys.git.defaultAuthorizationControl, "NAMED")), model.authorizationControl);
            assertEquals("Unexpected access restriction", AccessRestrictionType.fromName(GitBlit.getString(Keys.git.defaultAccessRestriction, "NONE")), model.accessRestriction);
@@ -687,4 +762,14 @@
        GitBlitSuite.close(git);
        GitBlit.self().deleteUser(user.username);
    }
    @Test
    public void testPushLog() throws IOException {
        String name = "refchecks/ticgit.git";
        File refChecks = new File(GitBlitSuite.REPOSITORIES, name);
        FileRepository repository = new FileRepository(refChecks);
        List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
        GitBlitSuite.close(repository);
        assertTrue("Repository has an empty push log!", pushes.size() > 0);
    }
}
tests/com/gitblit/tests/GroovyScriptTest.java
@@ -30,6 +30,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -70,6 +71,28 @@
    }
    @Test
    public void testFogbugz() throws Exception {
        MockGitblit gitblit = new MockGitblit();
        MockLogger logger = new MockLogger();
        MockClientLogger clientLogger = new MockClientLogger();
        List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
        commands.add(new ReceiveCommand(ObjectId
                .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
                .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
        commands.add(new ReceiveCommand(ObjectId
                .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
                .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master2"));
        RepositoryModel repository = GitBlit.self().getRepositoryModel("helloworld.git");
        repository.customFields = new HashMap<String,String>();
        repository.customFields.put( "fogbugzUrl", "http://bugs.test.com" );
        repository.customFields.put( "fogbugzRepositoryId", "1" );
        repository.customFields.put( "fogbugzCommitMessageRegex", "\\s*[Bb][Uu][Gg][(Zz)(Ss)]*\\s*[(IDs)]*\\s*[#:; ]+((\\d+[ ,:;#]*)+)" );
        test("fogbugz.groovy", gitblit, logger, clientLogger, commands, repository);
    }
    @Test
    public void testSendHtmlMail() throws Exception {
        MockGitblit gitblit = new MockGitblit();
        MockLogger logger = new MockLogger();
tests/com/gitblit/tests/LdapUserServiceTest.java
@@ -31,6 +31,7 @@
import com.gitblit.LdapUserService;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.StringUtils;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -154,5 +155,20 @@
        UserModel userOneModel = ldapUserService.authenticate("*)(userPassword=userOnePassword", "userOnePassword".toCharArray());
        assertNull(userOneModel);
    }
    @Test
    public void testLocalAccount() {
        UserModel localAccount = new UserModel("bruce");
        localAccount.displayName = "Bruce Campbell";
        localAccount.password = StringUtils.MD5_TYPE + StringUtils.getMD5("gimmesomesugar");
        ldapUserService.deleteUser(localAccount.username);
        assertTrue("Failed to add local account",
                ldapUserService.updateUserModel(localAccount));
        assertEquals("Accounts are not equal!",
                localAccount,
                ldapUserService.authenticate(localAccount.username, "gimmesomesugar".toCharArray()));
        assertTrue("Failed to delete local account!",
                ldapUserService.deleteUser(localAccount.username));
    }
}
tests/com/gitblit/tests/PermissionsTest.java
@@ -2327,7 +2327,7 @@
        repository.accessRestriction = AccessRestrictionType.VIEW;
        UserModel user = new UserModel("test");
        repository.owner = user.username;
        repository.addOwner(user.username);
        assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
        assertTrue("owner CAN NOT view!", user.canView(repository));
@@ -2345,13 +2345,58 @@
    }
    
    @Test
    public void testMultipleOwners() throws Exception {
        RepositoryModel repository = new RepositoryModel("myrepo.git", null, null, new Date());
        repository.authorizationControl = AuthorizationControl.NAMED;
        repository.accessRestriction = AccessRestrictionType.VIEW;
        UserModel user = new UserModel("test");
        repository.addOwner(user.username);
        UserModel user2 = new UserModel("test2");
        repository.addOwner(user2.username);
        // first owner
        assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
        assertTrue("owner CAN NOT view!", user.canView(repository));
        assertTrue("owner CAN NOT clone!", user.canClone(repository));
        assertTrue("owner CAN NOT push!", user.canPush(repository));
        assertTrue("owner CAN NOT create ref!", user.canCreateRef(repository));
        assertTrue("owner CAN NOT delete ref!", user.canDeleteRef(repository));
        assertTrue("owner CAN NOT rewind ref!", user.canRewindRef(repository));
        assertTrue("owner CAN NOT fork!", user.canFork(repository));
        assertFalse("owner CAN NOT delete!", user.canDelete(repository));
        assertTrue("owner CAN NOT edit!", user.canEdit(repository));
        // second owner
        assertFalse("user SHOULD NOT HAVE a repository permission!", user2.hasRepositoryPermission(repository.name));
        assertTrue("owner CAN NOT view!", user2.canView(repository));
        assertTrue("owner CAN NOT clone!", user2.canClone(repository));
        assertTrue("owner CAN NOT push!", user2.canPush(repository));
        assertTrue("owner CAN NOT create ref!", user2.canCreateRef(repository));
        assertTrue("owner CAN NOT delete ref!", user2.canDeleteRef(repository));
        assertTrue("owner CAN NOT rewind ref!", user2.canRewindRef(repository));
        assertTrue("owner CAN NOT fork!", user2.canFork(repository));
        assertFalse("owner CAN NOT delete!", user2.canDelete(repository));
        assertTrue("owner CAN NOT edit!", user2.canEdit(repository));
        assertTrue(repository.isOwner(user.username));
        assertTrue(repository.isOwner(user2.username));
    }
    @Test
    public void testOwnerPersonalRepository() throws Exception {
        RepositoryModel repository = new RepositoryModel("~test/myrepo.git", null, null, new Date());
        repository.authorizationControl = AuthorizationControl.NAMED;
        repository.accessRestriction = AccessRestrictionType.VIEW;
        UserModel user = new UserModel("test");
        repository.owner = user.username;
        repository.addOwner(user.username);
        assertFalse("user SHOULD NOT HAVE a repository permission!", user.hasRepositoryPermission(repository.name));
        assertTrue("user CAN NOT view!", user.canView(repository));
@@ -2375,7 +2420,7 @@
        repository.accessRestriction = AccessRestrictionType.VIEW;
        UserModel user = new UserModel("visitor");
        repository.owner = "test";
        repository.addOwner("test");
        assertFalse("user HAS a repository permission!", user.hasRepositoryPermission(repository.name));
        assertFalse("user CAN view!", user.canView(repository));
tests/com/gitblit/tests/PushLogTest.java
New file
@@ -0,0 +1,37 @@
/*
 * Copyright 2013 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.tests;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.storage.file.FileRepository;
import org.junit.Test;
import com.gitblit.models.PushLogEntry;
import com.gitblit.utils.PushLogUtils;
public class PushLogTest {
    @Test
    public void testPushLog() throws IOException {
        String name = "~james/helloworld.git";
        FileRepository repository = new FileRepository(new File(GitBlitSuite.REPOSITORIES, name));
        List<PushLogEntry> pushes = PushLogUtils.getPushLog(name, repository);
        GitBlitSuite.close(repository);
    }
}
tests/com/gitblit/tests/RedmineUserServiceTest.java
@@ -1,9 +1,10 @@
package com.gitblit.tests;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
@@ -12,6 +13,7 @@
import com.gitblit.RedmineUserService;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.StringUtils;
public class RedmineUserServiceTest {
@@ -28,8 +30,8 @@
        RedmineUserService redmineUserService = new RedmineUserService();
        redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));
        redmineUserService.setTestingCurrentUserAsJson(JSON);
        UserModel userModel = redmineUserService.authenticate("RedmineUserId", "RedmineAPIKey".toCharArray());
        assertThat(userModel.getName(), is("RedmineUserId"));
        UserModel userModel = redmineUserService.authenticate("RedmineAdminId", "RedmineAPIKey".toCharArray());
        assertThat(userModel.getName(), is("redmineadminid"));
        assertThat(userModel.getDisplayName(), is("baz foo"));
        assertThat(userModel.emailAddress, is("baz@example.com"));
        assertNotNull(userModel.cookie);
@@ -42,11 +44,29 @@
        redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));
        redmineUserService.setTestingCurrentUserAsJson(NOT_ADMIN_JSON);
        UserModel userModel = redmineUserService.authenticate("RedmineUserId", "RedmineAPIKey".toCharArray());
        assertThat(userModel.getName(), is("baz@example.com"));
        assertThat(userModel.getName(), is("redmineuserid"));
        assertThat(userModel.getDisplayName(), is("baz foo"));
        assertThat(userModel.emailAddress, is("baz@example.com"));
        assertNotNull(userModel.cookie);
        assertThat(userModel.canAdmin, is(false));
    }
    @Test
    public void testLocalAccount() {
        RedmineUserService redmineUserService = new RedmineUserService();
        redmineUserService.setup(new MemorySettings(new HashMap<String, Object>()));
        UserModel localAccount = new UserModel("bruce");
        localAccount.displayName = "Bruce Campbell";
        localAccount.password = StringUtils.MD5_TYPE + StringUtils.getMD5("gimmesomesugar");
        redmineUserService.deleteUser(localAccount.username);
        assertTrue("Failed to add local account",
                redmineUserService.updateUserModel(localAccount));
        assertEquals("Accounts are not equal!",
                localAccount,
                redmineUserService.authenticate(localAccount.username, "gimmesomesugar".toCharArray()));
        assertTrue("Failed to delete local account!",
                redmineUserService.deleteUser(localAccount.username));
    }
}
tests/com/gitblit/tests/RpcTests.java
@@ -167,7 +167,7 @@
        RepositoryModel model = new RepositoryModel();
        model.name = "garbagerepo.git";
        model.description = "created by RpcUtils";
        model.owner = "garbage";
        model.addOwner("garbage");
        model.accessRestriction = AccessRestrictionType.VIEW;
        model.authorizationControl = AuthorizationControl.AUTHENTICATED;
tests/de/akquinet/devops/GitblitRunnable.java
New file
@@ -0,0 +1,134 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops;
import java.net.InetAddress;
import java.net.ServerSocket;
import com.gitblit.GitBlitServer;
import com.gitblit.tests.GitBlitSuite;
/**
 * This is a runnable implementation, that is used to run a gitblit server in a
 * separate thread (e.g. alongside test cases)
 *
 * @author saheba
 *
 */
public class GitblitRunnable implements Runnable {
    private int httpPort, httpsPort, shutdownPort;
    private String userPropertiesPath, gitblitPropertiesPath;
    private boolean startFailed = false;
    /**
     * constructor with reduced set of start params
     *
     * @param httpPort
     * @param httpsPort
     * @param shutdownPort
     * @param gitblitPropertiesPath
     * @param userPropertiesPath
     */
    public GitblitRunnable(int httpPort, int httpsPort, int shutdownPort,
            String gitblitPropertiesPath, String userPropertiesPath) {
        this.httpPort = httpPort;
        this.httpsPort = httpsPort;
        this.shutdownPort = shutdownPort;
        this.userPropertiesPath = userPropertiesPath;
        this.gitblitPropertiesPath = gitblitPropertiesPath;
    }
    /*
     * (non-Javadoc)
     *
     * @see java.lang.Runnable#run()
     */
    public void run() {
        boolean portsFree = false;
        long lastRun = -1;
        while (!portsFree) {
            long current = System.currentTimeMillis();
            if (lastRun == -1 || lastRun + 100 < current) {
                portsFree = areAllPortsFree(new int[] { httpPort, httpsPort,
                        shutdownPort }, "127.0.0.1");
            }
            lastRun = current;
        }
        try {
            GitBlitServer.main("--httpPort", "" + httpPort, "--httpsPort", ""
                    + httpsPort, "--shutdownPort", "" + shutdownPort,
                    "--repositoriesFolder",
                    "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"",
                    "--userService", userPropertiesPath, "--settings",
                    gitblitPropertiesPath);
            setStartFailed(false);
        } catch (Exception iex) {
            System.out.println("Gitblit server start failed");
            setStartFailed(true);
        }
    }
    /**
     * Method used to ensure that all ports are free, if the runnable is used
     * JUnit test classes. Be aware that JUnit's setUpClass and tearDownClass
     * methods, which are executed before and after a test class (consisting of
     * several test cases), may be executed parallely if they are part of a test
     * suite consisting of several test classes. Therefore the run method of
     * this class calls areAllPortsFree to check port availability before
     * starting another gitblit instance.
     *
     * @param ports
     * @param inetAddress
     * @return
     */
    public static boolean areAllPortsFree(int[] ports, String inetAddress) {
        System.out
                .println("\n"
                        + System.currentTimeMillis()
                        + " ----------------------------------- testing if all ports are free ...");
        String blockedPorts = "";
        for (int i = 0; i < ports.length; i++) {
            ServerSocket s;
            try {
                s = new ServerSocket(ports[i], 1,
                        InetAddress.getByName(inetAddress));
                s.close();
            } catch (Exception e) {
                if (!blockedPorts.equals("")) {
                    blockedPorts += ", ";
                }
            }
        }
        if (blockedPorts.equals("")) {
            System.out
                    .println(" ----------------------------------- ... verified");
            return true;
        }
        System.out.println(" ----------------------------------- ... "
                + blockedPorts + " are still blocked");
        return false;
    }
    private void setStartFailed(boolean startFailed) {
        this.startFailed = startFailed;
    }
    public boolean isStartFailed() {
        return startFailed;
    }
}
tests/de/akquinet/devops/LaunchWithUITestConfig.java
New file
@@ -0,0 +1,126 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import junit.framework.Assert;
import org.junit.Test;
import com.gitblit.Constants;
import com.gitblit.GitBlitServer;
import com.gitblit.tests.GitBlitSuite;
/**
 * This test checks if it is possible to run two server instances in the same
 * JVM sequentially
 *
 * @author saheba
 *
 */
public class LaunchWithUITestConfig {
    @Test
    public void testSequentialLaunchOfSeveralInstances()
            throws InterruptedException {
        // different ports than in testParallelLaunchOfSeveralInstances to
        // ensure that both test cases do not affect each others test results
        int httpPort = 9191, httpsPort = 9292, shutdownPort = 9393;
        String gitblitPropertiesPath = "test-ui-gitblit.properties", usersPropertiesPath = "test-ui-users.conf";
        GitblitRunnable gitblitRunnable = new GitblitRunnable(httpPort,
                httpsPort, shutdownPort, gitblitPropertiesPath,
                usersPropertiesPath);
        Thread serverThread = new Thread(gitblitRunnable);
        serverThread.start();
        Thread.sleep(2000);
        Assert.assertFalse(gitblitRunnable.isStartFailed());
        LaunchWithUITestConfig.shutdownGitBlitServer(shutdownPort);
        Thread.sleep(5000);
        GitblitRunnable gitblitRunnable2 = new GitblitRunnable(httpPort,
                httpsPort, shutdownPort, gitblitPropertiesPath,
                usersPropertiesPath);
        Thread serverThread2 = new Thread(gitblitRunnable2);
        serverThread2.start();
        Thread.sleep(2000);
        Assert.assertFalse(gitblitRunnable2.isStartFailed());
        LaunchWithUITestConfig.shutdownGitBlitServer(shutdownPort);
    }
    @Test
    public void testParallelLaunchOfSeveralInstances()
            throws InterruptedException {
        // different ports than in testSequentialLaunchOfSeveralInstances to
        // ensure that both test cases do not affect each others test results
        int httpPort = 9797, httpsPort = 9898, shutdownPort = 9999;
        int httpPort2 = 9494, httpsPort2 = 9595, shutdownPort2 = 9696;
        String gitblitPropertiesPath = "test-ui-gitblit.properties", usersPropertiesPath = "test-ui-users.conf";
        GitblitRunnable gitblitRunnable = new GitblitRunnable(httpPort,
                httpsPort, shutdownPort, gitblitPropertiesPath,
                usersPropertiesPath);
        Thread serverThread = new Thread(gitblitRunnable);
        serverThread.start();
        Thread.sleep(2000);
        Assert.assertFalse(gitblitRunnable.isStartFailed());
        GitblitRunnable gitblitRunnable2 = new GitblitRunnable(httpPort2,
                httpsPort2, shutdownPort2, gitblitPropertiesPath,
                usersPropertiesPath);
        Thread serverThread2 = new Thread(gitblitRunnable2);
        serverThread2.start();
        Thread.sleep(2000);
        Assert.assertFalse(gitblitRunnable2.isStartFailed());
        LaunchWithUITestConfig.shutdownGitBlitServer(shutdownPort);
        LaunchWithUITestConfig.shutdownGitBlitServer(shutdownPort2);
    }
    /**
     * main runs the tests without assert checks. You have to check the console
     * output manually.
     *
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        new LaunchWithUITestConfig().testSequentialLaunchOfSeveralInstances();
        new LaunchWithUITestConfig().testParallelLaunchOfSeveralInstances();
    }
    private static void shutdownGitBlitServer(int shutdownPort) {
        try {
            Socket s = new Socket(InetAddress.getByName("127.0.0.1"),
                    shutdownPort);
            OutputStream out = s.getOutputStream();
            System.out.println("Sending Shutdown Request to " + Constants.NAME);
            out.write("\r\n".getBytes());
            out.flush();
            s.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
tests/de/akquinet/devops/ManualUITestLaunch.java
New file
@@ -0,0 +1,14 @@
package de.akquinet.devops;
public class ManualUITestLaunch {
public static void main(String[] args) {
    int httpPort = 8080, httpsPort = 8443, shutdownPort = 8081;
    String gitblitPropertiesPath = "test-ui-gitblit.properties", usersPropertiesPath = "test-ui-users.conf";
    GitblitRunnable gitblitRunnable = new GitblitRunnable(httpPort,
            httpsPort, shutdownPort, gitblitPropertiesPath,
            usersPropertiesPath);
    Thread serverThread = new Thread(gitblitRunnable);
    serverThread.start();
}
}
tests/de/akquinet/devops/test/ui/TestUISuite.java
New file
@@ -0,0 +1,33 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import de.akquinet.devops.test.ui.cases.UI_MultiAdminSupportTest;
/**
 * the test suite including all selenium-based ui-tests.
 *
 * @author saheba
 *
 */
@RunWith(Suite.class)
@Suite.SuiteClasses({ UI_MultiAdminSupportTest.class })
public class TestUISuite {
}
tests/de/akquinet/devops/test/ui/cases/UI_MultiAdminSupportTest.java
New file
@@ -0,0 +1,93 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.cases;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import de.akquinet.devops.test.ui.generic.AbstractUITest;
import de.akquinet.devops.test.ui.view.RepoEditView;
import de.akquinet.devops.test.ui.view.RepoListView;
/**
 * tests the multi admin per repo feature.
 *
 * @author saheba
 *
 */
public class UI_MultiAdminSupportTest extends AbstractUITest {
    String baseUrl = "https://localhost:8443";
    RepoListView view;
    RepoEditView editView;
    private static final String TEST_MULTI_ADMIN_SUPPORT_REPO_NAME = "testmultiadminsupport";
    private static final String TEST_MULTI_ADMIN_SUPPORT_REPO_PATH = "~repocreator/"
            + TEST_MULTI_ADMIN_SUPPORT_REPO_NAME + ".git";
    private static final String TEST_MULTI_ADMIN_SUPPORT_REPO_PATH_WITHOUT_SUFFIX = "~repocreator/"
            + TEST_MULTI_ADMIN_SUPPORT_REPO_NAME;
    @Before
    public void before() {
        System.out.println("IN BEFORE");
        this.view = new RepoListView(AbstractUITest.getDriver(), baseUrl);
        this.editView = new RepoEditView(AbstractUITest.getDriver());
        AbstractUITest.getDriver().navigate().to(baseUrl);
    }
    @Test
    public void test_MultiAdminSelectionInStandardRepo() {
        // login
        view.login("repocreator", "repocreator");
        // create new repo
        view.navigateToNewRepo(1);
        editView.changeName(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH);
        Assert.assertTrue(editView.navigateToPermissionsTab());
        Assert.assertTrue(editView
                .changeAccessRestriction(RepoEditView.RESTRICTION_AUTHENTICATED_VCP));
        Assert.assertTrue(editView
                .changeAuthorizationControl(RepoEditView.AUTHCONTROL_RWALL));
        // with a second admin
        editView.addRepoAdministrator("admin");
        Assert.assertTrue(editView.save());
        // user is automatically forwarded to repo list view
        Assert.assertTrue(view.isEmptyRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH));
        Assert.assertTrue(view
                .isEditableRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH));
        Assert.assertTrue(view
                .isDeletableRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH_WITHOUT_SUFFIX));
        // logout repocreator
        view.logout();
        // check with admin account if second admin has the same rights
        view.login("admin", "admin");
        Assert.assertTrue(view.isEmptyRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH));
        Assert.assertTrue(view
                .isEditableRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH));
        Assert.assertTrue(view
                .isDeletableRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH_WITHOUT_SUFFIX));
        // delete repo to reach state as before test execution
        view.navigateToDeleteRepo(TEST_MULTI_ADMIN_SUPPORT_REPO_PATH_WITHOUT_SUFFIX);
        view.acceptAlertDialog();
        view.logout();
        Assert.assertTrue(view.isLoginPartVisible());
    }
}
tests/de/akquinet/devops/test/ui/generic/AbstractUITest.java
New file
@@ -0,0 +1,96 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.generic;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import com.gitblit.GitBlitServer;
import de.akquinet.devops.GitblitRunnable;
/**
 * This abstract class implements the setUpClass and tearDownClass for
 * selenium-based UITests. They require a running gitblit server instance and a
 * webdriver instance, which are managed by the setUpClass and tearDownClass
 * method. Write a separate test class derived from this abstract class for each
 * scenario consisting of one or more test cases, which can share the same
 * server instance.
 *
 * @author saheba
 *
 */
public abstract class AbstractUITest {
    private static Thread serverThread;
    private static WebDriver driver;
    private static final int HTTP_PORT = 8080, HTTPS_PORT = 8443,
            SHUTDOWN_PORT = 8081;
    private static final String GITBLIT_PROPERTIES_PATH = "test-ui-gitblit.properties",
            USERS_PROPERTIES_PATH = "test-ui-users.conf";
    /**
     * starts a gitblit server instance in a separate thread before test cases
     * of concrete, non-abstract child-classes are executed
     */
    @BeforeClass
    public static void setUpClass() {
        Runnable gitblitRunnable = new GitblitRunnable(HTTP_PORT, HTTPS_PORT,
                SHUTDOWN_PORT, GITBLIT_PROPERTIES_PATH, USERS_PROPERTIES_PATH);
        serverThread = new Thread(gitblitRunnable);
        serverThread.start();
        FirefoxProfile firefoxProfile = new FirefoxProfile();
        firefoxProfile.setPreference("startup.homepage_welcome_url",
                "https://www.google.de");
        firefoxProfile.setPreference("browser.download.folderList", 2);
        firefoxProfile.setPreference(
                "browser.download.manager.showWhenStarting", false);
        String downloadDir = System.getProperty("java.io.tmpdir");
        firefoxProfile.setPreference("browser.download.dir", downloadDir);
        firefoxProfile.setPreference("browser.helperApps.neverAsk.saveToDisk",
                "text/csv,text/plain,application/zip,application/pdf");
        firefoxProfile.setPreference("browser.helperApps.alwaysAsk.force",
                false);
        System.out.println("Saving all attachments to: " + downloadDir);
        driver = new FirefoxDriver(firefoxProfile);
    }
    /**
     * stops the gitblit server instance running in a separate thread after test
     * cases of concrete, non-abstract child-classes have been executed
     */
    @AfterClass
    public static void tearDownClass() throws InterruptedException {
        driver.close();
        // Stop Gitblit
        GitBlitServer.main("--stop", "--shutdownPort", "" + SHUTDOWN_PORT);
        // Wait a few seconds for it to be running completely including thread
        // destruction
        Thread.sleep(1000);
    }
    public static WebDriver getDriver() {
        return AbstractUITest.driver;
    }
}
tests/de/akquinet/devops/test/ui/view/Exp.java
New file
@@ -0,0 +1,45 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.view;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
/**
 * container class for selenium conditions
 *
 * @author saheba
 *
 */
public class Exp {
    public static class EditRepoViewLoaded implements ExpectedCondition<Boolean> {
        public Boolean apply(WebDriver d) {
            List<WebElement> findElements = d.findElements(By.partialLinkText("general"));
            return findElements.size() == 1;
        }
    }
    public static class RepoListViewLoaded implements ExpectedCondition<Boolean> {
        public Boolean apply(WebDriver d) {
            String xpath = "//img[@src=\"git-black-16x16.png\"]";
            List<WebElement> findElements = d.findElements(By.xpath(xpath ));
            return findElements.size() == 1;
        }
    }
}
tests/de/akquinet/devops/test/ui/view/GitblitDashboardView.java
New file
@@ -0,0 +1,100 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.view;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
 * class representing the view componenents and possible user interactions, you
 * can see and do on most screens when you are logged in.
 *
 * @author saheba
 *
 */
public class GitblitDashboardView extends GitblitPageView {
    public static final String TITLE_STARTS_WITH = "localhost";
    public GitblitDashboardView(WebDriver driver, String baseUrl) {
        super(driver, baseUrl);
    }
    public boolean isLoginPartVisible() {
        List<WebElement> found = getDriver().findElements(
                By.partialLinkText("logout"));
        return found == null || found.size() == 0;
    }
    public void logout() {
        // String pathLogout = "//a[@href =\"?" + WICKET_HREF_PAGE_PATH
        // + ".LogoutPage\"]";
        // List<WebElement> logout =
        // getDriver().findElements(By.xpath(pathLogout));
        // logout.get(0).click();
        // replaced by url call because click hangs sometimes if the clicked
        // object is not a button or selenium ff driver does not notice the
        // change for any other reason
        getDriver().navigate().to(
                getBaseUrl() + "?" + WICKET_HREF_PAGE_PATH + ".LogoutPage");
    }
    public static final String LOGIN_AREA_SELECTOR = "//span[@class = \"form-search\" ]";
    public static final String WICKET_PAGES_PACKAGE_NAME = "com.gitblit.wicket.pages";
    public static final String WICKET_HREF_PAGE_PATH = "wicket:bookmarkablePage=:"
            + WICKET_PAGES_PACKAGE_NAME;
    synchronized public void waitToLoadFor(int sec) {
        WebDriverWait webDriverWait = new WebDriverWait(getDriver(), sec);
        webDriverWait.until(new ExpectedCondition<Boolean>() {
            public Boolean apply(WebDriver d) {
                return d.getTitle().toLowerCase()
                        .startsWith(GitblitDashboardView.TITLE_STARTS_WITH);
            }
        });
    }
    public void login(String id, String pw) {
        String pathID = LOGIN_AREA_SELECTOR + "/input[@name = \"username\" ]";
        String pathPW = LOGIN_AREA_SELECTOR + "/input[@name = \"password\" ]";
        String pathSubmit = LOGIN_AREA_SELECTOR
                + "/button[@type = \"submit\" ]";
        // System.out.println("DRIVER:"+getDriver());
        // List<WebElement> findElement =
        // getDriver().findElements(By.xpath("//span[@class = \"form-search\" ]"));
        //
        // System.out.println("ELEM: "+findElement);
        // System.out.println("SIZE: "+findElement.size());
        // System.out.println("XPath: "+pathID);
        WebElement idField = getDriver().findElement(By.xpath(pathID));
        // System.out.println("IDFIELD:"+idField);
        idField.sendKeys(id);
        WebElement pwField = getDriver().findElement(By.xpath(pathPW));
        // System.out.println(pwField);
        pwField.sendKeys(pw);
        WebElement submit = getDriver().findElement(By.xpath(pathSubmit));
        submit.click();
    }
    public void acceptAlertDialog() {
        getDriver().switchTo().alert().accept();
    }
}
tests/de/akquinet/devops/test/ui/view/GitblitPageView.java
New file
@@ -0,0 +1,73 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.view;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
/**
 * general basic class representing a gitblit webpage and offering basic methods
 * used in selenium tests.
 *
 * @author saheba
 *
 */
public class GitblitPageView {
    private WebDriver driver;
    private String baseUrl;
    public GitblitPageView(WebDriver driver, String baseUrl) {
        this.driver = driver;
        this.baseUrl = baseUrl;
    }
    public void sleep(int miliseconds) {
        try {
            Thread.sleep(miliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public WebElement getElementWithFocus() {
        String elScript = "return document.activeElement;";
        WebElement focuseedEl = (WebElement) ((JavascriptExecutor) getDriver())
                .executeScript(elScript);
        return focuseedEl;
    }
    public void navigateToPreviousPageOfBrowserHistory() {
        driver.navigate().back();
    }
    public void setDriver(WebDriver driver) {
        this.driver = driver;
    }
    public WebDriver getDriver() {
        return driver;
    }
    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    public String getBaseUrl() {
        return baseUrl;
    }
}
tests/de/akquinet/devops/test/ui/view/RepoEditView.java
New file
@@ -0,0 +1,158 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.view;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
 * class representing the tabs you can access when you edit a repo.
 *
 * @author saheba
 *
 */
public class RepoEditView extends GitblitDashboardView {
    public static final String PERMISSION_VIEW_USERS_NAME_PREFIX = "users:";
    public static final String PERMISSION_VIEW_TEAMS_NAME_PREFIX = "teams:";
    public static final String PERMISSION_VIEW_MUTABLE = "permissionToggleForm:showMutable";
    public static final String PERMISSION_VIEW_SPECIFIED = "permissionToggleForm:showSpecified";
    public static final String PERMISSION_VIEW_EFFECTIVE = "permissionToggleForm:showEffective";
    public static final int RESTRICTION_ANONYMOUS_VCP = 0;
    public static final int RESTRICTION_AUTHENTICATED_P = 1;
    public static final int RESTRICTION_AUTHENTICATED_CP = 2;
    public static final int RESTRICTION_AUTHENTICATED_VCP = 3;
    public static final int AUTHCONTROL_RWALL = 0;
    public static final int AUTHOCONTROL_FINE = 1;
    public RepoEditView(WebDriver driver) {
        super(driver, null);
    }
    public void changeName(String newName) {
        String pathName = "//input[@id = \"name\" ]";
        WebElement field = getDriver().findElement(By.xpath(pathName));
        field.clear();
        field.sendKeys(newName);
    }
    public boolean navigateToPermissionsTab() {
        String linkText = "access permissions";
        List<WebElement> found = getDriver().findElements(
                By.partialLinkText(linkText));
        System.out.println("PERM TABS found =" + found.size());
        if (found != null && found.size() == 1) {
            found.get(0).click();
            return true;
        }
        return false;
    }
    private void changeRepoAdministrators(String action,
            String affectedSelection, String username) {
        String xpath = "//select[@name=\"" + affectedSelection
                + "\"]/option[@value = \"" + username + "\" ]";
        WebElement option = getDriver().findElement(By.xpath(xpath));
        option.click();
        String buttonPath = "//button[@class=\"button " + action + "\"]";
        WebElement button = getDriver().findElement(By.xpath(buttonPath));
        button.click();
    }
    public void removeRepoAdministrator(String username) {
        changeRepoAdministrators("remove", "repoAdministrators:selection",
                username);
    }
    public void addRepoAdministrator(String username) {
        changeRepoAdministrators("add", "repoAdministrators:choices", username);
    }
    public WebElement getAccessRestrictionSelection() {
        String xpath = "//select[@name =\"accessRestriction\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(xpath));
        if (found != null && found.size() == 1) {
            return found.get(0);
        }
        return null;
    }
    public boolean changeAccessRestriction(int option) {
        WebElement accessRestrictionSelection = getAccessRestrictionSelection();
        if (accessRestrictionSelection == null) {
            return false;
        }
        accessRestrictionSelection.click();
        sleep(100);
        String xpath = "//select[@name =\"accessRestriction\"]/option[@value=\""
                + option + "\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(xpath));
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        found.get(0).click();
        return true;
    }
    public boolean changeAuthorizationControl(int option) {
        System.out.println("try to change auth control");
        String xpath = "//input[@name =\"authorizationControl\" and @value=\""
                + option + "\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(xpath));
        System.out.println("found auth CONTROL options " + found.size());
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        found.get(0).click();
        return true;
    }
    private boolean isPermissionViewDisabled(String prefix, String view) {
        String xpath = "//[@name =\"" + prefix + view + "\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(xpath));
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        String attrValue = found.get(0).getAttribute("disabled");
        return (attrValue != null) && (attrValue.equals("disabled"));
    }
    public boolean isPermissionViewSectionDisabled(String prefix) {
        return isPermissionViewDisabled(prefix, PERMISSION_VIEW_MUTABLE)
                && isPermissionViewDisabled(prefix, PERMISSION_VIEW_SPECIFIED)
                && isPermissionViewDisabled(prefix, PERMISSION_VIEW_EFFECTIVE);
    }
    public boolean save() {
        String xpath = "//div[@class=\"form-actions\"]/input[@name =\""
                + "save" + "\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(xpath));
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        found.get(0).click();
        WebDriverWait webDriverWait = new WebDriverWait(getDriver(), 1);
        webDriverWait.until(new Exp.RepoListViewLoaded());
        return true;
    }
}
tests/de/akquinet/devops/test/ui/view/RepoListView.java
New file
@@ -0,0 +1,130 @@
/*
 * Copyright 2013 akquinet tech@spree GmbH
 *
 * 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 de.akquinet.devops.test.ui.view;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
 * class representing the repo list view, which you see e.g. right after you
 * logged in.
 *
 * @author saheba
 *
 */
public class RepoListView extends GitblitDashboardView {
    public RepoListView(WebDriver driver, String baseUrl) {
        super(driver, baseUrl);
    }
    public boolean isEmptyRepo(String fullyQualifiedRepoName) {
        String pathToLink = "//a[@href = \"?" + WICKET_HREF_PAGE_PATH
                + ".EmptyRepositoryPage&r=" + fullyQualifiedRepoName + "\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(pathToLink));
        return found != null && found.size() > 0;
    }
    private String getEditRepoPath(String fullyQualifiedRepoName) {
        return "//a[@href =\"?" + WICKET_HREF_PAGE_PATH
                + ".EditRepositoryPage&r=" + fullyQualifiedRepoName + "\"]";
    }
    private String getDeleteRepoOnclickIdentifier(
            String fullyQualifiedRepoPathAndName) {
        return "var conf = confirm('Delete repository \""
                + fullyQualifiedRepoPathAndName
                + "\"?'); if (!conf) return false; ";
    }
    public boolean navigateToNewRepo(long waitSecToLoad) {
        String pathToLink = "//a[@href =\"?" + WICKET_HREF_PAGE_PATH
                + ".EditRepositoryPage\"]";
        List<WebElement> found = getDriver().findElements(By.xpath(pathToLink));
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        found.get(0).click();
        WebDriverWait webDriverWait = new WebDriverWait(getDriver(),
                waitSecToLoad);
        webDriverWait.until(new Exp.EditRepoViewLoaded());
        return true;
    }
    private boolean checkOrDoEditRepo(String fullyQualifiedRepoName,
            boolean doEdit) {
        List<WebElement> found = getDriver().findElements(
                By.xpath(getEditRepoPath(fullyQualifiedRepoName)));
        if (found == null || found.size() == 0 || found.size() > 1) {
            return false;
        }
        if (doEdit) {
            found.get(0).click();
        }
        return true;
    }
    public boolean navigateToEditRepo(String fullyQualifiedRepoName,
            int waitSecToLoad) {
        boolean result = checkOrDoEditRepo(fullyQualifiedRepoName, true);
        WebDriverWait webDriverWait = new WebDriverWait(getDriver(),
                waitSecToLoad);
        webDriverWait.until(new Exp.EditRepoViewLoaded());
        return result;
    }
    public boolean isEditableRepo(String fullyQualifiedRepoName) {
        return checkOrDoEditRepo(fullyQualifiedRepoName, false);
    }
    private boolean checkOrDoDeleteRepo(String fullyQualifiedRepoPathAndName,
            boolean doDelete) {
        List<WebElement> found = getDriver().findElements(
                By.partialLinkText("delete"));
        String onclickIdentifier = getDeleteRepoOnclickIdentifier(fullyQualifiedRepoPathAndName);
        WebElement result = null;
        for (WebElement webElement : found) {
            if (webElement.getAttribute("onclick") != null
                    && webElement.getAttribute("onclick").equals(
                            onclickIdentifier)) {
                result = webElement;
                break;
            }
        }
        System.out.println("result ? " + result);
        if (result == null) {
            return false;
        }
        if (doDelete) {
            System.out.println(".............. DO DELETE .... ");
            result.click();
        }
        return true;
    }
    public boolean isDeletableRepo(String fullyQualifiedRepoPathAndName) {
        return checkOrDoDeleteRepo(fullyQualifiedRepoPathAndName, false);
    }
    public boolean navigateToDeleteRepo(String fullyQualifiedRepoPathAndName) {
        return checkOrDoDeleteRepo(fullyQualifiedRepoPathAndName, true);
    }
}