James Moger
2011-09-25 dd9ae71bc1cb13b90dcc8d9689550eb7dfe7d035
Revised federation proposal mechanism. Added SendProposalPage.
2 files added
2 files renamed
11 files modified
432 ■■■■ changed files
src/com/gitblit/FederationServlet.java 88 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 80 ●●●●● patch | view | raw | blame | history
src/com/gitblit/models/FederationProposal.java 5 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/FederationUtils.java 28 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationRegistrationPage.html 3 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/FederationRegistrationPage.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ReviewProposalPage.html 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ReviewProposalPage.java 7 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SendProposalPage.html 30 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/SendProposalPage.java 127 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationProposalsPanel.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/FederationTokensPanel.java 24 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/FederationTests.java 10 ●●●● patch | view | raw | blame | history
src/com/gitblit/FederationServlet.java
@@ -35,7 +35,6 @@
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;
@@ -68,7 +67,7 @@
     * @param req
     *            the pull type request
     */
    public static String asPullLink(String sourceURL, String token, FederationRequest req) {
    public static String asFederationLink(String sourceURL, String token, FederationRequest req) {
        return asFederationLink(sourceURL, null, token, req, null);
    }
@@ -95,7 +94,7 @@
            req = FederationRequest.PULL_REPOSITORIES;
        }
        return remoteURL + Constants.FEDERATION_PATH + "?req=" + req.name().toLowerCase()
                + "&token=" + token
                + (token == null ? "" : ("&token=" + token))
                + (tokenType == null ? "" : ("&tokenType=" + tokenType.name().toLowerCase()))
                + (myURL == null ? "" : ("&url=" + StringUtils.encodeURL(myURL)));
    }
@@ -132,16 +131,6 @@
        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;
