James Moger
2011-09-12 831469ba89ea8bca3bfbd1d662dbdd2c9f233798
Largely completed, uber-cool federation feature.
30 files added
1 files renamed
44 files modified
3883 ■■■■■ changed files
.classpath 8 ●●●● patch | view | raw | blame | history
.gitignore 1 ●●●● patch | view | raw | blame | history
NOTICE 16 ●●●●● patch | view | raw | blame | history
build.xml 2 ●●●●● patch | view | raw | blame | history
distrib/gitblit.properties 134 ●●●●● patch | view | raw | blame | history
distrib/users.properties 2 ●●● patch | view | raw | blame | history
docs/00_index.mkd 23 ●●●● patch | view | raw | blame | history
docs/01_features.mkd 1 ●●●● patch | view | raw | blame | history
docs/01_setup.mkd 8 ●●●●● patch | view | raw | blame | history
docs/02_federation.mkd 215 ●●●●● patch | view | raw | blame | history
docs/03_faq.mkd 4 ●●●● patch | view | raw | blame | history
docs/04_design.mkd 2 ●●●●● patch | view | raw | blame | history
docs/04_releases.mkd 18 ●●●●● patch | view | raw | blame | history
docs/architecture.odg patch | view | raw | blame | history
docs/architecture.png patch | view | raw | blame | history
resources/arrow_left.png patch | view | raw | blame | history
resources/bullet_black.png patch | view | raw | blame | history
resources/bullet_blue.png patch | view | raw | blame | history
resources/bullet_green.png patch | view | raw | blame | history
resources/bullet_orange.png patch | view | raw | blame | history
resources/bullet_red.png patch | view | raw | blame | history
resources/bullet_white.png patch | view | raw | blame | history
resources/bullet_yellow.png patch | view | raw | blame | history
resources/federated_16x16.png patch | view | raw | blame | history
resources/gitblit.css 2 ●●● patch | view | raw | blame | history
resources/heart_16x16.png patch | view | raw | blame | history
resources/information_16x16.png patch | view | raw | blame | history
src/WEB-INF/web.xml 18 ●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 97 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FederationPullExecutor.java 310 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FederationServlet.java 318 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FileUserService.java 20 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 447 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitFilter.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/IStoredSettings.java 8 ●●●● patch | view | raw | blame | history
src/com/gitblit/Launcher.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/MailExecutor.java 234 ●●●●● patch | view | raw | blame | history
src/com/gitblit/SyndicationServlet.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/build/Build.java 14 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/FederationModel.java 215 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/FederationProposal.java 79 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/RepositoryModel.java 13 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/UserModel.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/FederationUtils.java 305 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/HttpUtils.java 45 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JGitUtils.java 48 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/StringUtils.java 53 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/TimeUtils.java 39 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 34 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/WicketUtils.java 101 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.html 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditRepositoryPage.java 52 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.html 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationProposalPage.html 27 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationProposalPage.java 100 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationRegistrationPage.html 45 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationRegistrationPage.java 105 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/LoginPage.java 5 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoriesPage.html 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/RepositoriesPage.java 35 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/BasePanel.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationProposalsPanel.html 34 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationProposalsPanel.java 92 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationRegistrationsPanel.html 38 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationRegistrationsPanel.java 83 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationTokensPanel.html 38 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationTokensPanel.java 109 ●●●●● 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 49 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FederationTests.java 105 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/MarkdownUtilsTest.java 12 ●●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/StringUtilsTest.java 18 ●●●●● patch | view | raw | blame | history
.classpath
@@ -70,7 +70,7 @@
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/jdom-1.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/junit-4.8.2.jar"/>
    <classpathentry kind="lib" path="ext/junit-4.8.2.jar" sourcepath="C:/Users/James Moger/.m2/repository/junit/junit/4.8.2/junit-4.8.2-sources.jar"/>
    <classpathentry kind="lib" path="ext/org.eclipse.jgit-1.0.0.201106090707-r.jar" sourcepath="ext/org.eclipse.jgit-1.0.0.201106090707-r-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/org.eclipse.jgit-1.0.0.201106090707-r-javadoc.jar!/"/>
@@ -91,5 +91,11 @@
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/markdownpapers-core-1.1.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/gson-1.7.1.jar" sourcepath="ext/gson-1.7.1-sources.jar">
        <attributes>
            <attribute name="javadoc_location" value="jar:platform:/resource/gitblit/ext/gson-1.7.1-javadoc.jar!/"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="ext/mail-1.4.3.jar" sourcepath="ext/mail-1.4.3-sources.jar"/>
    <classpathentry kind="output" path="bin"/>
</classpath>
.gitignore
@@ -12,3 +12,4 @@
/build.properties
/war
/*.war
/proposals
NOTICE
@@ -112,6 +112,22 @@
   http://www.jdom.org
---------------------------------------------------------------------------
google-gson
---------------------------------------------------------------------------
   google-gson, released under the
   Apache-style Software License.
   http://code.google.com/p/google-gson
---------------------------------------------------------------------------
javamail
---------------------------------------------------------------------------
   javamail, released under multiple licenses
   CDDL-1.0, BSD, GPL-2.0, GNU-Classpath.
   http://kenai.com/projects/javamail
---------------------------------------------------------------------------
JUnit
---------------------------------------------------------------------------
   JUnit, released under the
build.xml
@@ -192,6 +192,7 @@
                    <include name="bug_16x16.png" />
                    <include name="book_16x16.png" />
                    <include name="blank.png" />
                    <include name="federated_16x16.png" />
                </fileset>
                <!-- Copy Doc images -->
@@ -378,6 +379,7 @@
                <include name="bug_16x16.png" />
                <include name="book_16x16.png" />
                <include name="blank.png" />
                <include name="federated_16x16.png" />
            </fileset>
            <!-- Copy Doc images -->
distrib/gitblit.properties
@@ -104,6 +104,12 @@
# SINCE 0.5.2
web.showRepositorySizes = true
# 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 display above the repositories table.
# This can point to a file with Markdown content.
# Specifying "gitblit" uses the internal welcome message.
@@ -266,6 +272,134 @@
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 =
mail.password =
# from address for generated emails
#
# SINCE 0.6.0
mail.fromAddress =
# Space-separated list of email addresses for the Gitblit administrators
#
# SINCE 0.6.0
mail.adminAddresses =
#
# 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.
#
# 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 = proposals
# The default pull frequency if frequency is unspecified on a registration
#
# SINCE 0.6.0
federation.defaultFrequency = 60 mins
# Specify the unique id of this Gitblit instance.
#
# An unspecified (empty) uuid disables procesing 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 uuid will break any registrations you have established with other
# Gitblit instances.
#
# CASE-SENSITIVE
# SINCE 0.6.0
# RESTART REQUIRED
federation.uuid =
# 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 =
# 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 default to *federation.defaultFrequency*
#
# folder:
#   if blank, the folder is *git.repositoriesFolder*
#   if specified, the folder is relative to *git.repositoriesFolder*
#
# 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-separated 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.mergeAccounts = true
#
# Server Settings
#
distrib/users.properties
@@ -1,3 +1,3 @@
## Gitblit realm file format: username=password,\#permission,repository1,repository2...
#Fri Jul 22 14:27:08 EDT 2011
admin=admin,\#admin
admin=admin,\#admin,\#notfederated
docs/00_index.mkd
@@ -23,17 +23,18 @@
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
- fixed: active repositories with a HEAD that pointed to an empty branch caused internal errors (issue 14)
- fixed: bare-cloned repositories were listed as (empty) and were not clickable (issue 13)
- fixed: default port for Gitblit GO is now 8443 to be more linux/os x friendly (issue 12)
- fixed: repositories can now be reliably deleted and renamed (issue 10)
- 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<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<br/>**New:** *web.forwardSlashCharacter = /*
- updated: MarkdownPapers 1.1.0
- updated: Jetty 7.4.3
- added: federation feature to allow gitblit instances to pull repositories and, optionally, settings and accounts from other gitblit instances.<br/>
This is something like svn-sync for gitblit.
<br/>**New:** *federation.allowProposals = false*
<br/>**New:** *federation.proposalsFolder = proposals*
<br/>**New:** *federation.defaultFrequency = 60 mins*
<br/>**New:** *federation.uuid =*
<br/>**New:** *federation.name =*
<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
- updated: MarkdownPapers 1.1.1
issues, binaries, and sources @ [Google Code][googlecode]<br/>
sources @ [Github][gitbltsrc]
docs/01_features.mkd
@@ -9,6 +9,7 @@
    <li>![view](shield_16x16.png) *Authenticated View, Clone & Push*</li>
    <li>![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only)
    </ul>
- Ability to federate with one or more other Gitblit instances
- Gitweb inspired web UI
- Administrators may create, edit, rename, or delete repositories through the web UI
- Administrators may create, edit, rename, or delete users through the web UI
docs/01_setup.mkd
@@ -20,9 +20,9 @@
Open `gitblit.properties` in your favorite text editor and make sure to review and set:
    - *git.repositoryFolder* (path may be relative or absolute)
    - *server.tempFolder* (path may be relative or absolute)
    - *server.httpPort* and *server.httpsPort*<br/>
    - *server.httpPort* and *server.httpsPort*
    - *server.httpBindInterface* and *server.httpsBindInterface*<br/>
    **https** is strongly recommended because passwords are insecurely transmitted form your browser/git client using Basic authentication!
    **https** is strongly recommended because passwords are insecurely transmitted form your browser/git client using Basic authentication!
3. Execute `gitblit.cmd` or `java -jar gitblit.jar` from a command-line
4. Wait a minute or two while all dependencies are downloaded and your self-signed *localhost* certificate is generated.<br/>Please see the section titled **Creating your own Self-Signed Certificate** to generate a certificate for *your hostname*.
5. Open your browser to <http://localhost:8080> or <https://localhost:8443> depending on your chosen configuration.
@@ -130,6 +130,8 @@
        accessRestriction = clone
        isFrozen = false
        showReadme = false
        excludeFromFederation = false
        isFederated = false
        
#### 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/>
@@ -156,7 +158,7 @@
User passwords are CASE-SENSITIVE and may be *plain* or *md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
#### User Roles
There is only one actual *role* in Gitblit and that is *#admin* which grants administrative powers to that user.  Administrators automatically have access to all repositories.  All other *roles* are repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
There are two actual *roles* in Gitblit: *#admin*, which grants administrative powers to that user, and *#notfederated*, which prevents an account from being pulled by another Gitblit instance.  Administrators automatically have access to all repositories.  All other *roles* are repository names.  If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction.  This is how users are granted access to a restricted repository.
## Authentication and Authorization Customization
Instead of maintaining a `users.properties` file, you may want to integrate Gitblit into an existing environment.
docs/02_federation.mkd
New file
@@ -0,0 +1,215 @@
## Federating Gitblit
*SINCE 0.6.0*
A Gitblit federation is basically an automated backup solution from one Gitblit instance to another.  If you are/were a Subversion user you might think of this as [svn-sync](http://svnbook.red-bean.com/en/1.5/svn.ref.svnsync.html), but better.
If your Gitblit instance allows federation and it is properly registered with another Gitblit instance, each of the *non-excluded* repositories of your Gitblit instance can be mirrored, in their entirety, to the pulling Gitblit instance.  You may optionally allow pulling of user accounts and server settings.
### Source Gitblit Instance Requirements
- *git.enableGitServlet* must be true, all Git clone and pull requests are handled through Gitblit's JGit servlet
- *federation.uuid* must be non-empty
- The Gitblit source instance must be http/https accessible by the pulling Gitblit instance.<br/>That may require configuring port-forwarding on your router and/or opening ports on your firewall.
#### federation.uuid
The uuid is used to generate permission tokens that can be shared with other Gitblit instances.
The uuid value never needs to be shared, although if you give another Gitblit instance the *ALL* federation token then your uuid will be transferred/backed-up along with all other server settings.
This value can be anything you want: an integer, a sentence, an haiku, etc.  You should probably keep the uuid simple and use standard Latin characters to prevent Java properties file encoding errors.  The tokens generated from this value are affected by case, so consider this value CASE-SENSITIVE.
The federation feature is completely disabled if your uuid value is empty.
**NOTE**:<br/>
Changing your *federation.uuid* will break any registrations you have established with other Gitblit instances.
### Pulling Gitblit Instance Requirements
 - consider setting *federation.allowProposals=true* to facilitate the registration process from source Gitblit instances
 - properly registered Gitblit instance including, at a minimum, url, *federation token*, and update frequency
### Controlling What Gets Pulled
If you want your repositories (and optionally users accounts and settings) to be pulled by another Gitblit instance, you need to register your source Gitblit instance with a pulling Gitblit instance by providing the url of your Gitblit instance and a federation token.
Gitblit generates the following federation tokens:
%BEGINCODE%
String allToken = SHA1(uuid + "-ALL");
String usersAndRepositoriesToken = SHA1(uuid + "-USERS_AND_REPOSITORIES");
String repositoriesToken = SHA1(uuid + "-REPOSITORIES");
%ENDCODE%
The *ALL* token allows another Gitblit instance to pull all your repositories, user accounts, and server settings.<br/>
The *USERS_AND_REPOSITORIES* token allows another Gitblit instance to pull all your repositories and  user accounts.<br/>
The *REPOSITORIES* token only allows pulling of the repositories.
Individual Gitblit repository configurations such as *description* and *accessRestriction* are always mirrored.
If *federation.uuid* has a non-empty value, the federation tokens are displayed in the log file and are visible, to administrators, in the web ui.
### Federation Proposals (Source Gitblit Instance)
Once you have properly setup your uuid and can see your federation tokens, you are ready to share them with a pulling Gitblit instance.
The registration process can be partially automated by sending a *federation proposal* to another Gitblit instance.<br/>
To send a proposal:
1. Login to your Gitblit instance as an administrator
2. Select and click the *send proposal* link for the appropriate token at the bottom of the repositories page
3. Enter the url of the Gitblit instance you want to federate with
4. Click submit
Not all Gitblit instances accept *federation proposals*, there is a setting which allows Gitblit to outright reject them.  In this case an email or instant message to the administrator of the other Gitblit instance is required.  :)
If your proposal is accepted, the proposal is cached to disk on the remote Gitblit server and, if properly configured, the administrators of that Gitblit server will receive an email notification of your proposal.
Your proposal includes:
1. the url of your Gitblit instance
2. the federation token you selected and its type
3. the list of your *non-excluded* repositories, and their configuration details, that you propose to share
Submitting a proposal does not automatically register your server with the remote Gitblit instance.<br/>
Registration is a manual process for an administrator.
### Federation Proposals (Pulling Gitblit Instance)
If your Giblit instance has received a *federation proposal*, you will be alerted to that information the next time you login to Gitblit as an administrator.
You may view the details of a proposal from scrolling down to the bottom of the repositories page and selecting a proposal.  Sample registration settings will be generated for you that you may copy & paste into either your `gitblit.properties` file or your `web.xml` file.
### Excluding Repositories (Source Gitblit Instance)
You may exclude a repository from being pulled by a federated Gitblit instance by setting its *federation strategy* in the repository's settings page.
### Excluding Repositories (Pulling Gitblit Instance)
You may exclude repositories to pull in a federation registration.  You may exclude all or you may exclude based on a simple fuzzy pattern.  Only one wildcard character may be used within each pattern.  Patterns are space-separated within the exclude and include fields.
    federation.example.exclude = skipit.git
**OR**
    federation.example.exclude = *
    federation.example.include = somerepo.git someotherrepo.git
**OR**
    federation.example.exclude = *
    federation.example.include = common/* library/*
### Tracking Status (Pulling Gitblit Instance)
Below the repositories list on the repositories page you will find a section named *federation registrations*.  This section enumerates the other gitblit servers you have configured to periodically pull.  The *status* of the latest pull will be indicated on the left with a colored circle, similar to the status of an executed unit test or Hudson/Jenkins build.  You can drill into the details of the registration to view the status of the last pull from each repository available from that source Gitblit instance.  Additionally, you can specify the *federation.N.notifyOnError=true* flag, to be alerted via email of regressive status changes to individual registrations.
### Tracking Status (Source Gitblit Instance)
Source Gitblit instances can not directly track the success or failure status of Pulling Gitblit instances.  However, the Pulling Gitblit instance may elect to send a status acknowledgment (*federation.N.sendStatus=true*) to the source Gitblit server that indicates the per-repository status of the pull operation.  This is the same data that is displayed on the Pulling Gitblit instances ui.
### How does it work? (Source Gitblit Instances)
A pulling Gitblit instance will periodically contact your Gitblit instance and will provide the token as proof that you have granted it federation access.  Your Gitblit instance will decide, based on the supplied token, if the requested data should be returned to the pulling Gitblit instance.  Gitblit data (user accounts, repository metadata, and server settings) are serialized as [JSON](http://json.org) using [google-gson](http://google-gson.googlecode.com) and returned to the pulling Gitblit instance.  Standard Git clone and pull operations are used to transfer commits.
The federation process executes using an internal administrator account, *$gitblit*.  All the normal authentication and authorization processes are used for federation requests. For example, Git commands are authenticated as *$gitblit / token*.
While the *$gitblit* account has access to all repositories, server settings, and user accounts, it is prohibited from accessing the web ui and it is disabled if *federation.uuid* is empty.
The federation feature should be considered a backdoor and enabled or disabled as appropriate for your installation.
### How does it work? (Pulling Gitblit Instances)
Federated repositories defined in `gitblit.properties` are checked after Gitblit has been running for 1 minute.  The next registration check is scheduled at the completion of the current registration check based on the registration's specified 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 default to 60 minutes
After a repository has been cloned it is flagged as *isFederated* (which identifies it as being sourced from another Gitblit instance), *isFrozen* (which prevents Git pushes to this mirror) and *federationStrategy=EXCLUDED* (which prevents this repository from being pulled by another federated Gitblit instance).
#### Origin Verification
During a federated pull operation, Gitblit does check that the *origin* of the local repository starts with the url of the federation registration.<br/>
If they do not match, the repository is skipped and this is indicated in the log.
#### User Accounts
By default all user accounts except the *admin* account are automatically pulled when using the *ALL* token or the *USERS_AND_REPOSITORIES* token.  You may exclude a user account form being pulled by a federated Gitblit instance by checking *exclude form federation* in the edit user page.
The pulling Gitblit instance will store a registration-specific `users.properties` file for the pulled user accounts and their repository permissions. This file is stored in the *federation.N.folder* folder.
If you specify *federation.N.mergeAccounts=true*, then the user accounts from the source Gitblit instance will be integrated into the `users.properties` file of your Gitblit instance and allow sign-on of those users.
**NOTE:**<br/>
Upgrades from older Gitblit versions will not have the *#notfederated* role assigned to the *admin* account.  Without that role, your admin account WILL be transferred with an *ALL* or *USERS_AND_REPOSITORIES* token.<br/>
Please consider setting that flag!
#### Server Settings
Server settings are automatically pulled when using the *ALL* token.
The pulling Gitblit instance will store a registration-specific `gitblit.properties` file for all pulled settings.  This file is stored in the *federation.N.folder* folder.
These settings are unused by the pulling Gitblit instance.
### Collisions and Conflict Resolution
Gitblit does **not** detect conflict and it does **not** offer conflict resolution of repositories, users, or settings.
If an object exists locally that has the same name as the remote object, it is assumed they are the same and the contents of the remote object are merged into the local object.  If you can not guarantee that this is the case, then you should not store any federated repositories directly in *git.repositoriesFolder* and you should not enable *mergeAccounts*.
By default, federated repositories can not be pushed to, they are read-only by the *isFrozen* flag.  This flag is **ONLY** enforced by Gitblit's JGit servlet.  If you push to a federated repository after resetting the *isFrozen* flag or via some other Git access technique then you may break Gitblit's ability to continue pulling from the source repository.  If you are only pushing to a local branch then you might be safe.
## Example Federation Pull Registrations
These examples would be entered into the `gitblit.properties` file of the pulling gitblit instance.
#### (Nearly) Perfect Mirror Example
This assumes that the *token* is the *ALL* token from the source gitblit instance.<br/>
The repositories, example1_users.properties, and example1_gitblit.properties will be put in *git.repositoriesFolder* and the source user accounts will be merged into the local user accounts, including passwords and administrator status.  The mirror instance will also send a status acknowledgment at the end of the pull operation which will include the state of each repository pull (EXCLUDED, SKIPPED, PULLED).  This way the source Gitblit instance can monitor the health of its mirrors.
This example is considered *nearly* perfect because while the remote Gitblit's server settings are pulled and saved locally, they are not merged with your server settings so its not a true mirror, but its likely the mirror you'd want to configure.
    federation.example1.url = https://go.gitblit.com
    federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
    federation.example1.frequency = 120 mins
    federation.example1.folder =
    federation.example1.mergeAccounts = true
    federation.example1.sendStatus = true
#### Just Repositories Example
This assumes that the *token* is the *REPOSITORIES* token from the remote gitblit instance.<br/>
The repositories will be put in *git.repositoriesFolder*/example2.
    federation.example2.url = https://tomcat.gitblit.com/gitblit
    federation.example2.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
    federation.example2.frequency = 120 mins
    federation.example2.folder = example2
