From 9b72a2e674f7cbc320b0b72fc71ad813d296ab12 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 22 Sep 2011 12:04:43 -0400
Subject: [PATCH] Updated to JGit 1.1.0 and reverse-sort library dependencies.
---
src/com/gitblit/GitBlit.java | 518 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 502 insertions(+), 16 deletions(-)
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index e570e7b..33eaee9 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -16,15 +16,25 @@
package com.gitblit;
import java.io.File;
+import java.io.FileFilter;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
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;
@@ -44,10 +54,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
@@ -71,6 +88,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;
@@ -80,6 +104,8 @@
private IUserService userService;
private IStoredSettings settings;
+
+ private MailExecutor mailExecutor;
public GitBlit() {
if (gitblit == null) {
@@ -98,6 +124,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;
}
/**
@@ -141,7 +176,7 @@
public static char getChar(String key, char defaultValue) {
return self().settings.getChar(key, defaultValue);
}
-
+
/**
* Returns the string value for the specified key. If the key does not exist
* or the value for the key can not be interpreted as a string, the
@@ -224,6 +259,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;
}
@@ -461,6 +520,12 @@
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.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
+ "gitblit", null, "federationSets")));
+ model.isFederated = getConfig(config, "isFederated", false);
+ model.origin = config.getString("remote", "origin", "url");
}
r.close();
return model;
@@ -475,6 +540,41 @@
public long calculateSize(RepositoryModel model) {
File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
return com.gitblit.utils.FileUtils.folderSize(gitDir);
+ }
+
+ /**
+ * Ensure that a cached repository is completely closed and its resources
+ * are properly released.
+ *
+ * @param repositoryName
+ */
+ private void closeRepository(String repositoryName) {
+ Repository repository = getRepository(repositoryName);
+ // assume 2 uses in case reflection fails
+ int uses = 2;
+ try {
+ // The FileResolver caches repositories which is very useful
+ // for performance until you want to delete a repository.
+ // I have to use reflection to call close() the correct
+ // number of times to ensure that the object and ref databases
+ // are properly closed before I can delete the repository from
+ // the filesystem.
+ Field useCnt = Repository.class.getDeclaredField("useCnt");
+ useCnt.setAccessible(true);
+ uses = ((AtomicInteger) useCnt.get(repository)).get();
+ } catch (Exception e) {
+ logger.warn(MessageFormat
+ .format("Failed to reflectively determine use count for repository {0}",
+ repositoryName), e);
+ }
+ if (uses > 0) {
+ logger.info(MessageFormat
+ .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
+ repositoryName, uses, uses));
+ for (int i = 0; i < uses; i++) {
+ repository.close();
+ }
+ }
}
/**
@@ -540,6 +640,7 @@
} else {
// rename repository
if (!repositoryName.equalsIgnoreCase(repository.name)) {
+ closeRepository(repositoryName);
File folder = new File(repositoriesFolder, repositoryName);
File destFolder = new File(repositoriesFolder, repository.name);
if (destFolder.exists()) {
@@ -576,22 +677,37 @@
// 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.setStringList("gitblit", null, "federationSets", repository.federationSets);
+ 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);
}
}
@@ -615,6 +731,7 @@
*/
public boolean deleteRepository(String repositoryName) {
try {
+ closeRepository(repositoryName);
File folder = new File(repositoriesFolder, repositoryName);
if (folder.exists() && folder.isDirectory()) {
FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
@@ -672,6 +789,367 @@
}
/**
+ * Returns Gitblit's scheduled executor service for scheduling tasks.
+ *
+ * @return scheduledExecutor
+ */
+ public ScheduledExecutorService executor() {
+ return scheduledExecutor;
+ }
+
+ public static boolean canFederate() {
+ String passphrase = getString(Keys.federation.passphrase, "");
+ return !StringUtils.isEmpty(passphrase);
+ }
+
+ /**
+ * Configures this Gitblit instance to pull any registered federated gitblit
+ * instances.
+ */
+ private void configureFederation() {
+ boolean validPassphrase = true;
+ String passphrase = settings.getString(Keys.federation.passphrase, "");
+ if (StringUtils.isEmpty(passphrase)) {
+ logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
+ validPassphrase = false;
+ }
+ if (validPassphrase) {
+ // standard tokens
+ for (FederationToken tokenType : FederationToken.values()) {
+ logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
+ getFederationToken(tokenType)));
+ }
+
+ // federation set tokens
+ for (String set : settings.getStrings(Keys.federation.sets)) {
+ logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
+ getFederationToken(set)));
+ }
+ }
+
+ // 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.passphrase);
+ keys.remove(Keys.federation.allowProposals);
+ keys.remove(Keys.federation.proposalsFolder);
+ keys.remove(Keys.federation.defaultFrequency);
+ keys.remove(Keys.federation.sets);
+ 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("freeze")) {
+ // set the repository to read-only after pull
+ federatedModels.get(server).freeze = settings.getBoolean(key, true);
+ } 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>();
+ // generate standard tokens
+ for (FederationToken type : FederationToken.values()) {
+ tokens.add(getFederationToken(type));
+ }
+ // generate tokens for federation sets
+ for (String set : settings.getStrings(Keys.federation.sets)) {
+ tokens.add(getFederationToken(set));
+ }
+ return tokens;
+ }
+
+ /**
+ * Returns the specified federation token for this Gitblit instance.
+ *
+ * @param type
+ * @return a federation token
+ */
+ public String getFederationToken(FederationToken type) {
+ return getFederationToken(type.name());
+ }
+
+ /**
+ * Returns the specified federation token for this Gitblit instance.
+ *
+ * @param value
+ * @return a federation token
+ */
+ public String getFederationToken(String value) {
+ String passphrase = settings.getString(Keys.federation.passphrase, "");
+ return StringUtils.getSHA1(passphrase + "-" + value);
+ }
+
+ /**
+ * 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).
@@ -707,6 +1185,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.");
+ }
}
/**
@@ -731,5 +1216,6 @@
@Override
public void contextDestroyed(ServletContextEvent contextEvent) {
logger.info("Gitblit context destroyed by servlet container.");
+ scheduledExecutor.shutdownNow();
}
}
--
Gitblit v1.9.1