@@ -150,28 +139,32 @@
            }
            reader.close();
            // check to see if we have repository data
            // check to see if we have proposal data
            if (json.length() == 0) {
                logger.error(MessageFormat.format(
                        "Failed to receive proposed repositories list from {0}", url));
                logger.error(MessageFormat.format("Failed to receive proposal data from {0}",
                        request.getRemoteAddr()));
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
            // deserialize the repository data
            // deserialize the proposal
            Gson gson = new Gson();
            Map<String, RepositoryModel> repositories = gson.fromJson(json.toString(),
                    FederationUtils.REPOSITORIES_TYPE);
            FederationProposal proposal = gson.fromJson(json.toString(), FederationProposal.class);
            // submit a proposal
            FederationProposal proposal = new FederationProposal(url, tokenType, token,
                    repositories);
            // reject proposal, if not receipt prohibited
            if (!GitBlit.getBoolean(Keys.federation.allowProposals, false)) {
                logger.error(MessageFormat.format("Rejected {0} federation proposal from {1}",
                        proposal.tokenType.name(), proposal.url));
                response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                return;
            }
            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));
                    proposal.tokenType.name(), proposal.repositories.size(), proposal.url));
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
@@ -225,53 +218,8 @@
        Object result = null;
        if (FederationRequest.PULL_REPOSITORIES.equals(reqType)) {
            // build a reverse-lookup for token->federation set name
            Map<String, String> federationSets = new HashMap<String, String>();
            for (String set : GitBlit.getStrings(Keys.federation.sets)) {
                federationSets.put(GitBlit.self().getFederationToken(set), set);
            }
            // 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;
                }
                if (federationSets.containsKey(token)) {
                    // include repositories only for federation set
                    String set = federationSets.get(token);
                    if (model.federationSets.contains(set)) {
                        repositories.put(url, model);
                    }
                } else {
                    // standard federation token for ALL
                    repositories.put(url, model);
                }
            }
            result = repositories;
            String gitblitUrl = HttpUtils.getHostURL(request);
            result = GitBlit.self().getRepositories(gitblitUrl, token);
        } else {
            if (FederationRequest.PULL_SETTINGS.equals(reqType)) {
                // pull settings
src/com/gitblit/GitBlit.java
@@ -1102,6 +1102,86 @@
    }
    /**
     * Get repositories for the specified token.
     *
     * @param gitblitUrl
     *            the base url of this gitblit instance
     * @param token
     *            the federation token
     * @return a map of <cloneurl, RepositoryModel>
     */
    public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
        Map<String, String> federationSets = new HashMap<String, String>();
        for (String set : getStrings(Keys.federation.sets)) {
            federationSets.put(getFederationToken(set), set);
        }
        // Determine the Gitblit clone url
        StringBuilder sb = new StringBuilder();
        sb.append(gitblitUrl);
        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 = 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;
            }
            if (federationSets.containsKey(token)) {
                // include repositories only for federation set
                String set = federationSets.get(token);
                if (model.federationSets.contains(set)) {
                    repositories.put(url, model);
                }
            } else {
                // standard federation token for ALL
                repositories.put(url, model);
            }
        }
        return repositories;
    }
    /**
     * Creates a proposal from the token.
     *
     * @param gitblitUrl
     *            the url of this Gitblit instance
     * @param token
     * @return a potential proposal
     */
    public FederationProposal createFederationProposal(String gitblitUrl, String token) {
        FederationToken tokenType = FederationToken.REPOSITORIES;
        for (FederationToken type : FederationToken.values()) {
            if (token.equals(getFederationToken(type))) {
                tokenType = type;
                break;
            }
        }
        Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
        FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
                repositories);
        return proposal;
    }
    /**
     * Returns the proposal identified by the supplied token.
     * 
     * @param token
src/com/gitblit/models/FederationProposal.java
@@ -37,6 +37,8 @@
    public FederationToken tokenType;
    public String token;
    public String message;
    public Map<String, RepositoryModel> repositories;
@@ -59,6 +61,7 @@
        this.url = url;
        this.tokenType = tokenType;
        this.token = token;
        this.message = "";
        this.repositories = repositories;
        try {
            // determine server name and set that as the proposal name
@@ -66,7 +69,7 @@
            if (name.contains("/")) {
                name = name.substring(0, name.indexOf('/'));
            }
            name = name.replace(".", "");
            name = name.replace(".", "").replace(";", "").replace(":", "").replace("-", "");
        } catch (Exception e) {
            name = Long.toHexString(System.currentTimeMillis());
        }
src/com/gitblit/utils/FederationUtils.java
@@ -27,7 +27,6 @@
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;
@@ -41,9 +40,9 @@
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.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.google.gson.Gson;
@@ -94,22 +93,15 @@
     * 
     * @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
     * @param proposal
     *            a complete federation proposal
     * @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);
    public static boolean propose(String remoteUrl, FederationProposal proposal) throws Exception {
        String url = FederationServlet
                .asFederationLink(remoteUrl, null, FederationRequest.PROPOSAL);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String json = gson.toJson(myRepositories);
        String json = gson.toJson(proposal);
        int status = writeJson(url, json);
        return status == HttpServletResponse.SC_OK;
    }
@@ -126,7 +118,7 @@
     */
    public static Map<String, RepositoryModel> getRepositories(FederationModel registration,
            boolean checkExclusions) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
        String url = FederationServlet.asFederationLink(registration.url, registration.token,
                FederationRequest.PULL_REPOSITORIES);
        Map<String, RepositoryModel> models = readGson(url, REPOSITORIES_TYPE);
        if (checkExclusions) {
@@ -149,7 +141,7 @@
     * @throws Exception
     */
    public static Collection<UserModel> getUsers(FederationModel registration) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
        String url = FederationServlet.asFederationLink(registration.url, registration.token,
                FederationRequest.PULL_USERS);
        Collection<UserModel> models = readGson(url, USERS_TYPE);
        return models;
@@ -164,7 +156,7 @@
     * @throws Exception
     */
    public static Map<String, String> getSettings(FederationModel registration) throws Exception {
        String url = FederationServlet.asPullLink(registration.url, registration.token,
        String url = FederationServlet.asFederationLink(registration.url, registration.token,
                FederationRequest.PULL_SETTINGS);
        Map<String, String> settings = readGson(url, SETTINGS_TYPE);
        return settings;
src/com/gitblit/wicket/GitBlitWebApp.java
@@ -32,7 +32,7 @@
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.ReviewProposalPage;
import com.gitblit.wicket.pages.FederationRegistrationPage;
import com.gitblit.wicket.pages.HistoryPage;
import com.gitblit.wicket.pages.LogPage;
@@ -98,7 +98,7 @@
        mount("/markdown", MarkdownPage.class, "r", "h", "f");
        // federation urls
        mount("/proposal", FederationProposalPage.class, "t");
        mount("/proposal", ReviewProposalPage.class, "t");
        mount("/registration", FederationRegistrationPage.class, "u", "n");
        // setup login/logout urls, if we are using authentication
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -134,4 +134,8 @@
gb.federationStrategy = federation strategy
gb.federationRegistration = federation registration
gb.federationResults = federation pull results
gb.federationSets = federation sets
gb.federationSets = federation sets
gb.message = message
gb.myUrlDescription = the publicly accessible url for your Gitblit instance
gb.destinationUrl = send to
gb.destinationUrlDescription = the url of the Gitblit instance to send your proposal
src/com/gitblit/wicket/pages/FederationRegistrationPage.html
@@ -13,8 +13,7 @@
    <!-- 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.url">url</wicket:message></th><td><img style="border:0px;vertical-align:middle;" wicket:id="typeIcon" /> <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.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>
src/com/gitblit/wicket/pages/FederationRegistrationPage.java
@@ -36,8 +36,6 @@
    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);
@@ -55,10 +53,11 @@
            error("Could not find federation registration!", true);
        }
        setupPage("", registration.isResultData() ? getString("gb.federationResults")
                : getString("gb.federationRegistration"));
        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 : "--"));