#### All-but-One Repository Example
This assumes that the *token* is the *REPOSITORIES* token from the remote gitblit instance.<br/>
The repositories will be put in *git.repositoriesFolder*/example3.
    federation.example3.url = https://tomcat.gitblit.com/gitblit
    federation.example3.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
    federation.example3.frequency = 120 mins
    federation.example3.folder = example3
    federation.example3.exclude = somerepo.git
#### Just One Repository Example
This assumes that the *token* is the *REPOSITORIES* token from the remote gitblit instance.<br/>
The repositories will be put in *git.repositoriesFolder*/example4.
    federation.example4.url = https://tomcat.gitblit.com/gitblit
    federation.example4.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
    federation.example4.frequency = 120 mins
    federation.example4.folder = example4
    federation.example4.exclude = *
    federation.example4.include = somerepo.git
docs/03_faq.mkd
File was renamed from docs/02_faq.mkd
@@ -113,6 +113,10 @@
    
Alternatively, you could enable the search type dropdown list in your `gitblit.properties` file.
### Why did you call the setting federation.N.frequency instead of federation.N.period?!
Yes, yes I know that you are really specifying the period, but Frequency sounds better to me.  :)
### Can Gitblit be translated?
Yes.  Most messages are localized to a standard Java properties file.
docs/04_design.mkd
@@ -31,6 +31,8 @@
- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD)
- [Rome](http://rome.dev.java.net) (Apache 1.1)
- [jdom](http://www.jdom.org) (Apache-style JDOM license)
- [google-gson](http://code.google.com/google-gson) (Apache 2.0)
- [javamail](http://kenai.com/projects/javamail) (CDDL-1.0, BSD, GPL-2.0, GNU-Classpath)
### Other Build Dependencies
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
docs/04_releases.mkd
@@ -3,6 +3,23 @@
### Current Release
**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] &nbsp; *released %BUILDDATE%*
- added: federation feature to allow gitblit instances to pull repositories and, optionally, settings and accounts from other gitblit instances.<br/>
This is something like svn-sync for gitblit.
<br/>**New:** *federation.allowProposals = false*
<br/>**New:** *federation.proposalsFolder = proposals*
<br/>**New:** *federation.defaultFrequency = 60 mins*
<br/>**New:** *federation.uuid =*
<br/>**New:** *federation.name =*
<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
- updated: MarkdownPapers 1.1.1
### Older Releases
**0.5.2** ([go](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.5.1.zip)|[war](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.5.1.war)) based on [JGit 1.0.0 (201106090707-r)][jgit] &nbsp; *released 2006-06-28*
- fixed: active repositories with a HEAD that pointed to an empty branch caused internal errors (issue 14)
- fixed: bare-cloned repositories were listed as (empty) and were not clickable (issue 13)
- fixed: default port for Gitblit GO is now 8443 to be more linux/os x friendly (issue 12)
@@ -15,7 +32,6 @@
- updated: MarkdownPapers 1.1.0
- updated: Jetty 7.4.3
### Older Releases
**0.5.1** ([go](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.5.1.zip)|[war](http://code.google.com/p/gitblit/downloads/detail?name=gitblit-0.5.1.war)) based on [JGit 1.0.0 (201106090707-r)][jgit] &nbsp; *released 2006-06-28*
- clarified SSL certificate generation and configuration for both server-side and client-side
docs/architecture.odg
Binary files differ
docs/architecture.png

resources/arrow_left.png
resources/bullet_black.png
resources/bullet_blue.png
resources/bullet_green.png
resources/bullet_orange.png
resources/bullet_red.png
resources/bullet_white.png
resources/bullet_yellow.png
resources/federated_16x16.png
resources/gitblit.css
@@ -12,7 +12,7 @@
}
body {
    width: 980px;
    width: 1000px;
    margin: 5px;
    background-color: #ffffff;
    color: #000000;
resources/heart_16x16.png
resources/information_16x16.png
src/WEB-INF/web.xml
@@ -84,6 +84,19 @@
        <filter-name>SyndicationFilter</filter-name>
        <url-pattern>/feed/*</url-pattern>
    </filter-mapping>
    <!-- Federation Servlet
         <url-pattern> MUST match:
             * com.gitblit.Constants.FEDERATION_PATH
            * Wicket Filter ignorePaths parameter -->
    <servlet>
        <servlet-name>FederationServlet</servlet-name>
        <servlet-class>com.gitblit.FederationServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FederationServlet</servlet-name>
        <url-pattern>/federation/*</url-pattern>
    </servlet-mapping>
        
    <!-- Wicket Filter -->
@@ -106,8 +119,9 @@
                 * GitServlet <url-pattern>
                 * com.gitblit.Constants.GIT_PATH
                 * ZipServlet <url-pattern>
                 * com.gitblit.Constants.ZIP_PATH -->
            <param-value>git/,feed/,zip/</param-value>
                 * com.gitblit.Constants.ZIP_PATH
                 * FederationServlet <url-pattern> -->
            <param-value>git/,feed/,zip/,federation/</param-value>
        </init-param>
    </filter>
    <filter-mapping>
src/com/gitblit/Constants.java
@@ -41,6 +41,8 @@
    public static final String ADMIN_ROLE = "#admin";
    public static final String NOT_FEDERATED_ROLE = "#notfederated";
    public static final String PROPERTIES_FILE = "gitblit.properties";
    public static final String GIT_PATH = "/git/";
@@ -49,10 +51,20 @@
    public static final String SYNDICATION_PATH = "/feed/";
    public static final String FEDERATION_PATH = "/federation/";
    public static final String BORDER = "***********************************************************";
    public static final String FEDERATION_USER = "$gitblit";
    public static final String PROPOSAL_EXT = ".json";
    public static String getGitBlitVersion() {
        return NAME + " v" + VERSION;
    }
    /**
     * Enumeration representing the 4 access restriction levels.
     * Enumeration representing the four access restriction levels.
     */
    public static enum AccessRestrictionType {
        NONE, PUSH, CLONE, VIEW;
@@ -79,7 +91,86 @@
        }
    }
    public static String getGitBlitVersion() {
        return NAME + " v" + VERSION;
    /**
     * Enumeration representing the types of federation tokens.
     */
    public static enum FederationToken {
        ALL, USERS_AND_REPOSITORIES, REPOSITORIES;
        public static FederationToken fromName(String name) {
            for (FederationToken type : values()) {
                if (type.name().equalsIgnoreCase(name)) {
                    return type;
                }
            }
            return REPOSITORIES;
        }
        public String toString() {
            return name();
        }
    }
    /**
     * Enumeration representing the types of federation requests.
     */
    public static enum FederationRequest {
        PROPOSAL, PULL_REPOSITORIES, PULL_USERS, PULL_SETTINGS, STATUS;
        public static FederationRequest fromName(String name) {
            for (FederationRequest type : values()) {
                if (type.name().equalsIgnoreCase(name)) {
                    return type;
                }
            }
            return PULL_REPOSITORIES;
        }
        public String toString() {
            return name();
        }
    }
    /**
     * Enumeration representing the statii of federation requests.
     */
    public static enum FederationPullStatus {
        PENDING, FAILED, SKIPPED, PULLED, EXCLUDED;
        public static FederationPullStatus fromName(String name) {
            for (FederationPullStatus type : values()) {
                if (type.name().equalsIgnoreCase(name)) {
                    return type;
                }
            }
            return PENDING;
        }
        @Override
        public String toString() {
            return name();
        }
    }
    /**
     * Enumeration representing the federation types.
     */
    public static enum FederationStrategy {
        EXCLUDE, FEDERATE_THIS, FEDERATE_ORIGIN;
        public static FederationStrategy fromName(String name) {
            for (FederationStrategy type : values()) {
                if (type.name().equalsIgnoreCase(name)) {
                    return type;
                }
            }
            return FEDERATE_THIS;
        }
        @Override
        public String toString() {
            return name();
        }
    }
}
src/com/gitblit/FederationPullExecutor.java
New file
@@ -0,0 +1,310 @@
/*
 * 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;
import java.io.File;
import java.io.FileOutputStream;
import java.net.InetAddress;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.FederationPullStatus;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.models.FederationModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.utils.JGitUtils.CloneResult;
import com.gitblit.utils.StringUtils;
/**
 * FederationPullExecutor pulls repository updates and, optionally, user
 * accounts and server settings from registered Gitblit instances.
 */
public class FederationPullExecutor implements Runnable {
    private final Logger logger = LoggerFactory.getLogger(FederationPullExecutor.class);
    private final List<FederationModel> registrations;
    /**
     * Constructor for specifying a single federation registration. This
     * constructor is used to schedule the next pull execution.
     *
     * @param registration
     */
    private FederationPullExecutor(FederationModel registration) {
        this(Arrays.asList(registration));
    }
    /**
     * Constructor to specify a group of federation registrations. This is
     * normally used at startup to pull and then schedule the next update based
     * on each registrations frequency setting.
     *
     * @param registrations
     */
    public FederationPullExecutor(List<FederationModel> registrations) {
        this.registrations = registrations;
    }
    /**
     * Run method for this pull executor.
     */
    @Override
    public void run() {
        for (FederationModel registration : registrations) {
            FederationPullStatus was = registration.getLowestStatus();
            try {
                Date now = new Date(System.currentTimeMillis());
                pull(registration);
                sendStatusAcknowledgment(registration);
                registration.lastPull = now;
                FederationPullStatus is = registration.getLowestStatus();
                if (is.ordinal() < was.ordinal()) {
                    // the status for this registration has downgraded
                    logger.warn("Federation pull status of {0} is now {1}", registration.name,
                            is.name());
                    if (registration.notifyOnError) {
                        String message = "Federation pull of " + registration.name + " @ "
                                + registration.url + " is now at " + is.name();
                        GitBlit.self()
                                .notifyAdministrators(
                                        "Pull Status of " + registration.name + " is " + is.name(),
                                        message);
                    }
                }
            } catch (Throwable t) {
                logger.error(MessageFormat.format(
                        "Failed to pull from federated gitblit ({0} @ {1})", registration.name,
                        registration.url), t);
            } finally {
                schedule(registration);
            }
        }
    }
    /**
     * Mirrors a repository and, optionally, the server's users, and/or
     * configuration settings from a remote Gitblit instance.
     *
     * @param registration
     * @throws Exception
     */
    private void pull(FederationModel registration) throws Exception {
        Map<String, RepositoryModel> repositories = FederationUtils.getRepositories(registration,
                true);
        String registrationFolder = registration.folder.toLowerCase().trim();
        // confirm valid characters in server alias
        Character c = StringUtils.findInvalidCharacter(registrationFolder);
        if (c != null) {
            logger.error(MessageFormat
                    .format("Illegal character ''{0}'' in folder name ''{1}'' of federation registration {2}!",
                            c, registrationFolder, registration.name));
            return;
        }
        File repositoriesFolder = new File(GitBlit.getString(Keys.git.repositoriesFolder, "git"));
        File registrationFolderFile = new File(repositoriesFolder, registrationFolder);
        registrationFolderFile.mkdirs();
        // Clone/Pull the repository
        for (Map.Entry<String, RepositoryModel> entry : repositories.entrySet()) {
            String cloneUrl = entry.getKey();
            RepositoryModel repository = entry.getValue();
            if (!repository.hasCommits) {
                logger.warn(MessageFormat.format(
                        "Skipping federated repository {0} from {1} @ {2}. Repository is EMPTY.",
                        repository.name, registration.name, registration.url));
                registration.updateStatus(repository, FederationPullStatus.SKIPPED);
                continue;
            }
            String repositoryName;
            if (StringUtils.isEmpty(registrationFolder)) {
                repositoryName = repository.name;
            } else {
                repositoryName = registrationFolder + "/" + repository.name;
            }
            // confirm that the origin of any pre-existing repository matches
            // the clone url
            Repository existingRepository = GitBlit.self().getRepository(repositoryName);
            if (existingRepository != null) {
                StoredConfig config = existingRepository.getConfig();
                config.load();
                String origin = config.getString("remote", "origin", "url");
                existingRepository.close();
                if (!origin.startsWith(registration.url)) {
                    logger.warn(MessageFormat
                            .format("Skipping federated repository {0} from {1} @ {2}. Origin does not match, consider EXCLUDING.",
                                    repository.name, registration.name, registration.url));
                    registration.updateStatus(repository, FederationPullStatus.SKIPPED);
                    continue;
                }
            }
            // clone/pull this repository
            CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(
                    Constants.FEDERATION_USER, registration.token);
            logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}",
                    repository.name, registration.name, registration.url));
            CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name,
                    cloneUrl, credentials);
            Repository r = GitBlit.self().getRepository(repositoryName);
            RepositoryModel rm = GitBlit.self().getRepositoryModel(repositoryName);
            if (result.createdRepository) {
                // default local settings
                repository.federationStrategy = FederationStrategy.EXCLUDE;
                repository.isFrozen = true;
            } else {
                // preserve local settings
                repository.isFrozen = rm.isFrozen;
                repository.federationStrategy = rm.federationStrategy;
            }
            // only repositories that are actually _cloned_ from the source
            // Gitblit repository are marked as federated. If the origin
            // is from somewhere else, these repositories are not considered
            // "federated" repositories.
            repository.isFederated = cloneUrl.startsWith(registration.url);
            GitBlit.self().updateConfiguration(r, repository);
            r.close();
            registration.updateStatus(repository, FederationPullStatus.PULLED);
        }
        try {
            // Pull USERS
            Collection<UserModel> users = FederationUtils.getUsers(registration);
            if (users != null && users.size() > 0) {
                File realmFile = new File(registrationFolderFile, registration.name
                        + "_users.properties");
                realmFile.delete();
                FileUserService userService = new FileUserService(realmFile);
                for (UserModel user : users) {
                    userService.updateUserModel(user.username, user);
                    // merge the remote permissions and remote accounts into
                    // the user accounts of this Gitblit instance
                    if (registration.mergeAccounts) {
                        // reparent all repository permissions if the local
                        // repositories are stored within subfolders
                        if (!StringUtils.isEmpty(registrationFolder)) {
                            List<String> permissions = new ArrayList<String>(user.repositories);
                            user.repositories.clear();
                            for (String permission : permissions) {
                                user.addRepository(registrationFolder + "/" + permission);
                            }
                        }
                        // insert new user or update local user
                        UserModel localUser = GitBlit.self().getUserModel(user.username);
                        if (localUser == null) {
                            // create new local user
                            GitBlit.self().updateUserModel(user.username, user, true);
                        } else {
                            // update repository permissions of local user
                            for (String repository : user.repositories) {
                                localUser.addRepository(repository);
                            }
                            localUser.password = user.password;
                            localUser.canAdmin = user.canAdmin;
                            GitBlit.self().updateUserModel(localUser.username, localUser, false);
                        }
                    }
                }
            }
        } catch (Exception e) {
            // a 403 error code is normal for a PULL_REPOSITORIES token
            if (!e.getMessage().contains("403")) {
                logger.warn(MessageFormat.format(
                        "Failed to retrieve USERS from federated gitblit ({0} @ {1})",
                        registration.name, registration.url), e);
            }
        }
        try {
            // Pull SETTINGS
            Map<String, String> settings = FederationUtils.getSettings(registration);
            if (settings != null && settings.size() > 0) {
                Properties properties = new Properties();
                properties.putAll(settings);
                FileOutputStream os = new FileOutputStream(new File(registrationFolderFile,
                        registration.name + "_" + Constants.PROPERTIES_FILE));
                properties.store(os, null);
                os.close();
            }
        } catch (Exception e) {
            // a 403 error code is normal for a PULL_REPOSITORIES token
            if (!e.getMessage().contains("403")) {
                logger.warn(MessageFormat.format(
                        "Failed to retrieve SETTINGS from federated gitblit ({0} @ {1})",
                        registration.name, registration.url), e);
            }
        }
    }
    /**
     * Sends a status acknowledgment to the source Gitblit instance. This
     * includes the results of the federated pull.
     *
     * @param registration
     * @throws Exception
     */
    private void sendStatusAcknowledgment(FederationModel registration) throws Exception {
        if (!registration.sendStatus) {
            // skip status acknowledgment
            return;
        }
        InetAddress addr = InetAddress.getLocalHost();
        String federationName = GitBlit.getString(Keys.federation.name, null);
        if (StringUtils.isEmpty(federationName)) {
            federationName = addr.getHostName();
        }
        FederationUtils.acknowledgeStatus(addr.getHostAddress(), registration);
    }
    /**
     * Schedules the next check of the federated Gitblit instance.
     *
     * @param registration
     */
    private void schedule(FederationModel registration) {
        // schedule the next pull
        int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency);
        registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
        GitBlit.self().executor()
                .schedule(new FederationPullExecutor(registration), mins, TimeUnit.MINUTES);
        logger.info(MessageFormat.format(
                "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
                registration.name, registration.url, registration.nextPull));
    }
}
src/com/gitblit/FederationServlet.java
New file
@@ -0,0 +1,318 @@
/*
 * 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;
import java.io.BufferedReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
 * Handles federation requests.
 *
 * @author James Moger
 *
 */
public class FederationServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private transient Logger logger = LoggerFactory.getLogger(FederationServlet.class);
    public FederationServlet() {
        super();
    }
    /**
     * Returns an url to this servlet for the specified parameters.
     *
     * @param sourceURL
     *            the url of the source gitblit instance
     * @param token
     *            the federation token of the source gitblit instance
     * @param req
     *            the pull type request
     */
    public static String asPullLink(String sourceURL, String token, FederationRequest req) {
        return asFederationLink(sourceURL, null, token, req, null);
    }
    /**
     *
     * @param remoteURL
     *            the url of the remote gitblit instance
     * @param tokenType
     *            the type of federation token of a gitblit instance
     * @param token
     *            the federation token of a gitblit instance
     * @param req
     *            the pull type request
     * @param myURL
     *            the url of this gitblit instance
     * @return
     */
    public static String asFederationLink(String remoteURL, FederationToken tokenType,
            String token, FederationRequest req, String myURL) {
        if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
            remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
        }
        if (req == null) {
            req = FederationRequest.PULL_REPOSITORIES;
        }
        return remoteURL + Constants.FEDERATION_PATH + "?req=" + req.name().toLowerCase()
                + "&token=" + token
                + (tokenType == null ? "" : ("&tokenType=" + tokenType.name().toLowerCase()))
                + (myURL == null ? "" : ("&url=" + StringUtils.encodeURL(myURL)));
    }
    /**
     * Returns the list of repositories for federation requests.
     *
     * @param request
     * @param response
     * @throws javax.servlet.ServletException
     * @throws java.io.IOException
     */
    private void processRequest(javax.servlet.http.HttpServletRequest request,
            javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
            java.io.IOException {
        if (!GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {
            logger.warn(Keys.git.enableGitServlet + " must be set TRUE for federation requests.");
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        String uuid = GitBlit.getString(Keys.federation.uuid, "");
        if (StringUtils.isEmpty(uuid)) {
            logger.warn(Keys.federation.uuid + " is not properly set!  Federation request denied.");
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        String token = request.getParameter("token");
        FederationRequest reqType = FederationRequest.fromName(request.getParameter("req"));
        logger.info(MessageFormat.format("Federation {0} request from {1}", reqType,
                request.getRemoteAddr()));
        if (FederationRequest.PROPOSAL.equals(reqType)) {
            // Receive a gitblit federation proposal
            String url = StringUtils.decodeFromHtml(request.getParameter("url"));
            FederationToken tokenType = FederationToken.fromName(request.getParameter("tokenType"));
            if (!GitBlit.getBoolean(Keys.federation.allowProposals, false)) {
                logger.error(MessageFormat.format("Rejected {0} federation proposal from {1}",
                        tokenType.name(), url));
                response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                return;
            }
            BufferedReader reader = request.getReader();
            StringBuilder json = new StringBuilder();
            String line = null;
            while ((line = reader.readLine()) != null) {
                json.append(line);
            }
            reader.close();
            // check to see if we have repository data
            if (json.length() == 0) {
                logger.error(MessageFormat.format(
                        "Failed to receive proposed repositories list from {0}", url));
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
            // deserialize the repository data
            Gson gson = new Gson();
            Map<String, RepositoryModel> repositories = gson.fromJson(json.toString(),
                    FederationUtils.REPOSITORIES_TYPE);
            // submit a proposal
            FederationProposal proposal = new FederationProposal(url, tokenType, token,
                    repositories);
            String hosturl = HttpUtils.getHostURL(request);
            String gitblitUrl = hosturl + request.getContextPath();
            GitBlit.self().submitFederationProposal(proposal, gitblitUrl);
            logger.info(MessageFormat.format(
                    "Submitted {0} federation proposal to pull {1} repositories from {2}",
                    tokenType.name(), repositories.size(), url));
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        if (FederationRequest.STATUS.equals(reqType)) {
            // Receive a gitblit federation status acknowledgment
            String remoteId = StringUtils.decodeFromHtml(request.getParameter("url"));
            String identification = MessageFormat.format("{0} ({1})", remoteId,
                    request.getRemoteAddr());
            BufferedReader reader = request.getReader();
            StringBuilder json = new StringBuilder();
            String line = null;
            while ((line = reader.readLine()) != null) {
                json.append(line);
            }
            reader.close();
            // check to see if we have repository data
            if (json.length() == 0) {
                logger.error(MessageFormat.format(
                        "Failed to receive pulled repositories list from {0}", identification));
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
            // deserialize the status data
            Gson gson = new Gson();
            FederationModel results = gson.fromJson(json.toString(), FederationModel.class);
            // setup the last and netx pull dates
            results.lastPull = new Date();
            int mins = TimeUtils.convertFrequencyToMinutes(results.frequency);
            results.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
            // acknowledge the receipt of status
            GitBlit.self().acknowledgeFederationStatus(identification, results);
            logger.info(MessageFormat.format(
                    "Received status of {0} federated repositories from {1}", results
                            .getStatusList().size(), identification));
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        // Determine the federation tokens for this gitblit instance
        List<String> tokens = GitBlit.self().getFederationTokens();
        if (!tokens.contains(token)) {
            logger.warn(MessageFormat.format(
                    "Received Federation token ''{0}'' does not match the server tokens", token));
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        Object result = null;
        if (FederationRequest.PULL_REPOSITORIES.equals(reqType)) {
            // Determine the Gitblit clone url
            StringBuilder sb = new StringBuilder();
            sb.append(HttpUtils.getHostURL(request));
            sb.append(Constants.GIT_PATH);
            sb.append("{0}");
            String cloneUrl = sb.toString();
            // Retrieve all available repositories
            UserModel user = new UserModel(Constants.FEDERATION_USER);
            user.canAdmin = true;
            List<RepositoryModel> list = GitBlit.self().getRepositoryModels(user);
            // create the [cloneurl, repositoryModel] map
            Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
            for (RepositoryModel model : list) {
                // by default, setup the url for THIS repository
                String url = MessageFormat.format(cloneUrl, model.name);
                switch (model.federationStrategy) {
                case EXCLUDE:
                    // skip this repository
                    continue;
                case FEDERATE_ORIGIN:
                    // federate the origin, if it is defined
                    if (!StringUtils.isEmpty(model.origin)) {
                        url = model.origin;
                    }
                    break;
                }
                repositories.put(url, model);
            }
            result = repositories;
        } else {
            if (FederationRequest.PULL_SETTINGS.equals(reqType)) {
                // pull settings
                if (!GitBlit.self().validateFederationRequest(reqType, token)) {
                    // invalid token to pull users or settings
                    logger.warn(MessageFormat.format(
                            "Federation token from {0} not authorized to pull SETTINGS",
                            request.getRemoteAddr()));
                    response.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return;
                }
                Map<String, String> settings = new HashMap<String, String>();
                List<String> keys = GitBlit.getAllKeys(null);
                for (String key : keys) {
                    settings.put(key, GitBlit.getString(key, ""));
                }
                result = settings;
            } else if (FederationRequest.PULL_USERS.equals(reqType)) {
                // pull users
                if (!GitBlit.self().validateFederationRequest(reqType, token)) {
                    // invalid token to pull users or settings
                    logger.warn(MessageFormat.format(
                            "Federation token from {0} not authorized to pull USERS",
                            request.getRemoteAddr()));
                    response.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return;
                }
                List<String> usernames = GitBlit.self().getAllUsernames();
                List<UserModel> users = new ArrayList<UserModel>();
                for (String username : usernames) {
                    UserModel user = GitBlit.self().getUserModel(username);
                    if (!user.excludeFromFederation) {
                        users.add(user);
                    }
                }
                result = users;
            }
        }
        if (result != null) {
            // Send JSON response
            Gson gson = new GsonBuilder().setPrettyPrinting().create();
            String json = gson.toJson(result);
            response.getWriter().append(json);
        }
    }
    @Override
    protected void doPost(javax.servlet.http.HttpServletRequest request,
            javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
            java.io.IOException {
        processRequest(request, response);
    }
    @Override
    protected void doGet(javax.servlet.http.HttpServletRequest request,
            javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
            java.io.IOException {
        processRequest(request, response);
    }
}
src/com/gitblit/FileUserService.java
@@ -149,6 +149,8 @@
                // Permissions
                if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
                    model.canAdmin = true;
                } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
                    model.excludeFromFederation = true;
                }
                break;
            default:
@@ -188,6 +190,9 @@
            // Permissions
            if (model.canAdmin) {
                roles.add(Constants.ADMIN_ROLE);
            }
            if (model.excludeFromFederation) {
                roles.add(Constants.NOT_FEDERATED_ROLE);
            }
            StringBuilder sb = new StringBuilder();
@@ -499,14 +504,15 @@
        // If the write is successful, delete the current file and rename
        // the temporary copy to the original filename.
        if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
            if (propertiesFile.delete()) {
                if (!realmFileCopy.renameTo(propertiesFile)) {
                    throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
                            realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
            if (propertiesFile.exists()) {
                if (!propertiesFile.delete()) {
                    throw new IOException(MessageFormat.format("Failed to delete {0}!",
                            propertiesFile.getAbsolutePath()));
                }
            } else {
                throw new IOException(MessageFormat.format("Failed to delete (0)!",
                        propertiesFile.getAbsolutePath()));
            }
            if (!realmFileCopy.renameTo(propertiesFile)) {
                throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
                        realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
            }
        } else {
            throw new IOException(MessageFormat.format("Failed to save {0}!",
src/com/gitblit/GitBlit.java
@@ -16,6 +16,7 @@
package com.gitblit;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.MessageFormat;
@@ -25,8 +26,14 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.Cookie;
@@ -46,10 +53,17 @@
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.FederationToken;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
 * GitBlit is the servlet context listener singleton that acts as the core for
@@ -73,6 +87,13 @@
    private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
    private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
    private final List<FederationModel> federationRegistrations = Collections
            .synchronizedList(new ArrayList<FederationModel>());
    private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
    private RepositoryResolver<Void> repositoryResolver;
    private File repositoriesFolder;
@@ -82,6 +103,8 @@
    private IUserService userService;
    private IStoredSettings settings;
    private MailExecutor mailExecutor;
    public GitBlit() {
        if (gitblit == null) {
@@ -100,6 +123,15 @@
            new GitBlit();
        }
        return gitblit;
    }
    /**
     * Determine if this is the GO variant of Gitblit.
     *
     * @return true if this is the GO variant of Gitblit.
     */
    public static boolean isGO() {
        return self().settings instanceof FileSettings;
    }
    /**
@@ -226,6 +258,30 @@
     * @return a user object or null
     */
    public UserModel authenticate(String username, char[] password) {
        if (StringUtils.isEmpty(username)) {
            // can not authenticate empty username
            return null;
        }
        String pw = new String(password);
        if (StringUtils.isEmpty(pw)) {
            // can not authenticate empty password
            return null;
        }
        // check to see if this is the federation user
        if (canFederate()) {
            if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
                List<String> tokens = getFederationTokens();
                if (tokens.contains(pw)) {
                    // the federation user is an administrator
                    UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
                    federationUser.canAdmin = true;
                    return federationUser;
                }
            }
        }
        // delegate authentication to the user service
        if (userService == null) {
            return null;
        }
@@ -463,6 +519,10 @@
            model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
            model.isFrozen = getConfig(config, "isFrozen", false);
            model.showReadme = getConfig(config, "showReadme", false);
            model.federationStrategy = FederationStrategy.fromName(getConfig(config,
                    "federationStrategy", null));
            model.isFederated = getConfig(config, "isFederated", false);
            model.origin = config.getString("remote", "origin", "url");
        }
        r.close();
        return model;
@@ -614,22 +674,36 @@
        // update settings
        if (r != null) {
            StoredConfig config = JGitUtils.readConfig(r);
            config.setString("gitblit", null, "description", repository.description);
            config.setString("gitblit", null, "owner", repository.owner);
            config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
            config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
            config.setString("gitblit", null, "accessRestriction",
                    repository.accessRestriction.name());
            config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
            config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
            config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
            try {
                config.save();
            } catch (IOException e) {
                logger.error("Failed to save repository config!", e);
            }
            updateConfiguration(r, repository);
            r.close();
        }
    }
    /**
     * Updates the Gitblit configuration for the specified repository.
     *
     * @param r
     *            the Git repository
     * @param repository
     *            the Gitblit repository model
     */
    public void updateConfiguration(Repository r, RepositoryModel repository) {
        StoredConfig config = JGitUtils.readConfig(r);
        config.setString("gitblit", null, "description", repository.description);
        config.setString("gitblit", null, "owner", repository.owner);
        config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
        config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
        config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
        config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
        config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
        config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
        config.setString("gitblit", null, "federationStrategy",
                repository.federationStrategy.name());
        config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
        try {
            config.save();
        } catch (IOException e) {
            logger.error("Failed to save repository config!", e);
        }
    }
@@ -711,6 +785,341 @@
    }
    /**
     * Returns Gitblit's scheduled executor service for scheduling tasks.
     *
     * @return scheduledExecutor
     */
    public ScheduledExecutorService executor() {
        return scheduledExecutor;
    }
    public static boolean canFederate() {
        String uuid = getString(Keys.federation.uuid, "");
        return !StringUtils.isEmpty(uuid);
    }
    /**
     * Configures this Gitblit instance to pull any registered federated gitblit
     * instances.
     */
    private void configureFederation() {
        boolean validUuid = true;
        String uuid = settings.getString(Keys.federation.uuid, "");
        if (StringUtils.isEmpty(uuid)) {
            logger.warn("Federation UUID is blank! This server can not be PULLED from.");
            validUuid = false;
        }
        if (validUuid) {
            for (FederationToken tokenType : FederationToken.values()) {
                logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
                        getFederationToken(tokenType)));
            }
        }
        // Schedule the federation executor
        List<FederationModel> registrations = getFederationRegistrations();
        if (registrations.size() > 0) {
            scheduledExecutor.schedule(new FederationPullExecutor(registrations), 1,
                    TimeUnit.MINUTES);
        }
    }
    /**
     * Returns the list of federated gitblit instances that this instance will
     * try to pull.
     *
     * @return list of registered gitblit instances
     */
    public List<FederationModel> getFederationRegistrations() {
        if (federationRegistrations.isEmpty()) {
            List<String> keys = settings.getAllKeys(Keys.federation._ROOT);
            keys.remove(Keys.federation.name);
            keys.remove(Keys.federation.uuid);
            keys.remove(Keys.federation.allowProposals);
            keys.remove(Keys.federation.proposalsFolder);
            keys.remove(Keys.federation.defaultFrequency);
            Collections.sort(keys);
            Map<String, FederationModel> federatedModels = new HashMap<String, FederationModel>();
            for (String key : keys) {
                String value = key.substring(Keys.federation._ROOT.length() + 1);
                List<String> values = StringUtils.getStringsFromValue(value, "\\.");
                String server = values.get(0);
                if (!federatedModels.containsKey(server)) {
                    federatedModels.put(server, new FederationModel(server));
                }
                String setting = values.get(1);
                if (setting.equals("url")) {
                    // url of the remote Gitblit instance
                    federatedModels.get(server).url = settings.getString(key, "");
                } else if (setting.equals("token")) {
                    // token for the remote Gitblit instance
                    federatedModels.get(server).token = settings.getString(key, "");
                } else if (setting.equals("frequency")) {
                    // frequency of the pull operation
                    federatedModels.get(server).frequency = settings.getString(key, "");
                } else if (setting.equals("folder")) {
                    // destination folder of the pull operation
                    federatedModels.get(server).folder = settings.getString(key, "");
                } else if (setting.equals("mergeAccounts")) {
                    // merge remote accounts into local accounts
                    federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false);
                } else if (setting.equals("sendStatus")) {
                    // send a status acknowledgment to source Gitblit instance
                    // at end of git pull
                    federatedModels.get(server).sendStatus = settings.getBoolean(key, false);
                } else if (setting.equals("notifyOnError")) {
                    // notify administrators on federation pull failures
                    federatedModels.get(server).notifyOnError = settings.getBoolean(key, false);
                } else if (setting.equals("exclude")) {
                    // excluded repositories
                    federatedModels.get(server).exclusions = settings.getStrings(key);
                } else if (setting.equals("include")) {
                    // included repositories
                    federatedModels.get(server).inclusions = settings.getStrings(key);
                }
            }
            // verify that registrations have a url and a token
            for (FederationModel model : federatedModels.values()) {
                if (StringUtils.isEmpty(model.url)) {
                    logger.warn(MessageFormat.format(
                            "Dropping federation registration {0}. Missing url.", model.name));
                    continue;
                }
                if (StringUtils.isEmpty(model.token)) {
                    logger.warn(MessageFormat.format(
                            "Dropping federation registration {0}. Missing token.", model.name));
                    continue;
                }
                // set default frequency if unspecified
                if (StringUtils.isEmpty(model.frequency)) {
                    model.frequency = settings.getString(Keys.federation.defaultFrequency,
                            "60 mins");
                }
                federationRegistrations.add(model);
            }
        }
        return federationRegistrations;
    }
    /**
     * Retrieve the specified federation registration.
     *
     * @param name
     *            the name of the registration
     * @return a federation registration
     */
    public FederationModel getFederationRegistration(String url, String name) {
        // check registrations
        for (FederationModel r : getFederationRegistrations()) {
            if (r.name.equals(name) && r.url.equals(url)) {
                return r;
            }
        }
        // check the results
        for (FederationModel r : getFederationResultRegistrations()) {
            if (r.name.equals(name) && r.url.equals(url)) {
                return r;
            }
        }
        return null;
    }
    /**
     * Returns the list of possible federation tokens for this Gitblit instance.
     *
     * @return list of federation tokens
     */
    public List<String> getFederationTokens() {
        List<String> tokens = new ArrayList<String>();
        for (FederationToken type : FederationToken.values()) {
            tokens.add(getFederationToken(type));
        }
        return tokens;
    }
    /**
     * Returns the specified federation token for this Gitblit instance.
     *
     * @param type
     * @return a federation token
     */
    public String getFederationToken(FederationToken type) {
        String uuid = settings.getString(Keys.federation.uuid, "");
        return StringUtils.getSHA1(uuid + "-" + type.name());
    }
    /**
     * Compares the provided token with this Gitblit instance's tokens and
     * determines if the requested permission may be granted to the token.
     *
     * @param req
     * @param token
     * @return true if the request can be executed
     */
    public boolean validateFederationRequest(FederationRequest req, String token) {
        String all = getFederationToken(FederationToken.ALL);
        String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
        String jur = getFederationToken(FederationToken.REPOSITORIES);
        switch (req) {
        case PULL_REPOSITORIES:
            return token.equals(all) || token.equals(unr) || token.equals(jur);
        case PULL_USERS:
            return token.equals(all) || token.equals(unr);
        case PULL_SETTINGS:
            return token.equals(all);
        }
        return false;
    }
    /**
     * Acknowledge and cache the status of a remote Gitblit instance.
     *
     * @param identification
     *            the identification of the pulling Gitblit instance
     * @param registration
     *            the registration from the pulling Gitblit instance
     * @return true if acknowledged
     */
    public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
        // reset the url to the identification of the pulling Gitblit instance
        registration.url = identification;
        String id = identification;
        if (!StringUtils.isEmpty(registration.folder)) {
            id += "-" + registration.folder;
        }
        federationPullResults.put(id, registration);
        return true;
    }
    /**
     * Returns the list of registration results.
     *
     * @return the list of registration results
     */
    public List<FederationModel> getFederationResultRegistrations() {
        return new ArrayList<FederationModel>(federationPullResults.values());
    }
    /**
     * Submit a federation proposal. The proposal is cached locally and the
     * Gitblit administrator(s) are notified via email.
     *
     * @param proposal
     *            the proposal
     * @param gitblitUrl
     *            the url of your gitblit instance
     * @return true if the proposal was submitted
     */
    public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
        // convert proposal to json
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String json = gson.toJson(proposal);
        try {
            // make the proposals folder
            File proposalsFolder = new File(getString(Keys.federation.proposalsFolder, "proposals")
                    .trim());
            proposalsFolder.mkdirs();
            // cache json to a file
            File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
            com.gitblit.utils.FileUtils.writeContent(file, json);
        } catch (Exception e) {
            logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
        }
        // send an email, if possible
        try {
            Message message = mailExecutor.createMessageForAdministrators();
            if (message != null) {
                message.setSubject("Federation proposal from " + proposal.url);
                message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
                        + proposal.token);
                mailExecutor.queue(message);
            }
        } catch (Throwable t) {
            logger.error("Failed to notify administrators of proposal", t);
        }
        return true;
    }
    /**
     * Returns the list of pending federation proposals
     *
     * @return list of federation proposals
     */
    public List<FederationProposal> getPendingFederationProposals() {
        List<FederationProposal> list = new ArrayList<FederationProposal>();
        File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
        if (folder.exists()) {
            File[] files = folder.listFiles(new FileFilter() {
                @Override
                public boolean accept(File file) {
                    return file.isFile()
                            && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
                }
            });
            Gson gson = new Gson();
            for (File file : files) {
                String json = com.gitblit.utils.FileUtils.readContent(file, null);
                FederationProposal proposal = gson.fromJson(json, FederationProposal.class);
                list.add(proposal);
            }
        }
        return list;
    }
    /**
     * Returns the proposal identified by the supplied token.
     *
     * @param token
     * @return the specified proposal or null
     */
    public FederationProposal getPendingFederationProposal(String token) {
        List<FederationProposal> list = getPendingFederationProposals();
        for (FederationProposal proposal : list) {
            if (proposal.token.equals(token)) {
                return proposal;
            }
        }
        return null;
    }
    /**
     * Deletes a pending federation proposal.
     *
     * @param a
     *            proposal
     * @return true if the proposal was deleted
     */
    public boolean deletePendingFederationProposal(FederationProposal proposal) {
        File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
        File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
        return file.delete();
    }
    /**
     * Notify the administrators by email.
     *
     * @param subject
     * @param message
     */
    public void notifyAdministrators(String subject, String message) {
        try {
            Message mail = mailExecutor.createMessageForAdministrators();
            if (mail != null) {
                mail.setSubject(subject);
                mail.setText(message);
                mailExecutor.queue(mail);
            }
        } catch (MessagingException e) {
            logger.error("Messaging error", e);
        }
    }
    /**
     * Configure the Gitblit singleton with the specified settings source. This
     * source may be file settings (Gitblit GO) or may be web.xml settings
     * (Gitblit WAR).
@@ -746,6 +1155,13 @@
            loginService = new FileUserService(realmFile);
        }
        setUserService(loginService);
        configureFederation();
        mailExecutor = new MailExecutor(settings);
        if (mailExecutor.isReady()) {
            scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
        } else {
            logger.warn("Mail server is not properly configured.  Mail services disabled.");
        }
    }
    /**
@@ -770,5 +1186,6 @@
    @Override
    public void contextDestroyed(ServletContextEvent contextEvent) {
        logger.info("Gitblit context destroyed by servlet container.");
        scheduledExecutor.shutdownNow();
    }
}
src/com/gitblit/GitFilter.java
@@ -61,7 +61,7 @@
     * Analyze the url and returns the action of the request. Return values are
     * either "/git-receive-pack" or "/git-upload-pack".
     * 
     * @param url
     * @param serverUrl
     * @return action of the request
     */
    @Override
@@ -106,11 +106,12 @@
            // Git Servlet disabled
            return false;
        }
        if (repository.isFrozen || repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
        boolean readOnly = repository.isFrozen;
        if (readOnly || repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
            boolean authorizedUser = user.canAccessRepository(repository.name);
            if (action.equals(gitReceivePack)) {
                // Push request
                if (!repository.isFrozen && authorizedUser) {
                if (!readOnly && authorizedUser) {
                    // clone-restricted or push-authorized
                    return true;
                } else {
src/com/gitblit/IStoredSettings.java
@@ -86,7 +86,7 @@
        if (props.containsKey(name)) {
            String value = props.getProperty(name);
            if (!StringUtils.isEmpty(value)) {
                return Boolean.parseBoolean(value);
                return Boolean.parseBoolean(value.trim());
            }
        }
        return defaultValue;
@@ -107,7 +107,7 @@
            try {
                String value = props.getProperty(name);
                if (!StringUtils.isEmpty(value)) {
                    return Integer.parseInt(value);
                    return Integer.parseInt(value.trim());
                }
            } catch (NumberFormatException e) {
                logger.warn("Failed to parse integer for " + name + " using default of "
@@ -131,7 +131,7 @@
        if (props.containsKey(name)) {
            String value = props.getProperty(name);
            if (!StringUtils.isEmpty(value)) {
                return value.charAt(0);
                return value.trim().charAt(0);
            }
        }
        return defaultValue;
@@ -151,7 +151,7 @@
        if (props.containsKey(name)) {
            String value = props.getProperty(name);
            if (value != null) {
                return value;
                return value.trim();
            }
        }
        return defaultValue;
src/com/gitblit/Launcher.java
@@ -78,12 +78,15 @@
        if (jars.size() == 0) {
            for (String folder : folders) {
                File libFolder = new File(folder);
                System.err.println("Failed to find any JARs in " + libFolder.getPath());
                // this is a test of adding a comment
                // more really interesting things
                System.err.println("Failed to find any really cool JARs in " + libFolder.getPath());
            }
            System.exit(-1);
        } else {
            for (File jar : jars) {
                try {
                    jar.canRead();
                    addJarFile(jar);
                } catch (Throwable t) {
                    t.printStackTrace();
@@ -113,6 +116,7 @@
                }
            }
        }
        return jars;
    }
src/com/gitblit/MailExecutor.java
New file
@@ -0,0 +1,234 @@
/*
 * 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;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.utils.StringUtils;
/**
 * The mail executor handles sending email messages asynchronously from queue.
 *
 * @author James Moger
 *
 */
public class MailExecutor implements Runnable {
    private final Logger logger = LoggerFactory.getLogger(MailExecutor.class);
    private final Queue<Message> queue = new ConcurrentLinkedQueue<Message>();
    private final Set<Message> failures = Collections.synchronizedSet(new HashSet<Message>());
    private final Session session;
    private final IStoredSettings settings;
    public MailExecutor(IStoredSettings settings) {
        this.settings = settings;
        final String mailUser = settings.getString(Keys.mail.username, null);
        final String mailPassword = settings.getString(Keys.mail.password, null);
        boolean authenticate = !StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword);
        String server = settings.getString(Keys.mail.server, "");
        if (StringUtils.isEmpty(server)) {
            session = null;
            return;
        }
        int port = settings.getInteger(Keys.mail.port, 25);
        boolean isGMail = false;
        if (server.equals("smtp.gmail.com")) {
            port = 465;
            isGMail = true;
        }
        Properties props = new Properties();
        props.setProperty("mail.smtp.host", server);
        props.setProperty("mail.smtp.port", String.valueOf(port));
        props.setProperty("mail.smtp.auth", String.valueOf(authenticate));
        props.setProperty("mail.smtp.auths", String.valueOf(authenticate));
        if (isGMail) {
            props.setProperty("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.socketFactory.port", String.valueOf(port));
            props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            props.put("mail.smtp.socketFactory.fallback", "false");
        }
        if (!StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword)) {
            // SMTP requires authentication
            session = Session.getInstance(props, new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication passwordAuthentication = new PasswordAuthentication(
                            mailUser, mailPassword);
                    return passwordAuthentication;
                }
            });
        } else {
            // SMTP does not require authentication
            session = Session.getInstance(props);
        }
    }
    /**
     * Indicates if the mail executor can send emails.
     *
     * @return true if the mail executor is ready to send emails
     */
    public boolean isReady() {
        return session != null;
    }
    /**
     * Creates a message for the administrators.
     *
     * @returna message
     */
    public Message createMessageForAdministrators() {
        List<String> toAddresses = settings.getStrings(Keys.mail.adminAddresses);
        if (toAddresses.size() == 0) {
            logger.warn("Can not notify administrators because no email addresses are defined!");
            return null;
        }
        return createMessage(toAddresses);
    }
    /**
     * Create a message.
     *
     * @param toAddresses
     * @return a message
     */
    public Message createMessage(String... toAddresses) {
        return createMessage(Arrays.asList(toAddresses));
    }
    /**
     * Create a message.
     *
     * @param toAddresses
     * @return a message
     */
    public Message createMessage(List<String> toAddresses) {
        MimeMessage message = new MimeMessage(session);
        try {
            InternetAddress from = new InternetAddress(settings.getString(Keys.mail.fromAddress,
                    "gitblit@gitblit.com"), "Gitblit");
            message.setFrom(from);
            InternetAddress[] tos = new InternetAddress[toAddresses.size()];
            for (int i = 0; i < toAddresses.size(); i++) {
                tos[i] = new InternetAddress(toAddresses.get(i));
            }
            message.setRecipients(Message.RecipientType.TO, tos);
            message.setSentDate(new Date());
        } catch (Exception e) {
            logger.error("Failed to properly create message", e);
        }
        return message;
    }
    /**
     * Queue's an email message to be sent.
     *
     * @param message
     * @return true if the message was queued
     */
    public boolean queue(Message message) {
        if (!isReady()) {
            return false;
        }
        try {
            message.saveChanges();
        } catch (Throwable t) {
            logger.error("Failed to save changes to message!", t);
        }
        queue.add(message);
        return true;
    }
    @Override
    public void run() {
        if (!queue.isEmpty()) {
            if (session != null) {
                // send message via mail server
                Message message = null;
                while ((message = queue.peek()) != null) {
                    try {
                        if (settings.getBoolean(Keys.mail.debug, false)) {
                            logger.info("send: "
                                    + StringUtils.trimString(
                                            message.getSubject()
                                                    + " => "
                                                    + message.getRecipients(RecipientType.TO)[0]
                                                            .toString(), 60));
                        }
                        Transport.send(message);
                        queue.remove();
                        failures.remove(message);
                    } catch (Throwable e) {
                        if (!failures.contains(message)) {
                            logger.error("Failed to send message", e);
                            failures.add(message);
                        }
                    }
                }
            }
        } else {
            // log message to console and drop
            if (!queue.isEmpty()) {
                Message message = null;
                while ((message = queue.peek()) != null) {
                    try {
                        logger.info("drop: "
                                + StringUtils.trimString(
                                        (message.getSubject())
                                                + " => "
                                                + message.getRecipients(RecipientType.TO)[0]
                                                        .toString(), 60));
                        queue.remove();
                        failures.remove(message);
                    } catch (Throwable e) {
                        if (!failures.contains(message)) {
                            logger.error("Failed to remove message from queue");
                            failures.add(message);
                        }
                    }
                }
            }
        }
    }
}
src/com/gitblit/SyndicationServlet.java
@@ -26,10 +26,10 @@
import org.slf4j.LoggerFactory;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.SyndicationUtils;
import com.gitblit.wicket.WicketUtils;
/**
 * SyndicationServlet generates RSS 2.0 feeds and feed links.
@@ -116,7 +116,7 @@
            javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
            java.io.IOException {
        String hostURL = WicketUtils.getHostURL(request);
        String hostURL = HttpUtils.getHostURL(request);
        String url = request.getRequestURI().substring(request.getServletPath().length());
        if (url.charAt(0) == '/' && url.length() > 1) {
            url = url.substring(1);
src/com/gitblit/build/Build.java
@@ -81,6 +81,8 @@
        downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
        downloadFromApache(MavenObject.ROME, BuildType.RUNTIME);
        downloadFromApache(MavenObject.JDOM, BuildType.RUNTIME);
        downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
        downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
        downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME);
@@ -104,6 +106,8 @@
        downloadFromApache(MavenObject.JSCH, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.ROME, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.JDOM, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.GSON, BuildType.COMPILETIME);
        downloadFromApache(MavenObject.MAIL, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
        downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
@@ -442,6 +446,16 @@
                "a7ed425c4c46605b8f2bf2ee118c1609682f4f2c",
                "f3df91edccba2f07a0fced70887c2f7b7836cb75");
        public static final MavenObject GSON = new MavenObject("gson", "com/google/code/gson",
                "gson", "1.7.1", 174000, 142000, 247000,
                "0697e3a1fa094a983cd12f7f6f61abf9c6ea52e2",
                "51f6f78aec2d30d0c2bfb4a5f00d456a6f7a5e7e",
                "f0872fe17d484815328538b89909d5e46d85db74");
        public static final MavenObject MAIL = new MavenObject("javax.mail", "javax/mail", "mail",
                "1.4.3", 462000, 642000, 0, "8154bf8d666e6db154c548dc31a8d512c273f5ee",
                "5875e2729de83a4e46391f8f979ec8bd03810c10", null);
        public final String name;
        public final String group;
        public final String artifact;
src/com/gitblit/models/FederationModel.java
New file
@@ -0,0 +1,215 @@
/*
 * 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.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.gitblit.Constants.FederationPullStatus;
import com.gitblit.utils.StringUtils;
/**
 * Represents a federated server registration. Gitblit federation allows one
 * Gitblit instance to pull the repositories and configuration from another
 * Gitblit instance. This is a backup operation and can be considered something
 * like svn-sync.
 *
 */
public class FederationModel implements Serializable, Comparable<FederationModel> {
    private static final long serialVersionUID = 1L;
    public String name;
    public String url;
    public String token;
    public String frequency;
    public String folder;
    public boolean mergeAccounts;
    public boolean sendStatus;
    public boolean notifyOnError;
    public List<String> exclusions = new ArrayList<String>();
    public List<String> inclusions = new ArrayList<String>();
    public Date lastPull;
    public Date nextPull;
    private Map<String, FederationPullStatus> results = new ConcurrentHashMap<String, FederationPullStatus>();
    /**
     * The constructor for a remote server configuration.
     *
     * @param serverName
     */
    public FederationModel(String serverName) {
        this.name = serverName;
        this.lastPull = new Date(0);
        this.nextPull = new Date(0);
    }
    public boolean isIncluded(RepositoryModel repository) {
        // if exclusions has the all wildcard, then check for specific
        // inclusions
        if (exclusions.contains("*")) {
            for (String name : inclusions) {
                if (StringUtils.fuzzyMatch(repository.name, name)) {
                    results.put(repository.name, FederationPullStatus.PENDING);
                    return true;
                }
            }
            results.put(repository.name, FederationPullStatus.EXCLUDED);
            return false;
        }
        // named exclusions
        for (String name : exclusions) {
            if (StringUtils.fuzzyMatch(repository.name, name)) {
                results.put(repository.name, FederationPullStatus.EXCLUDED);
                return false;
            }
        }
        // included by default
        results.put(repository.name, FederationPullStatus.PENDING);
        return true;
    }
    /**
     * Updates the pull status of a particular repository in this federation
     * registration.
     *
     * @param repository
     * @param status
     */
    public void updateStatus(RepositoryModel repository, FederationPullStatus status) {
        if (!results.containsKey(repository)) {
            results.put(repository.name, FederationPullStatus.PENDING);
        }
        if (status != null) {
            results.put(repository.name, status);
        }
    }
    public List<RepositoryStatus> getStatusList() {
        List<RepositoryStatus> list = new ArrayList<RepositoryStatus>();
        for (Map.Entry<String, FederationPullStatus> entry : results.entrySet()) {
            list.add(new RepositoryStatus(entry.getKey(), entry.getValue()));
        }
        return list;
    }
    /**
     * Iterates over the current pull results and returns the lowest pull
     * status.
     *
     * @return the lowest pull status of the registration
     */
    public FederationPullStatus getLowestStatus() {
        if (results.size() == 0) {
            return FederationPullStatus.PENDING;
        }
        FederationPullStatus status = FederationPullStatus.PULLED;
        for (FederationPullStatus result : results.values()) {
            if (result.ordinal() < status.ordinal()) {
                status = result;
            }
        }
        return status;
    }
    /**
     * Returns true if this registration represents the result data sent by a
     * pulling Gitblit instance.
     *
     * @return true, if this is result data
     */
    public boolean isResultData() {
        return !url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://");
    }
    @Override
    public String toString() {
        return "Federated " + name + " (" + url + ")";
    }
    @Override
    public int compareTo(FederationModel o) {
        boolean r1 = isResultData();
        boolean r2 = o.isResultData();
        if ((r1 && r2) || (!r1 && !r2)) {
            // sort registrations and results by name
            return name.compareTo(o.name);
        }
        // sort registrations first
        if (r1) {
            return 1;
        }
        return -1;
    }
    /**
     * Class that encapsulates a point-in-time pull result.
     *
     */
    public static class RepositoryStatus implements Serializable, Comparable<RepositoryStatus> {
        private static final long serialVersionUID = 1L;
        public final String name;
        public final FederationPullStatus status;
        RepositoryStatus(String name, FederationPullStatus status) {
            this.name = name;
            this.status = status;
        }
        @Override
        public int compareTo(RepositoryStatus o) {
            if (status.equals(o.status)) {
                // sort root repositories first, alphabetically
                // then sort grouped repositories, alphabetically
                int s1 = name.indexOf('/');
                int s2 = o.name.indexOf('/');
                if (s1 == -1 && s2 == -1) {
                    // neither grouped
                    return name.compareTo(o.name);
                } else if (s1 > -1 && s2 > -1) {
                    // both grouped
                    return name.compareTo(o.name);
                } else if (s1 == -1) {
                    return -1;
                } else if (s2 == -1) {
                    return 1;
                }
                return 0;
            }
            return status.compareTo(o.status);
        }
    }
}
src/com/gitblit/models/FederationProposal.java
New file
@@ -0,0 +1,79 @@
/*
 * 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.util.Date;
import java.util.Map;
import com.gitblit.Constants.FederationToken;
/**
 * Represents a proposal from a Gitblit instance to pull its repositories.
 */
public class FederationProposal implements Serializable {
    private static final long serialVersionUID = 1L;
    public Date received;
    public String name;
    public String url;
    public FederationToken tokenType;
    public String token;
    public Map<String, RepositoryModel> repositories;
    /**
     * The constructor for a federation proposal.
     *
     * @param url
     *            the url of the source Gitblit instance
     * @param tokenType
     *            the type of token from the source Gitblit instance
     * @param token
     *            the federation token from the source Gitblit instance
     * @param repositories
     *            the map of repositories to be pulled from the source Gitblit
     *            instance keyed by the repository clone url
     */
    public FederationProposal(String url, FederationToken tokenType, String token,
            Map<String, RepositoryModel> repositories) {
        this.received = new Date();
        this.url = url;
        this.tokenType = tokenType;
        this.token = token;
        this.repositories = repositories;
        try {
            // determine server name and set that as the proposal name
            name = url.substring(url.indexOf("//") + 2);
            if (name.contains("/")) {
                name = name.substring(0, name.indexOf('/'));
            }
            name = name.replace(".", "");
        } catch (Exception e) {
            name = Long.toHexString(System.currentTimeMillis());
        }
    }
    @Override
    public String toString() {
        return "Federation Proposal (" + url + ")";
    }
}
src/com/gitblit/models/RepositoryModel.java
@@ -19,6 +19,7 @@
import java.util.Date;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationStrategy;
/**
 * RepositoryModel is a serializable model class that represents a Gitblit
@@ -27,7 +28,7 @@
 * @author James Moger
 * 
 */
public class RepositoryModel implements Serializable {
public class RepositoryModel implements Serializable, Comparable<RepositoryModel> {
    private static final long serialVersionUID = 1L;
@@ -43,6 +44,11 @@
    public AccessRestrictionType accessRestriction;
    public boolean isFrozen;
    public boolean showReadme;
    public FederationStrategy federationStrategy;
    public boolean isFederated;
    public String frequency;
    public String origin;
    public String size;
    public RepositoryModel() {
        this("", "", "", new Date(0));
@@ -60,4 +66,9 @@
    public String toString() {
        return name;
    }
    @Override
    public int compareTo(RepositoryModel o) {
        return name.compareTo(o.name);
    }
}
src/com/gitblit/models/UserModel.java
@@ -17,8 +17,8 @@
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
/**
 * UserModel is a serializable model class that represents a user and the user's
@@ -36,7 +36,8 @@
    public String username;
    public String password;
    public boolean canAdmin;
    public final List<String> repositories = new ArrayList<String>();
    public boolean excludeFromFederation;
    public final Set<String> repositories = new HashSet<String>();
    public UserModel(String username) {
        this.username = username;
src/com/gitblit/utils/FederationUtils.java
New file
@@ -0,0 +1,305 @@
/*
 * 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.utils;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletResponse;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.FederationServlet;
import com.gitblit.models.FederationModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
/**
 * Utility methods for federation functions.
 *
 * @author James Moger
 *
 */
public class FederationUtils {
    public static final String CHARSET;
    public static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
    }.getType();
    public static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
    }.getType();
    public static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
    }.getType();
    public static final Type RESULTS_TYPE = new TypeToken<List<FederationModel>>() {
    }.getType();
    private static final SSLContext SSL_CONTEXT;
    private static final DummyHostnameVerifier HOSTNAME_VERIFIER;
    static {
        SSLContext context = null;
        try {
            context = SSLContext.getInstance("SSL");
            context.init(null, new TrustManager[] { new DummyTrustManager() }, new SecureRandom());
        } catch (Throwable t) {
            t.printStackTrace();
        }
        SSL_CONTEXT = context;
        HOSTNAME_VERIFIER = new DummyHostnameVerifier();
        CHARSET = "UTF-8";
    }
    /**
     * Sends a federation proposal to the Gitblit instance at remoteUrl
     *
     * @param remoteUrl
     *            the remote Gitblit instance to send a federation proposal to
     * @param tokenType
     *            type of the provided federation token
     * @param myToken
     *            my federation token
     * @param myUrl
     *            my Gitblit url
     * @param myRepositories
     *            the repositories I want to share keyed by their clone url
     * @return true if the proposal was received
     */
    public static boolean propose(String remoteUrl, FederationToken tokenType, String myToken,
            String myUrl, Map<String, RepositoryModel> myRepositories) throws Exception {
        String url = FederationServlet.asFederationLink(remoteUrl, tokenType, myToken,
                FederationRequest.PROPOSAL, myUrl);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String json = gson.toJson(myRepositories);
        int status = writeJson(url, json);
        return status == HttpServletResponse.SC_OK;
    }
    /**
     * Retrieves a map of the repositories at the remote gitblit instance keyed
     * by the repository clone url.
     *
     * @param registration
     * @param checkExclusions
     *            should returned repositories remove registration exclusions
     * @return a map of cloneable repositories
     * @throws Exception
     */
    public static Map<String, RepositoryModel> getRepositories(FederationModel registration,
            boolean checkExclusions) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
                FederationRequest.PULL_REPOSITORIES);
        Map<String, RepositoryModel> models = readGson(url, REPOSITORIES_TYPE);
        if (checkExclusions) {
            Map<String, RepositoryModel> includedModels = new HashMap<String, RepositoryModel>();
            for (Map.Entry<String, RepositoryModel> entry : models.entrySet()) {
                if (registration.isIncluded(entry.getValue())) {
                    includedModels.put(entry.getKey(), entry.getValue());
                }
            }
            return includedModels;
        }
        return models;
    }
    /**
     * Tries to pull the gitblit user accounts from the remote gitblit instance.
     *
     * @param registration
     * @return a collection of UserModel objects
     * @throws Exception
     */
    public static Collection<UserModel> getUsers(FederationModel registration) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
                FederationRequest.PULL_USERS);
        Collection<UserModel> models = readGson(url, USERS_TYPE);
        return models;
    }
    /**
     * Tries to pull the gitblit server settings from the remote gitblit
     * instance.
     *
     * @param registration
     * @return a map of the remote gitblit settings
     * @throws Exception
     */
    public static Map<String, String> getSettings(FederationModel registration) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
                FederationRequest.PULL_SETTINGS);
        Map<String, String> settings = readGson(url, SETTINGS_TYPE);
        return settings;
    }
    /**
     * Send an status acknowledgment to the remote Gitblit server.
     *
     * @param identification
     *            identification of this pulling instance
     * @param registration
     *            the source Gitblit instance to receive an acknowledgment
     * @param results
     *            the results of your pull operation
     * @return true, if the remote Gitblit instance acknowledged your results
     * @throws Exception
     */
    public static boolean acknowledgeStatus(String identification, FederationModel registration)
            throws Exception {
        String url = FederationServlet.asFederationLink(registration.url, null, registration.token,
                FederationRequest.STATUS, identification);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String json = gson.toJson(registration);
        int status = writeJson(url, json);
        return status == HttpServletResponse.SC_OK;
    }
    /**
     * Reads a gson object from the specified url.
     *
     * @param url
     * @param type
     * @return
     * @throws Exception
     */
    public static <X> X readGson(String url, Type type) throws Exception {
        String json = readJson(url);
        if (StringUtils.isEmpty(json)) {
            return null;
        }
        Gson gson = new Gson();
        return gson.fromJson(json, type);
    }
    /**
     * Reads a JSON response.
     *
     * @param url
     * @return the JSON response as a string
     * @throws Exception
     */
    public static String readJson(String url) throws Exception {
        URL urlObject = new URL(url);
        URLConnection conn = urlObject.openConnection();
        conn.setRequestProperty("Accept-Charset", CHARSET);
        conn.setUseCaches(false);
        conn.setDoInput(true);
        if (conn instanceof HttpsURLConnection) {
            HttpsURLConnection secureConn = (HttpsURLConnection) conn;
            secureConn.setSSLSocketFactory(SSL_CONTEXT.getSocketFactory());
            secureConn.setHostnameVerifier(HOSTNAME_VERIFIER);
        }
        InputStream is = conn.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is, CHARSET));
        StringBuilder json = new StringBuilder();
        char[] buffer = new char[4096];
        int len = 0;
        while ((len = reader.read(buffer)) > -1) {
            json.append(buffer, 0, len);
        }
        is.close();
        return json.toString();
    }
    /**
     * Writes a JSON message to the specified url.
     *
     * @param url
     *            the url to write to
     * @param json
     *            the json message to send
     * @return the http request result code
     * @throws Exception
     */
    public static int writeJson(String url, String json) throws Exception {
        byte[] jsonBytes = json.getBytes(CHARSET);
        URL urlObject = new URL(url);
        URLConnection conn = urlObject.openConnection();
        conn.setRequestProperty("Content-Type", "text/plain;charset=" + CHARSET);
        conn.setRequestProperty("Content-Length", "" + jsonBytes.length);
        conn.setUseCaches(false);
        conn.setDoOutput(true);
        if (conn instanceof HttpsURLConnection) {
            HttpsURLConnection secureConn = (HttpsURLConnection) conn;
            secureConn.setSSLSocketFactory(SSL_CONTEXT.getSocketFactory());
            secureConn.setHostnameVerifier(HOSTNAME_VERIFIER);
        }
        // write json body
        OutputStream os = conn.getOutputStream();
        os.write(jsonBytes);
        os.close();
        int status = ((HttpURLConnection) conn).getResponseCode();
        return status;
    }
    /**
     * DummyTrustManager trusts all certificates.
     */
    private static class DummyTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] certs, String authType)
                throws CertificateException {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] certs, String authType)
                throws CertificateException {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }
    /**
     * Trusts all hostnames from a certificate, including self-signed certs.
     */
    private static class DummyHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }
}
src/com/gitblit/utils/HttpUtils.java
New file
@@ -0,0 +1,45 @@
/*
 * 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.utils;
import javax.servlet.http.HttpServletRequest;
/**
 * Collection of utility methods for http requests.
 *
 * @author James Moger
 *
 */