src/com/gitblit/wicket/pages/ReviewProposalPage.html
File was renamed from src/com/gitblit/wicket/pages/FederationProposalPage.html
@@ -13,10 +13,11 @@
    <!-- 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><wicket:message key="gb.url">url</wicket:message></th><td><span wicket:id="url">[url]</span></td></tr>
        <tr><th><wicket:message key="gb.message">message</wicket:message></th><td><span wicket:id="message">[message]</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.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</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>
        
src/com/gitblit/wicket/pages/ReviewProposalPage.java
File was renamed from src/com/gitblit/wicket/pages/FederationProposalPage.java
@@ -33,13 +33,13 @@
import com.gitblit.wicket.panels.RepositoriesPanel;
@RequiresAdminRole
public class FederationProposalPage extends BasePage {
public class ReviewProposalPage 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) {
    public ReviewProposalPage(PageParameters params) {
        super(params);
        setupPage("", getString("gb.proposals"));
@@ -53,9 +53,10 @@
        }
        add(new Label("url", proposal.url));
        add(new Label("message", proposal.message));
        add(WicketUtils.createTimestampLabel("received", proposal.received, getTimeZone()));
        add(new Label("tokenType", proposal.tokenType.name()));
        add(new Label("token", proposal.token));
        add(new Label("tokenType", proposal.tokenType.name()));
        boolean go = true;
        String p;
src/com/gitblit/wicket/pages/SendProposalPage.html
New file
@@ -0,0 +1,30 @@
<!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">
<wicket:extend>
<body onload="document.getElementById('myUrl').focus();">
    <div style="padding-top:20px"></div>
    <div style="text-align:center;" wicket:id="feedback">[Feedback Panel]</div>
    <!-- proposal info -->
    <form wicket:id="editForm">
    <table class="plain">
        <tr><th><wicket:message key="gb.url">url</wicket:message></th><td class="edit"><input type="text" wicket:id="myUrl" id="myUrl" size="60" />  &nbsp;<i><wicket:message key="gb.myUrlDescription"></wicket:message></i></td></tr>
        <tr><th><wicket:message key="gb.destinationUrl">destination url</wicket:message></th><td class="edit"><input type="text" wicket:id="destinationUrl" size="60" /> &nbsp;<i><wicket:message key="gb.destinationUrlDescription"></wicket:message></i></td></tr>
        <tr><th valign="top"><wicket:message key="gb.message">message</wicket:message></th><td class="edit"><input type="text" wicket:id="message" size="80" /></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.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
        <tr><th></th><td class="editButton"><input type="submit" value="propose" wicket:message="value:gb.sendProposal" wicket:id="save" /> <input type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /></td></tr>
    </table>
    </form>
    <div style="padding-top:10px;" wicket:id="repositories"></div>
</body>
</wicket:extend>
</html>
src/com/gitblit/wicket/pages/SendProposalPage.java
New file
@@ -0,0 +1,127 @@
/*
 * 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 org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.CompoundPropertyModel;
import com.gitblit.GitBlit;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.RepositoriesPanel;
@RequiresAdminRole
public class SendProposalPage extends BasePage {
    public String myUrl;
    public String destinationUrl;
    public String message;
    public SendProposalPage(PageParameters params) {
        super(params);
        setupPage("", getString("gb.sendProposal"));
        setStatelessHint(true);
        final String token = WicketUtils.getToken(params);
        myUrl = WicketUtils.getHostURL(getRequest());
        destinationUrl = "https://";
        // temporary proposal
        FederationProposal proposal = GitBlit.self().createFederationProposal(myUrl, token);
        if (proposal == null) {
            error("Could not create federation proposal!", true);
        }
        CompoundPropertyModel<SendProposalPage> model = new CompoundPropertyModel<SendProposalPage>(
                this);
        Form<SendProposalPage> form = new Form<SendProposalPage>("editForm", model) {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit() {
                // confirm a repository name was entered
                if (StringUtils.isEmpty(myUrl)) {
                    error("Please enter your Gitblit url!");
                    return;
                }
                if (StringUtils.isEmpty(destinationUrl)) {
                    error("Please enter a destination url for your proposal!");
                    return;
                }
                // build new proposal
                FederationProposal proposal = GitBlit.self().createFederationProposal(myUrl, token);
                proposal.url = myUrl;
                proposal.message = message;
                try {
                    if (FederationUtils.propose(destinationUrl, proposal)) {
                        info(MessageFormat.format("Proposal successfully received by {0}.", destinationUrl));
                        setResponsePage(RepositoriesPage.class);
                    } else {
                        error(MessageFormat.format("Sorry, {0} rejected your proposal.", destinationUrl));
                    }
                } catch (Exception e) {
                    if (!StringUtils.isEmpty(e.getMessage())) {
                        error(e.getMessage());
                    } else {
                        error("Failed to send proposal!");
                    }
                }
            }
        };
        form.add(new TextField<String>("myUrl"));
        form.add(new TextField<String>("destinationUrl"));
        form.add(new TextField<String>("message"));
        form.add(new Label("tokenType", proposal.tokenType.name()));
        form.add(new Label("token", proposal.token));
        form.add(new Button("save"));
        Button cancel = new Button("cancel") {
            private static final long serialVersionUID = 1L;
            @Override
            public void onSubmit() {
                setResponsePage(RepositoriesPage.class);
            }
        };
        cancel.setDefaultFormProcessing(false);
        form.add(cancel);
        add(form);
        List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(
                proposal.repositories.values());
        RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositories", false,
                repositories, getAccessRestrictions());
        add(repositoriesPanel);
    }
}
src/com/gitblit/wicket/panels/FederationProposalsPanel.java
@@ -28,7 +28,7 @@
import com.gitblit.GitBlit;
import com.gitblit.models.FederationProposal;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.FederationProposalPage;
import com.gitblit.wicket.pages.ReviewProposalPage;
public class FederationProposalsPanel extends BasePanel {
@@ -54,11 +54,11 @@
            public void populateItem(final Item<FederationProposal> item) {
                final FederationProposal entry = item.getModelObject();
                item.add(new LinkPanel("url", "list", entry.url, FederationProposalPage.class,
                item.add(new LinkPanel("url", "list", entry.url, ReviewProposalPage.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,
                item.add(new LinkPanel("token", "list", entry.token, ReviewProposalPage.class,
                        WicketUtils.newTokenParameter(entry.token)));
                Link<Void> deleteLink = new Link<Void>("deleteProposal") {
src/com/gitblit/wicket/panels/FederationTokensPanel.java
@@ -20,8 +20,8 @@
import java.util.List;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.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;
@@ -32,6 +32,7 @@
import com.gitblit.GitBlit;
import com.gitblit.Keys;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.SendProposalPage;
public class FederationTokensPanel extends BasePanel {
@@ -41,11 +42,11 @@
        super(wicketId);
        final String baseUrl = getRequest().getRelativePathPrefixToContextRoot();
        add(new ExternalLink("federatedUsers", FederationServlet.asPullLink(baseUrl, GitBlit.self()
        add(new ExternalLink("federatedUsers", FederationServlet.asFederationLink(baseUrl, GitBlit.self()
                .getFederationToken(FederationToken.USERS_AND_REPOSITORIES),
                FederationRequest.PULL_USERS)));
        add(new ExternalLink("federatedSettings", FederationServlet.asPullLink(baseUrl, GitBlit
        add(new ExternalLink("federatedSettings", FederationServlet.asFederationLink(baseUrl, GitBlit
                .self().getFederationToken(FederationToken.ALL), FederationRequest.PULL_SETTINGS)));
        final List<String[]> data = new ArrayList<String[]>();
@@ -82,22 +83,11 @@
                }
                item.add(new Label("value", entry[1]));
                item.add(new ExternalLink("repositoryDefinitions", FederationServlet.asPullLink(
                item.add(new ExternalLink("repositoryDefinitions", FederationServlet.asFederationLink(
                        baseUrl, entry[1], FederationRequest.PULL_REPOSITORIES)));
                // 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 BookmarkablePageLink<Void>("send",
                        SendProposalPage.class, WicketUtils.newTokenParameter(entry[1])));
                WicketUtils.setAlternatingBackground(item, counter);
                counter++;
tests/com/gitblit/tests/FederationTests.java
@@ -28,6 +28,7 @@
import com.gitblit.Constants.FederationToken;
import com.gitblit.FederationServlet;
import com.gitblit.GitBlitServer;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FederationUtils;
import com.google.gson.Gson;
@@ -86,14 +87,17 @@
            repositories.put(model.name, model);
        }
        FederationProposal proposal = new FederationProposal("http://testurl", FederationToken.ALL,
                "testtoken", repositories);
        // propose federation
        assertTrue("proposal refused", FederationUtils.propose("http://localhost:" + port,
                FederationToken.ALL, "testtoken", "http://testurl", repositories));
        assertTrue("proposal refused",
                FederationUtils.propose("http://localhost:" + port, proposal));
    }
    public void testPullRepositories() throws Exception {
        try {
            String url = FederationServlet.asPullLink("http://localhost:" + port, "testtoken",
            String url = FederationServlet.asFederationLink("http://localhost:" + port, "testtoken",
                    FederationRequest.PULL_REPOSITORIES);
            String json = FederationUtils.readJson(url);
        } catch (IOException e) {