public class HttpUtils {
    /**
     * Returns the host URL based on the request.
     *
     * @param request
     * @return the host url
     */
    public static String getHostURL(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getScheme());
        sb.append("://");
        sb.append(request.getServerName());
        if ((request.getScheme().equals("http") && request.getServerPort() != 80)
                || (request.getScheme().equals("https") && request.getServerPort() != 443)) {
            sb.append(":" + request.getServerPort());
        }
        return sb.toString();
    }
}
src/com/gitblit/utils/JGitUtils.java
@@ -62,6 +62,7 @@
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.storage.file.FileRepository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -134,6 +135,14 @@
    }
    /**
     * Encapsulates the result of cloning or pulling from a repository.
     */
    public static class CloneResult {
        public FetchResult fetchResult;
        public boolean createdRepository;
    }
    /**
     * Clone or Fetch a repository. If the local repository does not exist,
     * clone is called. If the repository does exist, fetch is called. By
     * default the clone/fetch retrieves the remote heads, tags, and notes.
@@ -141,12 +150,29 @@
     * @param repositoriesFolder
     * @param name
     * @param fromUrl
     * @return FetchResult
     * @return CloneResult
     * @throws Exception
     */
    public static FetchResult cloneRepository(File repositoriesFolder, String name, String fromUrl)
    public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl)
            throws Exception {
        FetchResult result = null;
        return cloneRepository(repositoriesFolder, name, fromUrl, null);
    }
    /**
     * Clone or Fetch a repository. If the local repository does not exist,
     * clone is called. If the repository does exist, fetch is called. By
     * default the clone/fetch retrieves the remote heads, tags, and notes.
     *
     * @param repositoriesFolder
     * @param name
     * @param fromUrl
     * @param credentialsProvider
     * @return CloneResult
     * @throws Exception
     */
    public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl,
            CredentialsProvider credentialsProvider) throws Exception {
        CloneResult result = new CloneResult();
        if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) {
            name += Constants.DOT_GIT_EXT;
        }
@@ -154,7 +180,7 @@
        if (folder.exists()) {
            File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED);
            FileRepository repository = new FileRepository(gitDir);
            result = fetchRepository(repository);
            result.fetchResult = fetchRepository(credentialsProvider, repository);
            repository.close();
        } else {
            CloneCommand clone = new CloneCommand();
@@ -162,12 +188,16 @@
            clone.setCloneAllBranches(true);
            clone.setURI(fromUrl);
            clone.setDirectory(folder);
            if (credentialsProvider != null) {
                clone.setCredentialsProvider(credentialsProvider);
            }
            clone.call();
            // Now we have to fetch because CloneCommand doesn't fetch
            // refs/notes nor does it allow manual RefSpec.
            File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED);
            FileRepository repository = new FileRepository(gitDir);
            result = fetchRepository(repository);
            result.createdRepository = true;
            result.fetchResult = fetchRepository(credentialsProvider, repository);
            repository.close();
        }
        return result;
@@ -177,13 +207,14 @@
     * Fetch updates from the remote repository. If refSpecs is unspecifed,
     * remote heads, tags, and notes are retrieved.
     * 
     * @param credentialsProvider
     * @param repository
     * @param refSpecs
     * @return FetchResult
     * @throws Exception
     */
    public static FetchResult fetchRepository(Repository repository, RefSpec... refSpecs)
            throws Exception {
    public static FetchResult fetchRepository(CredentialsProvider credentialsProvider,
            Repository repository, RefSpec... refSpecs) throws Exception {
        Git git = new Git(repository);
        FetchCommand fetch = git.fetch();
        List<RefSpec> specs = new ArrayList<RefSpec>();
@@ -194,6 +225,9 @@
        } else {
            specs.addAll(Arrays.asList(refSpecs));
        }
        if (credentialsProvider != null) {
            fetch.setCredentialsProvider(credentialsProvider);
        }
        fetch.setRefSpecs(specs);
        FetchResult result = fetch.call();
        return result;
src/com/gitblit/utils/StringUtils.java
@@ -342,4 +342,57 @@
        }
        return strings;
    }
    /**
     * Validates that a name is composed of letters, digits, or limited other
     * characters.
     *
     * @param name
     * @return the first invalid character found or null if string is acceptable
     */
    public static Character findInvalidCharacter(String name) {
        char[] validChars = { '/', '.', '_', '-' };
        for (char c : name.toCharArray()) {
            if (!Character.isLetterOrDigit(c)) {
                boolean ok = false;
                for (char vc : validChars) {
                    ok |= c == vc;
                }
                if (!ok) {
                    return c;
                }
            }
        }
        return null;
    }
    /**
     * Simple fuzzy string comparison. This is a case-insensitive check. A
     * single wildcard * value is supported.
     *
     * @param value
     * @param pattern
     * @return true if the value matches the pattern
     */
    public static boolean fuzzyMatch(String value, String pattern) {
        if (value.equalsIgnoreCase(pattern)) {
            return true;
        }
        if (pattern.contains("*")) {
            boolean prefixMatches = false;
            boolean suffixMatches = false;
            int wildcard = pattern.indexOf('*');
            String prefix = pattern.substring(0, wildcard).toLowerCase();
            prefixMatches = value.toLowerCase().startsWith(prefix);
            if (pattern.length() > (wildcard + 1)) {
                String suffix = pattern.substring(wildcard + 1).toLowerCase();
                suffixMatches = value.toLowerCase().endsWith(suffix);
                return prefixMatches && suffixMatches;
            }
            return prefixMatches || suffixMatches;
        }
        return false;
    }
}
src/com/gitblit/utils/TimeUtils.java
@@ -18,6 +18,8 @@
import java.util.Calendar;
import java.util.Date;
import com.gitblit.models.FederationModel;
/**
 * Utility class of time functions.
 * 
@@ -238,4 +240,41 @@
            }
        }
    }
    /**
     * Convert a frequency string into minutes.
     *
     * @param frequency
     * @return minutes
     */
    public static int convertFrequencyToMinutes(String frequency) {
        // parse the frequency
        frequency = frequency.toLowerCase();
        int mins = 60;
        if (!StringUtils.isEmpty(frequency)) {
            try {
                String str;
                if (frequency.indexOf(' ') > -1) {
                    str = frequency.substring(0, frequency.indexOf(' ')).trim();
                } else {
                    str = frequency.trim();
                }
                mins = (int) Float.parseFloat(str);
            } catch (NumberFormatException e) {
            }
            if (mins < 5) {
                mins = 5;
            }
        }
        if (frequency.indexOf("day") > -1) {
            // convert to minutes
            mins *= 24 * 60;
        } else if (frequency.indexOf("hour") > -1) {
            // convert to minutes
            mins *= 60;
        } else if (frequency.indexOf("min") > -1) {
            // default mins
        }
        return mins;
    }
}
src/com/gitblit/wicket/GitBlitWebApp.java
@@ -32,6 +32,8 @@
import com.gitblit.wicket.pages.CommitDiffPage;
import com.gitblit.wicket.pages.CommitPage;
import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.FederationProposalPage;
import com.gitblit.wicket.pages.FederationRegistrationPage;
import com.gitblit.wicket.pages.HistoryPage;
import com.gitblit.wicket.pages.LogPage;
import com.gitblit.wicket.pages.LoginPage;
@@ -95,6 +97,10 @@
        mount("/docs", DocsPage.class, "r");
        mount("/markdown", MarkdownPage.class, "r", "h", "f");
        // federation urls
        mount("/proposal", FederationProposalPage.class, "t");
        mount("/registration", FederationRegistrationPage.class, "u", "n");
        // setup login/logout urls, if we are using authentication
        if (useAuthentication) {
            mount("/login", LoginPage.class);
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -101,4 +101,36 @@
gb.commitActivityAuthors = primary authors by commit activity
gb.feed = feed
gb.cancel = cancel
gb.changePassword = change password
gb.changePassword = change password
gb.isFederated = is federated
gb.federateThis = federate this repository
gb.federateOrigin = federate the origin
gb.excludeFromFederation = exclude from federation
gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this object
gb.tokens = federation tokens
gb.tokenAllDescription = federate repositories, users, & settings
gb.tokenUnrDescription = federate repositories & users
gb.tokenJurDescription = federate repositories
gb.federatedRepositoryDefinitions = repository definitions
gb.federatedUserDefinitions = user definitions
gb.federatedSettingDefinitions = setting definitions
gb.proposals = federation proposals
gb.received = received
gb.type = type
gb.token = token
gb.repositories = repositories
gb.proposal = proposal
gb.frequency = frequency
gb.folder = folder
gb.lastPull = last pull
gb.nextPull = next pull
gb.inclusions = inclusions
gb.exclusions = exclusions
gb.registration = registration
gb.registrations = federation registrations
gb.sendProposal send proposal
gb.status = status
gb.origin = origin
gb.federationStrategy = federation strategy
gb.federationRegistration = federation registration
gb.federationResults = federation pull results
src/com/gitblit/wicket/WicketUtils.java
@@ -39,9 +39,12 @@
import org.wicketstuff.googlecharts.AbstractChartData;
import org.wicketstuff.googlecharts.IChartData;
import com.gitblit.Constants.FederationPullStatus;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.FederationModel;
import com.gitblit.models.Metric;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.JGitUtils.SearchType;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
@@ -109,6 +112,28 @@
        return label;
    }
    public static ContextImage getPullStatusImage(String wicketId, FederationPullStatus status) {
        String filename = null;
        switch (status) {
        case PULLED:
            filename = "bullet_green.png";
            break;
        case SKIPPED:
            filename = "bullet_yellow.png";
            break;
        case FAILED:
            filename = "bullet_red.png";
            break;
        case EXCLUDED:
            filename = "bullet_white.png";
            break;
        case PENDING:
        default:
            filename = "bullet_black.png";
        }
        return WicketUtils.newImage(wicketId, filename, status.name());
    }
    public static ContextImage getFileImage(String wicketId, String filename) {
        filename = filename.toLowerCase();
        if (filename.endsWith(".java")) {
@@ -155,6 +180,17 @@
        return newImage(wicketId, "file_16x16.png");
    }
    public static ContextImage getRegistrationImage(String wicketId, FederationModel registration,
            Component c) {
        if (registration.isResultData()) {
            return WicketUtils.newImage(wicketId, "information_16x16.png",
                    c.getString("gb.federationResults"));
        } else {
            return WicketUtils.newImage(wicketId, "arrow_left.png",
                    c.getString("gb.federationRegistration"));
        }
    }
    public static ContextImage newClearPixel(String wicketId) {
        return newImage(wicketId, "pixel.png");
    }
@@ -181,19 +217,7 @@
    public static String getHostURL(Request request) {
        HttpServletRequest req = ((WebRequest) request).getHttpServletRequest();
        return getHostURL(req);
    }
    public static String getHostURL(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getScheme());
        sb.append("://");
        sb.append(request.getServerName());
        if ((request.getScheme().equals("http") && request.getServerPort() != 80)
                || (request.getScheme().equals("https") && request.getServerPort() != 443)) {
            sb.append(":" + request.getServerPort());
        }
        return sb.toString();
        return HttpUtils.getHostURL(req);
    }
    public static HeaderContributor syndicationDiscoveryLink(final String feedTitle,
@@ -213,6 +237,14 @@
            }
        });
    }
    public static PageParameters newTokenParameter(String token) {
        return new PageParameters("t=" + token);
    }
    public static PageParameters newRegistrationParameter(String url, String name) {
        return new PageParameters("u=" + url + ",n=" + name);
    }
    public static PageParameters newUsernameParameter(String username) {
        return new PageParameters("user=" + username);
@@ -220,6 +252,10 @@
    public static PageParameters newRepositoryParameter(String repositoryName) {
        return new PageParameters("r=" + repositoryName);
    }
    public static PageParameters newObjectParameter(String objectId) {
        return new PageParameters("h=" + objectId);
    }
    public static PageParameters newObjectParameter(String repositoryName, String objectId) {
@@ -324,14 +360,35 @@
        return params.getString("user", "");
    }
    public static String getToken(PageParameters params) {
        return params.getString("t", "");
    }
    public static String getUrlParameter(PageParameters params) {
        return params.getString("u", "");
    }
    public static String getNameParameter(PageParameters params) {
        return params.getString("n", "");
    }
    public static Label createDateLabel(String wicketId, Date date, TimeZone timeZone) {
        String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
        DateFormat df = new SimpleDateFormat(format);
        if (timeZone != null) {
            df.setTimeZone(timeZone);
        }
        String dateString = df.format(date);
        String title = TimeUtils.timeAgo(date);
        String dateString;
        if (date.getTime() == 0) {
            dateString = "--";
        } else {
            dateString = df.format(date);
        }
        String title = null;
        if (date.getTime() <= System.currentTimeMillis()) {
            // past
            title = TimeUtils.timeAgo(date);
        }
        if ((System.currentTimeMillis() - date.getTime()) < 10 * 24 * 60 * 60 * 1000L) {
            String tmp = dateString;
            dateString = title;
@@ -339,7 +396,9 @@
        }
        Label label = new Label(wicketId, dateString);
        WicketUtils.setCssClass(label, TimeUtils.timeAgoCss(date));
        WicketUtils.setHtmlTooltip(label, title);
        if (!StringUtils.isEmpty(title)) {
            WicketUtils.setHtmlTooltip(label, title);
        }
        return label;
    }
@@ -356,9 +415,15 @@
        } else {
            dateString = df.format(date);
        }
        String title = TimeUtils.timeAgo(date);
        String title = null;
        if (date.getTime() <= System.currentTimeMillis()) {
            // past
            title = TimeUtils.timeAgo(date);
        }
        Label label = new Label(wicketId, dateString);
        WicketUtils.setHtmlTooltip(label, title);
        if (!StringUtils.isEmpty(title)) {
            WicketUtils.setHtmlTooltip(label, title);
        }
        return label;
    }
src/com/gitblit/wicket/pages/BasePage.java
@@ -39,6 +39,7 @@
import com.gitblit.Constants;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.UserModel;
@@ -140,6 +141,24 @@
        }
        return map;
    }
    protected Map<FederationStrategy, String> getFederationTypes() {
        Map<FederationStrategy, String> map = new LinkedHashMap<FederationStrategy, String>();
        for (FederationStrategy type : FederationStrategy.values()) {
            switch (type) {
            case EXCLUDE:
                map.put(type, getString("gb.excludeFromFederation"));
                break;
            case FEDERATE_THIS:
                map.put(type, getString("gb.federateThis"));
                break;
            case FEDERATE_ORIGIN:
                map.put(type, getString("gb.federateOrigin"));
                break;
            }
        }
        return map;
    }
    protected TimeZone getTimeZone() {
        return GitBlit.getBoolean(Keys.web.useClientTimezone, false) ? GitBlitWebSession.get()
src/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -17,15 +17,17 @@
            <tbody>
                <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<i><wicket:message key="gb.nameDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input type="text" wicket:id="description" size="40" tabindex="2" /></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="3" /> &nbsp;<i><wicket:message key="gb.ownerDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="4" /> &nbsp;<i><wicket:message key="gb.useTicketsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="5" /> &nbsp;<i><wicket:message key="gb.useDocsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="6" /> &nbsp;<i><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showReadme" tabindex="7" /> &nbsp;<i><wicket:message key="gb.showReadmeDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select wicket:id="accessRestriction" tabindex="8" /></td></tr>
                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="9" /> &nbsp;<i><wicket:message key="gb.isFrozenDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.origin"></wicket:message></th><td class="edit"><input type="text" wicket:id="origin" size="80" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="4" /> &nbsp;<i><wicket:message key="gb.ownerDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="5" /> &nbsp;<i><wicket:message key="gb.useTicketsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="6" /> &nbsp;<i><wicket:message key="gb.useDocsDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="7" /> &nbsp;<i><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showReadme" tabindex="8" /> &nbsp;<i><wicket:message key="gb.showReadmeDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select wicket:id="accessRestriction" tabindex="9" /></td></tr>
                <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="10" /> &nbsp;<i><wicket:message key="gb.isFrozenDescription"></wicket:message></i></td></tr>
                <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select wicket:id="federationStrategy" tabindex="11" /></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>                
                <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="10" /> <input type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="11" /></td></tr>
                <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="12" /> <input type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="13" /></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -37,6 +37,7 @@
import org.apache.wicket.model.util.ListModel;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
@@ -122,19 +123,11 @@
                    }
                    // confirm valid characters in repository name
                    char[] validChars = { '/', '.', '_', '-' };
                    for (char c : repositoryModel.name.toCharArray()) {
                        if (!Character.isLetterOrDigit(c)) {
                            boolean ok = false;
                            for (char vc : validChars) {
                                ok |= c == vc;
                            }
                            if (!ok) {
                                error(MessageFormat.format(
                                        "Illegal character ''{0}'' in repository name!", c));
                                return;
                            }
                        }
                    Character c = StringUtils.findInvalidCharacter(repositoryModel.name);
                    if (c != null) {
                        error(MessageFormat.format("Illegal character ''{0}'' in repository name!",
                                c));
                        return;
                    }
                    // confirm access restriction selection
@@ -177,6 +170,18 @@
        form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
                .asList(AccessRestrictionType.values()), new AccessRestrictionRenderer()));
        form.add(new CheckBox("isFrozen"));
        // TODO enable origin definition
        form.add(new TextField<String>("origin").setEnabled(false/*isCreate*/));
        // federation strategies - remove ORIGIN choice if this repository has
        // no origin.
        List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
                Arrays.asList(FederationStrategy.values()));
        if (StringUtils.isEmpty(repositoryModel.origin)) {
            federationStrategies.remove(FederationStrategy.FEDERATE_ORIGIN);
        }
        form.add(new DropDownChoice<FederationStrategy>("federationStrategy", federationStrategies,
                new FederationTypeRenderer()));
        form.add(new CheckBox("useTickets"));
        form.add(new CheckBox("useDocs"));
        form.add(new CheckBox("showRemoteBranches"));
@@ -264,4 +269,25 @@
            return Integer.toString(index);
        }
    }
    private class FederationTypeRenderer implements IChoiceRenderer<FederationStrategy> {
        private static final long serialVersionUID = 1L;
        private final Map<FederationStrategy, String> map;
        public FederationTypeRenderer() {
            map = getFederationTypes();
        }
        @Override
        public String getDisplayValue(FederationStrategy type) {
            return map.get(type);
        }
        @Override
        public String getIdValue(FederationStrategy type, int index) {
            return Integer.toString(index);
        }
    }
}
src/com/gitblit/wicket/pages/EditUserPage.html
@@ -19,8 +19,9 @@
                <tr><th><wicket:message key="gb.password"></wicket:message></th><td class="edit"><input type="password" wicket:id="password" size="30" tabindex="2" /></td></tr>
                <tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
                <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<i><wicket:message key="gb.canAdminDescription"></wicket:message></i></td></tr>                
                <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<i><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></i></td></tr>
                <tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
                <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="7" /> <input type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="8" /></td></tr>
                <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="8" /> <input type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="9" /></td></tr>
            </tbody>
        </table>
    </form>    
src/com/gitblit/wicket/pages/EditUserPage.java
@@ -83,8 +83,8 @@
        }
        final String oldName = userModel.username;
        final Palette<String> repositories = new Palette<String>("repositories",
                new ListModel<String>(userModel.repositories), new CollectionModel<String>(repos),
                new ChoiceRenderer<String>("", ""), 10, false);
                new ListModel<String>(new ArrayList<String>(userModel.repositories)),
                new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
        Form<UserModel> form = new Form<UserModel>("editForm", model) {
            private static final long serialVersionUID = 1L;
@@ -172,6 +172,7 @@
        confirmPasswordField.setResetPassword(false);
        form.add(confirmPasswordField);
        form.add(new CheckBox("canAdmin"));
        form.add(new CheckBox("excludeFromFederation"));
        form.add(repositories);
        form.add(new Button("save"));
src/com/gitblit/wicket/pages/FederationProposalPage.html
New file
@@ -0,0 +1,27 @@
<!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="en"
      lang="en">
<body>
<wicket:extend>
    <div style="padding-top:20px"></div>
    <div style="text-align:center;" wicket:id="feedback">[Feedback Panel]</div>
    <!-- proposal info -->
    <table class="plain">
        <tr><th><wicket:message key="gb.url">url</wicket:message></th><td><span wicket:id="url">[url]</span></td></tr>
        <tr><th><wicket:message key="gb.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
        <tr><th><wicket:message key="gb.type">type</wicket:message></th><td><span wicket:id="tokenType">[token type]</span></td></tr>
        <tr><th><wicket:message key="gb.received">received</wicket:message></th><td><span wicket:id="received">[received]</span></td></tr>
        <tr><th valign="top"><wicket:message key="gb.proposal">proposal</wicket:message></th><td><span class="sha1" wicket:id="definition">[definition]</span></td></tr>
    </table>
    <div wicket:id="repositories"></div>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/FederationProposalPage.java
New file
@@ -0,0 +1,100 @@
/*
 * 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.wicket.pages;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import com.gitblit.Constants.FederationToken;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.RepositoriesPanel;
@RequiresAdminRole
public class FederationProposalPage extends BasePage {
    private final String PROPS_PATTERN = "{0} = {1}\n";
    private final String WEBXML_PATTERN = "\n<context-param>\n\t<param-name>{0}</param-name>\n\t<param-value>{1}</param-value>\n</context-param>\n";
    public FederationProposalPage(PageParameters params) {
        super(params);
        setupPage("", getString("gb.proposals"));
        setStatelessHint(true);
        final String token = WicketUtils.getToken(params);
        FederationProposal proposal = GitBlit.self().getPendingFederationProposal(token);
        if (proposal == null) {
            error("Could not find federation proposal!", true);
        }
        add(new Label("url", proposal.url));
        add(WicketUtils.createTimestampLabel("received", proposal.received, getTimeZone()));
        add(new Label("tokenType", proposal.tokenType.name()));
        add(new Label("token", proposal.token));
        boolean go = true;
        String p;
        if (GitBlit.isGO()) {
            // gitblit.properties definition
            p = PROPS_PATTERN;
        } else {
            // web.xml definition
            p = WEBXML_PATTERN;
        }
        // build proposed definition
        StringBuilder sb = new StringBuilder();
        sb.append(asParam(p, proposal.name, "url", proposal.url));
        sb.append(asParam(p, proposal.name, "token", proposal.token));
        if (FederationToken.USERS_AND_REPOSITORIES.equals(proposal.tokenType)
                || FederationToken.ALL.equals(proposal.tokenType)) {
            sb.append(asParam(p, proposal.name, "mergeAccounts", "false"));
        }
        sb.append(asParam(p, proposal.name, "frequency",
                GitBlit.getString(Keys.federation.defaultFrequency, "60 mins")));
        sb.append(asParam(p, proposal.name, "folder", proposal.name));
        sb.append(asParam(p, proposal.name, "sendStatus", "true"));
        sb.append(asParam(p, proposal.name, "notifyOnError", "true"));
        sb.append(asParam(p, proposal.name, "exclude", ""));
        sb.append(asParam(p, proposal.name, "include", ""));
        add(new Label("definition", StringUtils.breakLinesForHtml(StringUtils.escapeForHtml(sb
                .toString().trim(), true))).setEscapeModelStrings(false));
        List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(
                proposal.repositories.values());
        RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositories", false,
                repositories, getAccessRestrictions());
        add(repositoriesPanel);
    }
    private String asParam(String pattern, String name, String key, String value) {
        return MessageFormat.format(pattern, Keys.federation._ROOT + "." + name + "." + key, value);
    }
}
src/com/gitblit/wicket/pages/FederationRegistrationPage.html
New file
@@ -0,0 +1,45 @@
<!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="en"
      lang="en">
<body>
<wicket:extend>
    <div style="padding-top:20px"></div>
    <div style="text-align:center;" wicket:id="feedback">[Feedback Panel]</div>
    <!-- registration info -->
    <table class="plain">
        <tr><th><wicket:message key="gb.url">url</wicket:message></th><td><span wicket:id="url">[url]</span></td></tr>
        <tr><th></th><td><img style="border:0px;vertical-align:middle;" wicket:id="typeIcon" /> <span wicket:id="typeName">[url]</span></td></tr>
        <tr><th><wicket:message key="gb.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
        <tr><th><wicket:message key="gb.folder">folder</wicket:message></th><td><span wicket:id="folder">[folder]</span></td></tr>
        <tr><th><wicket:message key="gb.frequency">frequency</wicket:message></th><td><span wicket:id="frequency">[frequency]</span></td></tr>
        <tr><th><wicket:message key="gb.lastPull">lastPull</wicket:message></th><td><span wicket:id="lastPull">[lastPull]</span></td></tr>
        <tr><th><wicket:message key="gb.nextPull">nextPull</wicket:message></th><td><span wicket:id="nextPull">[nextPull]</span></td></tr>
        <tr><th valign="top"><wicket:message key="gb.exclusions">exclusions</wicket:message></th><td><span class="sha1" wicket:id="exclusions">[exclusions]</span></td></tr>
        <tr><th valign="top"><wicket:message key="gb.inclusions">inclusions</wicket:message></th><td><span class="sha1" wicket:id="inclusions">[inclusions]</span></td></tr>
    </table>
    <table class="repositories">
        <tr>
            <th class="left">
                <img style="vertical-align: top; border: 1px solid #888; background-color: white;" src="gitweb-favicon.png"/>
                <wicket:message key="gb.repositories">[repositories]</wicket:message>
            </th>
            <th class="right"><wicket:message key="gb.status">[status]</wicket:message></th>
        </tr>
        <tbody>
               <tr wicket:id="row">
                   <td class="left"><img style="border:0px;vertical-align:middle;" wicket:id="statusIcon" /><span wicket:id="name">[name]</span></td>
                   <td class="right"><span wicket:id="status">[status]</span></td>
               </tr>
        </tbody>
    </table>
</wicket:extend>
</body>
</html>
src/com/gitblit/wicket/pages/FederationRegistrationPage.java
New file
@@ -0,0 +1,105 @@
/*
 * 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.wicket.pages;
import java.util.Collections;
import java.util.List;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationModel.RepositoryStatus;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
public class FederationRegistrationPage extends BasePage {
    public FederationRegistrationPage(PageParameters params) {
        super(params);
        setupPage("", getString("gb.registrations"));
        final boolean showAdmin;
        if (GitBlit.getBoolean(Keys.web.authenticateAdminPages, true)) {
            boolean allowAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, false);
            showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
        } else {
            showAdmin = false;
        }
        setStatelessHint(true);
        String url = WicketUtils.getUrlParameter(params);
        String name = WicketUtils.getNameParameter(params);
        FederationModel registration = GitBlit.self().getFederationRegistration(url, name);
        if (registration == null) {
            error("Could not find federation registration!", true);
        }
        add(new Label("url", registration.url));
        add(WicketUtils.getRegistrationImage("typeIcon", registration, this));
        add(new Label("typeName", registration.isResultData() ? getString("gb.federationResults")
                : getString("gb.federationRegistration")));
        add(new Label("frequency", registration.frequency));
        add(new Label("folder", registration.folder));
        add(new Label("token", showAdmin ? registration.token : "--"));
        add(WicketUtils.createTimestampLabel("lastPull", registration.lastPull, getTimeZone()));
        add(WicketUtils.createTimestampLabel("nextPull", registration.nextPull, getTimeZone()));
        StringBuilder inclusions = new StringBuilder();
        for (String inc : registration.inclusions) {
            inclusions.append(inc).append("<br/>");
        }
        StringBuilder exclusions = new StringBuilder();
        for (String ex : registration.exclusions) {
            exclusions.append(ex).append("<br/>");
        }
        add(new Label("inclusions", inclusions.toString()).setEscapeModelStrings(false));
        add(new Label("exclusions", exclusions.toString()).setEscapeModelStrings(false));
        List<RepositoryStatus> list = registration.getStatusList();
        Collections.sort(list);
        DataView<RepositoryStatus> dataView = new DataView<RepositoryStatus>("row",
                new ListDataProvider<RepositoryStatus>(list)) {
            private static final long serialVersionUID = 1L;
            private int counter;
            @Override
            protected void onBeforeRender() {
                super.onBeforeRender();
                counter = 0;
            }
            public void populateItem(final Item<RepositoryStatus> item) {
                final RepositoryStatus entry = item.getModelObject();
                item.add(WicketUtils.getPullStatusImage("statusIcon", entry.status));
                item.add(new Label("name", entry.name));
                item.add(new Label("status", entry.status.name()));
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
        };
        add(dataView);
    }
}
src/com/gitblit/wicket/pages/LoginPage.java
@@ -69,6 +69,11 @@
                UserModel user = GitBlit.self().authenticate(username, password);
                if (user == null) {
                    error("Invalid username or password!");
                } else if (user.username.equals(Constants.FEDERATION_USER)) {
                    // disallow the federation user from logging in via the
                    // web ui
                    error("Invalid username or password!");
                    user = null;
                } else {
                    loginUser(user);
                }
src/com/gitblit/wicket/pages/RepositoriesPage.html
@@ -19,6 +19,12 @@
    <div wicket:id="repositoriesPanel">[repositories panel]</div>
    <div style="padding-top: 10px;"wicket:id="usersPanel">[users panel]</div>
    <div style="padding-top: 10px;"wicket:id="federationTokensPanel">[federation tokens panel]</div>
    <div style="padding-top: 10px;"wicket:id="federationProposalsPanel">[federation proposals panel]</div>
    <div style="padding-top: 10px;"wicket:id="federationRegistrationsPanel">[federation registrations panel]</div>
        
</wicket:extend>
</body>
src/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -19,6 +19,7 @@
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
@@ -30,6 +31,9 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.FederationProposalsPanel;
import com.gitblit.wicket.panels.FederationRegistrationsPanel;
import com.gitblit.wicket.panels.FederationTokensPanel;
import com.gitblit.wicket.panels.RepositoriesPanel;
import com.gitblit.wicket.panels.UsersPanel;
@@ -60,6 +64,14 @@
        String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
        if (!StringUtils.isEmpty(cachedMessage)) {
            error(cachedMessage);
        } else if (showAdmin) {
            int pendingProposals = GitBlit.self().getPendingFederationProposals().size();
            if (pendingProposals == 1) {
                info("There is 1 federation proposal awaiting review.");
            } else if (pendingProposals > 1) {
                info(MessageFormat.format("There are {0} federation proposals awaiting review.",
                        pendingProposals));
            }
        }
        // Load the markdown welcome message
@@ -97,7 +109,28 @@
        Component repositoriesMessage = new Label("repositoriesMessage", message)
                .setEscapeModelStrings(false);
        add(repositoriesMessage);
        add(new RepositoriesPanel("repositoriesPanel", showAdmin, getAccessRestrictions()));
        add(new RepositoriesPanel("repositoriesPanel", showAdmin, null, getAccessRestrictions()));
        add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
        boolean showFederation = showAdmin && GitBlit.canFederate();
        add(new FederationTokensPanel("federationTokensPanel", showFederation)
                .setVisible(showFederation));
        FederationProposalsPanel proposalsPanel = new FederationProposalsPanel(
                "federationProposalsPanel");
        if (showFederation) {
            proposalsPanel.hideIfEmpty();
        } else {
            proposalsPanel.setVisible(false);
        }
        boolean showRegistrations = GitBlit.getBoolean(Keys.web.showFederationRegistrations, false);
        FederationRegistrationsPanel registrationsPanel = new FederationRegistrationsPanel(
                "federationRegistrationsPanel");
        if (showAdmin || showRegistrations) {
            registrationsPanel.hideIfEmpty();
        } else {
            registrationsPanel.setVisible(false);
        }
        add(proposalsPanel);
        add(registrationsPanel);
    }
}
src/com/gitblit/wicket/panels/BasePanel.java
@@ -67,4 +67,23 @@
            return result;
        }
    }
    public static class JavascriptTextPrompt extends AttributeModifier {
        private static final long serialVersionUID = 1L;
        public JavascriptTextPrompt(String event, String msg) {
            super(event, true, new Model<String>(msg));
        }
        protected String newValue(final String currentValue, final String message) {
            String result = "var userText = prompt('" + message + "','"
                    + (currentValue == null ? "" : currentValue) + "'); " + "return userText; ";
            // String result = prefix;
            // if (currentValue != null) {
            // result = prefix + currentValue;
            // }
            return result;
        }
    }
}
src/com/gitblit/wicket/panels/FederationProposalsPanel.html
New file
@@ -0,0 +1,34 @@
<!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="en"
      lang="en">
<body>
<wicket:panel>
        <table class="repositories">
        <tr>
            <th class="left">
                <img style="vertical-align: top; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
                <wicket:message key="gb.proposals">[proposals]</wicket:message>
            </th>
            <th><wicket:message key="gb.received">[received]</wicket:message></th>
            <th><wicket:message key="gb.type">[type]</wicket:message></th>
            <th><wicket:message key="gb.token">[token]</wicket:message></th>
            <th class="right"></th>
        </tr>
        <tbody>
               <tr wicket:id="row">
                   <td class="left"><span class="list" wicket:id="url">[field]</span></td>
                   <td><span class="date"" wicket:id="received">[received]</span></td>
                   <td><span wicket:id="tokenType">[token type]</span></td>
                   <td><span class="sha1"" wicket:id="token">[token]</span></td>
                   <td class="rightAlign"><span class="link"><a wicket:id="deleteProposal"><wicket:message key="gb.delete">[delete]</wicket:message></a></span></td>
               </tr>
        </tbody>
    </table>
</wicket:panel>
</body>
</html>
src/com/gitblit/wicket/panels/FederationProposalsPanel.java
New file
@@ -0,0 +1,92 @@
/*
 * 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.wicket.panels;
import java.text.MessageFormat;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.GitBlit;
import com.gitblit.models.FederationProposal;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.FederationProposalPage;
public class FederationProposalsPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final boolean hasProposals;
    public FederationProposalsPanel(String wicketId) {
        super(wicketId);
        final List<FederationProposal> list = GitBlit.self().getPendingFederationProposals();
        hasProposals = list.size() > 0;
        DataView<FederationProposal> dataView = new DataView<FederationProposal>("row",
                new ListDataProvider<FederationProposal>(list)) {
            private static final long serialVersionUID = 1L;
            private int counter;
            @Override
            protected void onBeforeRender() {
                super.onBeforeRender();
                counter = 0;
            }
            public void populateItem(final Item<FederationProposal> item) {
                final FederationProposal entry = item.getModelObject();
                item.add(new LinkPanel("url", "list", entry.url, FederationProposalPage.class,
                        WicketUtils.newTokenParameter(entry.token)));
                item.add(WicketUtils.createDateLabel("received", entry.received, getTimeZone()));
                item.add(new Label("tokenType", entry.tokenType.name()));
                item.add(new LinkPanel("token", "list", entry.token, FederationProposalPage.class,
                        WicketUtils.newTokenParameter(entry.token)));
                Link<Void> deleteLink = new Link<Void>("deleteProposal") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick() {
                        if (GitBlit.self().deletePendingFederationProposal(entry)) {
                            list.remove(entry);
                            info(MessageFormat.format("Proposal ''{0}'' deleted.", entry.name));
                        } else {
                            error(MessageFormat.format("Failed to delete proposal ''{0}''!",
                                    entry.name));
                        }
                    }
                };
                deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
                        "Delete proposal \"{0}\"?", entry.name)));
                item.add(deleteLink);
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
        };
        add(dataView);
    }
    public Component hideIfEmpty() {
        return super.setVisible(hasProposals);
    }
}
src/com/gitblit/wicket/panels/FederationRegistrationsPanel.html
New file
@@ -0,0 +1,38 @@
<!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="en"
      lang="en">
<body>
<wicket:panel>
    <table class="repositories">
        <tr>
            <th class="left">
                <img style="vertical-align: top; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
                <wicket:message key="gb.registrations">[registrations]</wicket:message>
            </th>
            <th><wicket:message key="gb.name">[name]</wicket:message></th>
            <th><wicket:message key="gb.frequency">[frequency]</wicket:message></th>
            <th></th>
            <th><wicket:message key="gb.lastPull">[lastPull]</wicket:message></th>
            <th><wicket:message key="gb.nextPull">[nextPull]</wicket:message></th>
            <th class="right"></th>
        </tr>
        <tbody>
               <tr wicket:id="row">
                   <td class="left"><img style="border:0px;vertical-align:middle;" wicket:id="statusIcon" /><span class="list" wicket:id="url">[url]</span></td>
                   <td><span class="list" wicket:id="name">[name]</span></td>
                   <td><span wicket:id="frequency">[frequency]</span></td>
                   <td><img style="border:0px;vertical-align:middle;" wicket:id="typeIcon" /></td>
                   <td><span class="date"" wicket:id="lastPull">[lastPull]</span></td>
                   <td><span class="date"" wicket:id="nextPull">[nextPull]</span></td>
                   <td class="rightAlign"><span class="link"></span></td>
               </tr>
        </tbody>
    </table>
</wicket:panel>
</body>
</html>
src/com/gitblit/wicket/panels/FederationRegistrationsPanel.java
New file
@@ -0,0 +1,83 @@
/*
 * 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.wicket.panels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.GitBlit;
import com.gitblit.models.FederationModel;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.FederationRegistrationPage;
public class FederationRegistrationsPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final boolean hasRegistrations;
    public FederationRegistrationsPanel(String wicketId) {
        super(wicketId);
        final List<FederationModel> list = new ArrayList<FederationModel>(GitBlit.self()
                .getFederationRegistrations());
        list.addAll(GitBlit.self().getFederationResultRegistrations());
        Collections.sort(list);
        hasRegistrations = list.size() > 0;
        DataView<FederationModel> dataView = new DataView<FederationModel>("row",
                new ListDataProvider<FederationModel>(list)) {
            private static final long serialVersionUID = 1L;
            private int counter;
            @Override
            protected void onBeforeRender() {
                super.onBeforeRender();
                counter = 0;
            }
            public void populateItem(final Item<FederationModel> item) {
                final FederationModel entry = item.getModelObject();
                item.add(new LinkPanel("url", "list", entry.url, FederationRegistrationPage.class,
                        WicketUtils.newRegistrationParameter(entry.url, entry.name)));
                item.add(WicketUtils.getPullStatusImage("statusIcon", entry.getLowestStatus()));
                item.add(new LinkPanel("name", "list", entry.name,
                        FederationRegistrationPage.class, WicketUtils.newRegistrationParameter(
                                entry.url, entry.name)));
                item.add(WicketUtils.getRegistrationImage("typeIcon", entry, this));
                item.add(WicketUtils.createDateLabel("lastPull", entry.lastPull, getTimeZone()));
                item.add(WicketUtils
                        .createTimestampLabel("nextPull", entry.nextPull, getTimeZone()));
                item.add(new Label("frequency", entry.frequency));
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
        };
        add(dataView);
    }
    public Component hideIfEmpty() {
        return super.setVisible(hasRegistrations);
    }
}
src/com/gitblit/wicket/panels/FederationTokensPanel.html
New file
@@ -0,0 +1,38 @@
<!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="en"
      lang="en">
<body>
<wicket:panel>
        <div class="admin_nav">
            <a wicket:id="federatedRepositories"><wicket:message key="gb.federatedRepositoryDefinitions">[repositories]</wicket:message></a>
             | <a wicket:id="federatedUsers"><wicket:message key="gb.federatedUserDefinitions">[users]</wicket:message></a>
             | <a wicket:id="federatedSettings"><wicket:message key="gb.federatedSettingDefinitions">[settings]</wicket:message></a>
        </div>
        <table class="repositories">
        <tr>
            <th class="left">
                <img style="vertical-align: top; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
                <wicket:message key="gb.tokens">[tokens]</wicket:message>
            </th>
            <th></th>
            <th></th>
            <th class="right"></th>
        </tr>
        <tbody>
               <tr wicket:id="row">
                   <td class="left"><span class="list" wicket:id="field">[field]</span></td>
                   <td><span class="sha1"" wicket:id="value">[value]</span></td>
                   <td><span wicket:id="description"></span></td>
                   <td class="rightAlign"><span class="link"><a wicket:id="send"><wicket:message key="gb.sendProposal">[send proposal]</wicket:message></a></span></td>
               </tr>
        </tbody>
    </table>
</wicket:panel>
</body>
</html>
src/com/gitblit/wicket/panels/FederationTokensPanel.java
New file
@@ -0,0 +1,109 @@
/*
 * 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.wicket.panels;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.FederationServlet;
import com.gitblit.GitBlit;
import com.gitblit.wicket.WicketUtils;
public class FederationTokensPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    public FederationTokensPanel(String wicketId, final boolean showFederation) {
        super(wicketId);
        String baseUrl = getRequest().getRelativePathPrefixToContextRoot();
        add(new ExternalLink("federatedRepositories", FederationServlet.asPullLink(baseUrl, GitBlit
                .self().getFederationToken(FederationToken.REPOSITORIES),
                FederationRequest.PULL_REPOSITORIES)));
        add(new ExternalLink("federatedUsers", FederationServlet.asPullLink(baseUrl, GitBlit.self()
                .getFederationToken(FederationToken.USERS_AND_REPOSITORIES),
                FederationRequest.PULL_USERS)));
        add(new ExternalLink("federatedSettings", FederationServlet.asPullLink(baseUrl, GitBlit
                .self().getFederationToken(FederationToken.ALL), FederationRequest.PULL_SETTINGS)));
        final List<String[]> data = new ArrayList<String[]>();
        for (FederationToken token : FederationToken.values()) {
            data.add(new String[] { token.name(), GitBlit.self().getFederationToken(token) });
        }
        DataView<String[]> dataView = new DataView<String[]>("row", new ListDataProvider<String[]>(
                data)) {
            private static final long serialVersionUID = 1L;
            private int counter;
            @Override
            protected void onBeforeRender() {
                super.onBeforeRender();
                counter = 0;
            }
            public void populateItem(final Item<String[]> item) {
                final String[] entry = item.getModelObject();
                final FederationToken token = FederationToken.fromName(entry[0]);
                item.add(new Label("field", entry[0]));
                item.add(new Label("value", entry[1]));
                // TODO make this work
                Link<Void> sendProposal = new Link<Void>("send") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick() {
                        error("Sorry, this does not work yet.  :(");
                    }
                };
                sendProposal.add(new JavascriptTextPrompt("onclick",
                        "Please enter URL for remote Gitblit instance:"));
                item.add(sendProposal);
                item.add(new Label("description", describeToken(token)));
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
        };
        add(dataView.setVisible(showFederation));
    }
    private String describeToken(FederationToken token) {
        switch (token) {
        case ALL:
            return getString("gb.tokenAllDescription");
        case USERS_AND_REPOSITORIES:
            return getString("gb.tokenUnrDescription");
        case REPOSITORIES:
        default:
            return getString("gb.tokenJurDescription");
        }
    }
}
src/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -74,7 +74,7 @@
        <td class="left"><div class="list" wicket:id="repositoryName">[repository name]</div></td>
        <td><div class="list" wicket:id="repositoryDescription">[repository description]</div></td>
        <td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
        <td style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
        <td style="text-align: right;padding-right:10px;"><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 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
@@ -60,11 +60,29 @@
    private static final long serialVersionUID = 1L;
    public RepositoriesPanel(String wicketId, final boolean showAdmin,
            List<RepositoryModel> models,
            final Map<AccessRestrictionType, String> accessRestrictionTranslations) {
        super(wicketId);
        final boolean linksActive;
        final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
        final UserModel user = GitBlitWebSession.get().getUser();
        List<RepositoryModel> models = GitBlit.self().getRepositoryModels(user);
        if (models == null) {
            linksActive = true;
            models = GitBlit.self().getRepositoryModels(user);
            final ByteFormat byteFormat = new ByteFormat();
            if (showSize) {
                for (RepositoryModel model : models) {
                    model.size = byteFormat.format(GitBlit.self().calculateSize(model));
                }
            }
        } else {
            // disable links if the repositories are already provided
            // the repositories are most likely from a proposal
            linksActive = false;
        }
        final IDataProvider<RepositoryModel> dp;
        Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
@@ -100,6 +118,7 @@
            for (String root : roots) {
                List<RepositoryModel> subModels = groups.get(root);
                groupedModels.add(new GroupRepositoryModel(root, subModels.size()));
                Collections.sort(subModels);
                groupedModels.addAll(subModels);
            }
            dp = new RepositoriesProvider(groupedModels);
@@ -107,8 +126,6 @@
            dp = new SortableRepositoriesProvider(models);
        }
        final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
        final ByteFormat byteFormat = new ByteFormat();
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
            private static final long serialVersionUID = 1L;
            int counter;
@@ -130,23 +147,27 @@
                }
                Fragment row = new Fragment("rowContent", "repositoryRow", this);
                item.add(row);
                if (entry.hasCommits) {
                    // Existing repository
                if (entry.hasCommits && linksActive) {
                    PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
                    row.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class,
                            pp));
                    row.add(new LinkPanel("repositoryDescription", "list", entry.description,
                            SummaryPage.class, pp));
                } else {
                    // new/empty repository OR proposed repository
                    row.add(new Label("repositoryName", entry.name));
                    row.add(new Label("repositoryDescription", entry.description));
                }
                if (entry.hasCommits) {
                    // Existing repository
                    if (showSize) {
                        row.add(new Label("repositorySize", byteFormat.format(GitBlit.self()
                                .calculateSize(entry))));
                        row.add(new Label("repositorySize", entry.size));
                    } else {
                        row.add(new Label("repositorySize").setVisible(false));
                    }
                } else {
                    // New repository
                    row.add(new Label("repositoryName", entry.name));
                    row.add(new Label("repositoryDescription", entry.description));
                    row.add(new Label("repositorySize", "<span class='empty'>(empty)</span>")
                            .setEscapeModelStrings(false));
                }
@@ -170,6 +191,13 @@
                            getString("gb.isFrozen")));
                } else {
                    row.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
                }
                if (entry.isFederated) {
                    row.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png",
                            getString("gb.isFederated")));
                } else {
                    row.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
                }
                switch (entry.accessRestriction) {
                case NONE:
@@ -244,7 +272,8 @@
                    row.add(new Label("repositoryLinks"));
                }
                row.add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
                        .getRelativePathPrefixToContextRoot(), entry.name, null, 0)));
                        .getRelativePathPrefixToContextRoot(), entry.name, null, 0))
                        .setVisible(linksActive));
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
            }
tests/com/gitblit/tests/FederationTests.java
New file
@@ -0,0 +1,105 @@
/*
 * Copyright 2011 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.tests;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import junit.framework.TestCase;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationToken;
import com.gitblit.FederationServlet;
import com.gitblit.GitBlitServer;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FederationUtils;
import com.google.gson.Gson;
public class FederationTests extends TestCase {
    int port = 8180;
    int shutdownPort = 8181;
    @Override
    protected void setUp() throws Exception {
        // Start a Gitblit instance
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            public void run() {
                GitBlitServer.main("--httpPort", "" + port, "--httpsPort", "0", "--shutdownPort",
                        "" + shutdownPort, "--repositoriesFolder",
                        "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService",
                        "distrib/users.properties");
            }
        });
        // Wait a few seconds for it to be running
        Thread.sleep(2500);
    }
    @Override
    protected void tearDown() throws Exception {
        // Stop Gitblit
        GitBlitServer.main("--stop", "--shutdownPort", "" + shutdownPort);
        // Wait a few seconds for it to be running
        Thread.sleep(2500);
    }
    public void testDeserialization() throws Exception {
        String json = "{\"https://localhost:8443/git/a.b.c.orphan.git\":{\"name\":\"a.b.c.orphan.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 22, 2011 3:15:07 PM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/test/jgit.git\":{\"name\":\"test/jgit.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 13, 2011 9:42:33 AM\",\"hasCommits\":true,\"showRemoteBranches\":true,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/test/helloworld.git\":{\"name\":\"test/helloworld.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 15, 2008 7:26:48 PM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/working/ticgit\":{\"name\":\"working/ticgit\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 22, 2011 10:35:27 AM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/ticgit.git\":{\"name\":\"ticgit.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 22, 2011 10:35:27 AM\",\"hasCommits\":true,\"showRemoteBranches\":true,\"useTickets\":true,\"useDocs\":true,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/helloworld.git\":{\"name\":\"helloworld.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 15, 2008 7:26:48 PM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/test/helloworld3.git\":{\"name\":\"test/helloworld3.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Jul 15, 2008 7:26:48 PM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false},\"https://localhost:8443/git/test/bluez-gnome.git\":{\"name\":\"test/bluez-gnome.git\",\"description\":\"\",\"owner\":\"\",\"lastChange\":\"Dec 19, 2008 6:35:33 AM\",\"hasCommits\":true,\"showRemoteBranches\":false,\"useTickets\":false,\"useDocs\":false,\"accessRestriction\":\"NONE\",\"isFrozen\":false,\"showReadme\":false,\"isFederated\":false}}";
        Gson gson = new Gson();
        Map<String, RepositoryModel> models = gson
                .fromJson(json, FederationUtils.REPOSITORIES_TYPE);
        assertEquals(8, models.size());
    }
    public void testProposal() throws Exception {
        // create dummy repository data
        Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
        for (int i = 0; i < 5; i++) {
            RepositoryModel model = new RepositoryModel();
            model.accessRestriction = AccessRestrictionType.VIEW;
            model.description = "cloneable repository " + i;
            model.lastChange = new Date();
            model.owner = "adminuser";
            model.name = "repo" + i + ".git";
            model.size = "5 MB";
            model.hasCommits = true;
            repositories.put(model.name, model);
        }
        // propose federation
        assertTrue("proposal refused", FederationUtils.propose("http://localhost:" + port,
                FederationToken.ALL, "testtoken", "http://testurl", repositories));
    }
    public void testPullRepositories() throws Exception {
        try {
            String url = FederationServlet.asPullLink("http://localhost:" + port, "testtoken",
                    FederationRequest.PULL_REPOSITORIES);
            String json = FederationUtils.readJson(url);
        } catch (IOException e) {
            if (!e.getMessage().contains("403")) {
                throw e;
            }
        }
    }
}
tests/com/gitblit/tests/MarkdownUtilsTest.java
@@ -26,10 +26,14 @@
    public void testMarkdown() throws Exception {
        assertEquals("<h1> H1</h1>", MarkdownUtils.transformMarkdown("# H1"));
        assertEquals("<h2> H2</h2>", MarkdownUtils.transformMarkdown("## H2"));
        assertEquals("<p><strong>THIS</strong> is a test</p>", MarkdownUtils.transformMarkdown("**THIS** is a test"));
        assertEquals("<p>** THIS ** is a test</p>", MarkdownUtils.transformMarkdown("** THIS ** is a test"));
        assertEquals("<p>**THIS ** is a test</p>", MarkdownUtils.transformMarkdown("**THIS ** is a test"));
        assertEquals("<p>** THIS** is a test</p>", MarkdownUtils.transformMarkdown("** THIS** is a test"));
        assertEquals("<p><strong>THIS</strong> is a test</p>",
                MarkdownUtils.transformMarkdown("**THIS** is a test"));
        assertEquals("<p>** THIS ** is a test</p>",
                MarkdownUtils.transformMarkdown("** THIS ** is a test"));
        assertEquals("<p>**THIS ** is a test</p>",
                MarkdownUtils.transformMarkdown("**THIS ** is a test"));
        assertEquals("<p>** THIS** is a test</p>",
                MarkdownUtils.transformMarkdown("** THIS** is a test"));
        try {
            MarkdownUtils.transformMarkdown((String) null);
            assertTrue(false);
tests/com/gitblit/tests/StringUtilsTest.java
@@ -103,4 +103,22 @@
        assertTrue(strings.get(2).equals("C"));
        assertTrue(strings.get(3).equals("D"));
    }
    public void testStringsFromValue2() throws Exception {
        List<String> strings = StringUtils.getStringsFromValue("common/* libraries/*");
        assertTrue(strings.size() == 2);
        assertTrue(strings.get(0).equals("common/*"));
        assertTrue(strings.get(1).equals("libraries/*"));
    }
    public void testFuzzyMatching() throws Exception {
        assertTrue(StringUtils.fuzzyMatch("12345", "12345"));
        assertTrue(StringUtils.fuzzyMatch("AbCdEf", "abcdef"));
        assertTrue(StringUtils.fuzzyMatch("AbCdEf", "abc*"));
        assertTrue(StringUtils.fuzzyMatch("AbCdEf", "*def"));
        assertTrue(StringUtils.fuzzyMatch("AbCdEfHIJ", "abc*hij"));
        assertFalse(StringUtils.fuzzyMatch("123", "12345"));
        assertFalse(StringUtils.fuzzyMatch("AbCdEfHIJ", "abc*hhh"));
    }
}