From 79dfe69726b6255464599ab852018e4d2ff96fdc Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 19 Oct 2012 22:47:35 -0400
Subject: [PATCH] Split edit repository page into tabs
---
src/com/gitblit/GitBlit.java | 1271 ++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 1,149 insertions(+), 122 deletions(-)
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 89dcbf1..ce556b6 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -22,23 +22,32 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
import java.util.TreeSet;
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 java.util.concurrent.atomic.AtomicReference;
import javax.mail.Message;
import javax.mail.MessagingException;
@@ -46,42 +55,55 @@
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
import org.apache.wicket.protocol.http.WebResponse;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.transport.resolver.FileResolver;
-import org.eclipse.jgit.transport.resolver.RepositoryResolver;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.WindowCache;
+import org.eclipse.jgit.storage.file.WindowCacheConfig;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.Constants.AccessPermission;
import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.FederationToken;
+import com.gitblit.Constants.RegistrantType;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.FederationSet;
+import com.gitblit.models.ForkModel;
import com.gitblit.models.Metric;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SearchResult;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ByteFormat;
+import com.gitblit.utils.ContainerUtils;
+import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.MetricUtils;
import com.gitblit.utils.ObjectCache;
import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
/**
* GitBlit is the servlet context listener singleton that acts as the core for
@@ -102,7 +124,7 @@
public class GitBlit implements ServletContextListener {
private static GitBlit gitblit;
-
+
private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
@@ -115,14 +137,16 @@
private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
-
- private RepositoryResolver<Void> repositoryResolver;
+
+ private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>();
+
+ private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>();
+
+ private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
private ServletContext servletContext;
private File repositoriesFolder;
-
- private boolean exportAll = true;
private IUserService userService;
@@ -133,12 +157,23 @@
private ServerStatus serverStatus;
private MailExecutor mailExecutor;
+
+ private LuceneExecutor luceneExecutor;
+
+ private TimeZone timezone;
+
+ private FileBasedConfig projectConfigs;
public GitBlit() {
if (gitblit == null) {
// set the static singleton reference
gitblit = this;
}
+ }
+
+ public GitBlit(final IUserService userService) {
+ this.userService = userService;
+ gitblit = this;
}
/**
@@ -161,6 +196,33 @@
public static boolean isGO() {
return self().settings instanceof FileSettings;
}
+
+ /**
+ * Returns the preferred timezone for the Gitblit instance.
+ *
+ * @return a timezone
+ */
+ public static TimeZone getTimezone() {
+ if (self().timezone == null) {
+ String tzid = getString("web.timezone", null);
+ if (StringUtils.isEmpty(tzid)) {
+ self().timezone = TimeZone.getDefault();
+ return self().timezone;
+ }
+ self().timezone = TimeZone.getTimeZone(tzid);
+ }
+ return self().timezone;
+ }
+
+ /**
+ * Returns the user-defined blob encodings.
+ *
+ * @return an array of encodings, may be empty
+ */
+ public static String [] getEncodings() {
+ return getStrings(Keys.web.blobEncodings).toArray(new String[0]);
+ }
+
/**
* Returns the boolean value for the specified key. If the key does not
@@ -227,6 +289,17 @@
*/
public static List<String> getStrings(String key) {
return self().settings.getStrings(key);
+ }
+
+ /**
+ * Returns a map of space-separated key-value pairs from the specified key.
+ *
+ * @see IStoredSettings.getStrings(String key)
+ * @param name
+ * @return map of string, string
+ */
+ public static Map<String, String> getMap(String key) {
+ return self().settings.getMap(key);
}
/**
@@ -352,6 +425,38 @@
this.userService = userService;
this.userService.setup(settings);
}
+
+ /**
+ *
+ * @return true if the user service supports credential changes
+ */
+ public boolean supportsCredentialChanges() {
+ return userService.supportsCredentialChanges();
+ }
+
+ /**
+ *
+ * @return true if the user service supports display name changes
+ */
+ public boolean supportsDisplayNameChanges() {
+ return userService.supportsDisplayNameChanges();
+ }
+
+ /**
+ *
+ * @return true if the user service supports email address changes
+ */
+ public boolean supportsEmailAddressChanges() {
+ return userService.supportsEmailAddressChanges();
+ }
+
+ /**
+ *
+ * @return true if the user service supports team membership changes
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return userService.supportsTeamMembershipChanges();
+ }
/**
* Authenticate a user based on a username and password.
@@ -416,6 +521,28 @@
}
/**
+ * Authenticate a user based on HTTP request paramters.
+ * This method is inteded to be used as fallback when other
+ * means of authentication are failing (username / password or cookies).
+ * @param httpRequest
+ * @return a user object or null
+ */
+ public UserModel authenticate(HttpServletRequest httpRequest) {
+ return null;
+ }
+
+ /**
+ * Open a file resource using the Servlet container.
+ * @param file to open
+ * @return InputStream of the opened file
+ * @throws ResourceStreamNotFoundException
+ */
+ public InputStream getResourceAsStream(String file) throws ResourceStreamNotFoundException {
+ ContextRelativeResource res = WicketUtils.getResource(file);
+ return res.getResourceStream().getInputStream();
+ }
+
+ /**
* Sets a cookie for the specified user.
*
* @param response
@@ -432,13 +559,31 @@
userCookie = new Cookie(Constants.NAME, "");
} else {
// set cookie for login
- char[] cookie = userService.getCookie(user);
- userCookie = new Cookie(Constants.NAME, new String(cookie));
- userCookie.setMaxAge(Integer.MAX_VALUE);
+ String cookie = userService.getCookie(user);
+ if (StringUtils.isEmpty(cookie)) {
+ // create empty cookie
+ userCookie = new Cookie(Constants.NAME, "");
+ } else {
+ // create real cookie
+ userCookie = new Cookie(Constants.NAME, cookie);
+ userCookie.setMaxAge(Integer.MAX_VALUE);
+ }
}
userCookie.setPath("/");
response.addCookie(userCookie);
}
+ }
+
+ /**
+ * Logout a user.
+ *
+ * @param user
+ */
+ public void logout(UserModel user) {
+ if (userService == null) {
+ return;
+ }
+ userService.logout(user);
}
/**
@@ -451,7 +596,7 @@
List<String> names = new ArrayList<String>(userService.getAllUsernames());
return names;
}
-
+
/**
* Returns the list of all users available to the login service.
*
@@ -487,12 +632,44 @@
}
/**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
+ * Returns the list of users and their access permissions for the specified repository.
+ *
+ * @param repository
+ * @return a list of User-AccessPermission tuples
+ */
+ public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> permissions = new ArrayList<RegistrantAccessPermission>();
+ for (String user : userService.getUsernamesForRepositoryRole(repository.name)) {
+ AccessPermission ap = userService.getUserModel(user).getRepositoryPermission(repository);
+ permissions.add(new RegistrantAccessPermission(user, ap, RegistrantType.USER));
+ }
+ return permissions;
+ }
+
+ /**
+ * Sets the access permissions to the specified repository for the specified users.
+ *
+ * @param repository
+ * @param permissions
+ * @return true if the user models have been updated
+ */
+ public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
+ List<UserModel> users = new ArrayList<UserModel>();
+ for (RegistrantAccessPermission up : permissions) {
+ UserModel user = userService.getUserModel(up.registrant);
+ user.setRepositoryPermission(repository.name, up.permission);
+ users.add(user);
+ }
+ return userService.updateUserModels(users);
+ }
+
+ /**
+ * Returns the list of all users who have an explicit access permission
+ * for the specified repository.
*
* @see IUserService.getUsernamesForRepositoryRole(String)
* @param repository
- * @return list of all usernames that can bypass the access restriction
+ * @return list of all usernames that have an access permission for the repository
*/
public List<String> getRepositoryUsers(RepositoryModel repository) {
return userService.getUsernamesForRepositoryRole(repository.name);
@@ -507,8 +684,11 @@
* @param usernames
* @return true if successful
*/
+ @Deprecated
public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
- return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers);
+ // rejects all changes since 1.2.0 because this would elevate
+ // all discrete access permissions to RW+
+ return false;
}
/**
@@ -529,6 +709,22 @@
"Failed to rename ''{0}'' because ''{1}'' already exists.", username,
user.username));
}
+
+ // rename repositories and owner fields for all repositories
+ for (RepositoryModel model : getRepositoryModels(user)) {
+ if (model.isUsersPersonalRepository(username)) {
+ // personal repository
+ model.owner = user.username;
+ String oldRepositoryName = model.name;
+ model.name = "~" + user.username + model.name.substring(model.projectPath.length());
+ model.projectPath = "~" + user.username;
+ updateRepositoryModel(oldRepositoryName, model, false);
+ } else if (model.isOwner(username)) {
+ // common/shared repo
+ model.owner = user.username;
+ updateRepositoryModel(model.name, model, false);
+ }
+ }
}
if (!userService.updateUserModel(username, user)) {
throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
@@ -545,7 +741,7 @@
List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
return teams;
}
-
+
/**
* Returns the list of available teams that a user or repository may be
* assigned to.
@@ -566,14 +762,46 @@
public TeamModel getTeamModel(String teamname) {
return userService.getTeamModel(teamname);
}
-
+
/**
- * Returns the list of all teams who are allowed to bypass the access
- * restriction placed on the specified repository.
+ * Returns the list of teams and their access permissions for the specified repository.
+ *
+ * @param repository
+ * @return a list of Team-AccessPermission tuples
+ */
+ public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> permissions = new ArrayList<RegistrantAccessPermission>();
+ for (String team : userService.getTeamnamesForRepositoryRole(repository.name)) {
+ AccessPermission ap = userService.getTeamModel(team).getRepositoryPermission(repository);
+ permissions.add(new RegistrantAccessPermission(team, ap, RegistrantType.TEAM));
+ }
+ return permissions;
+ }
+
+ /**
+ * Sets the access permissions to the specified repository for the specified teams.
+ *
+ * @param repository
+ * @param permissions
+ * @return true if the team models have been updated
+ */
+ public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
+ List<TeamModel> teams = new ArrayList<TeamModel>();
+ for (RegistrantAccessPermission tp : permissions) {
+ TeamModel team = userService.getTeamModel(tp.registrant);
+ team.setRepositoryPermission(repository.name, tp.permission);
+ teams.add(team);
+ }
+ return userService.updateTeamModels(teams);
+ }
+
+ /**
+ * Returns the list of all teams who have an explicit access permission for
+ * the specified repository.
*
* @see IUserService.getTeamnamesForRepositoryRole(String)
* @param repository
- * @return list of all teamnames that can bypass the access restriction
+ * @return list of all teamnames with explicit access permissions to the repository
*/
public List<String> getRepositoryTeams(RepositoryModel repository) {
return userService.getTeamnamesForRepositoryRole(repository.name);
@@ -588,8 +816,11 @@
* @param teamnames
* @return true if successful
*/
+ @Deprecated
public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
- return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
+ // rejects all changes since 1.2.0 because this would elevate
+ // all discrete access permissions to RW+
+ return false;
}
/**
@@ -623,15 +854,89 @@
public boolean deleteTeam(String teamname) {
return userService.deleteTeam(teamname);
}
+
+ /**
+ * Adds the repository to the list of cached repositories if Gitblit is
+ * configured to cache the repository list.
+ *
+ * @param model
+ */
+ private void addToCachedRepositoryList(RepositoryModel model) {
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ repositoryListCache.put(model.name, model);
+
+ // update the fork origin repository with this repository clone
+ if (!StringUtils.isEmpty(model.originRepository)) {
+ if (repositoryListCache.containsKey(model.originRepository)) {
+ RepositoryModel origin = repositoryListCache.get(model.originRepository);
+ origin.addFork(model.name);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the repository from the list of cached repositories.
+ *
+ * @param name
+ * @return the model being removed
+ */
+ private RepositoryModel removeFromCachedRepositoryList(String name) {
+ if (StringUtils.isEmpty(name)) {
+ return null;
+ }
+ return repositoryListCache.remove(name);
+ }
/**
- * Clears all the cached data for the specified repository.
+ * Clears all the cached metadata for the specified repository.
*
* @param repositoryName
*/
- public void clearRepositoryCache(String repositoryName) {
+ private void clearRepositoryMetadataCache(String repositoryName) {
repositorySizeCache.remove(repositoryName);
repositoryMetricsCache.remove(repositoryName);
+ }
+
+ /**
+ * Resets the repository list cache.
+ *
+ */
+ public void resetRepositoryListCache() {
+ logger.info("Repository cache manually reset");
+ repositoryListCache.clear();
+ }
+
+ /**
+ * Calculate the checksum of settings that affect the repository list cache.
+ * @return a checksum
+ */
+ private String getRepositoryListSettingsChecksum() {
+ StringBuilder ns = new StringBuilder();
+ ns.append(settings.getString(Keys.git.cacheRepositoryList, "")).append('\n');
+ ns.append(settings.getString(Keys.git.onlyAccessBareRepositories, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchRepositoriesSubfolders, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchRecursionDepth, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchExclusions, "")).append('\n');
+ String checksum = StringUtils.getSHA1(ns.toString());
+ return checksum;
+ }
+
+ /**
+ * Compare the last repository list setting checksum to the current checksum.
+ * If different then clear the cache so that it may be rebuilt.
+ *
+ * @return true if the cached repository list is valid since the last check
+ */
+ private boolean isValidRepositoryList() {
+ String newChecksum = getRepositoryListSettingsChecksum();
+ boolean valid = newChecksum.equals(repositoryListSettingsChecksum.get());
+ repositoryListSettingsChecksum.set(newChecksum);
+ if (!valid && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ logger.info("Repository list settings have changed. Clearing repository list cache.");
+ repositoryListCache.clear();
+ }
+ return valid;
}
/**
@@ -641,8 +946,48 @@
* @return list of all repositories
*/
public List<String> getRepositoryList() {
- return JGitUtils.getRepositoryList(repositoriesFolder, exportAll,
- settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
+ if (repositoryListCache.size() == 0 || !isValidRepositoryList()) {
+ // we are not caching OR we have not yet cached OR the cached list is invalid
+ long startTime = System.currentTimeMillis();
+ List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder,
+ settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
+ settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true),
+ settings.getInteger(Keys.git.searchRecursionDepth, -1),
+ settings.getStrings(Keys.git.searchExclusions));
+
+ if (!settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // we are not caching
+ StringUtils.sortRepositorynames(repositories);
+ return repositories;
+ } else {
+ // we are caching this list
+ String msg = "{0} repositories identified in {1} msecs";
+
+ // optionally (re)calculate repository sizes
+ if (getBoolean(Keys.web.showRepositorySizes, true)) {
+ msg = "{0} repositories identified with calculated folder sizes in {1} msecs";
+ for (String repository : repositories) {
+ RepositoryModel model = getRepositoryModel(repository);
+ if (!model.skipSizeCalculation) {
+ calculateSize(model);
+ }
+ }
+ } else {
+ // update cache
+ for (String repository : repositories) {
+ getRepositoryModel(repository);
+ }
+ }
+
+ long duration = System.currentTimeMillis() - startTime;
+ logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration));
+ }
+ }
+
+ // return sorted copy of cached list
+ List<String> list = new ArrayList<String>(repositoryListCache.keySet());
+ StringUtils.sortRepositorynames(list);
+ return list;
}
/**
@@ -652,21 +997,30 @@
* @return repository or null
*/
public Repository getRepository(String repositoryName) {
+ return getRepository(repositoryName, true);
+ }
+
+ /**
+ * Returns the JGit repository for the specified name.
+ *
+ * @param repositoryName
+ * @param logError
+ * @return repository or null
+ */
+ public Repository getRepository(String repositoryName, boolean logError) {
+ File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
+ if (dir == null)
+ return null;
+
Repository r = null;
try {
- r = repositoryResolver.open(null, repositoryName);
- } catch (RepositoryNotFoundException e) {
- r = null;
- logger.error("GitBlit.getRepository(String) failed to find "
- + new File(repositoriesFolder, repositoryName).getAbsolutePath());
- } catch (ServiceNotAuthorizedException e) {
- r = null;
- logger.error("GitBlit.getRepository(String) failed to find "
- + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
- } catch (ServiceNotEnabledException e) {
- r = null;
- logger.error("GitBlit.getRepository(String) failed to find "
- + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
+ FileKey key = FileKey.exact(dir, FS.DETECTED);
+ r = RepositoryCache.open(key, true);
+ } catch (IOException e) {
+ if (logError) {
+ logger.error("GitBlit.getRepository(String) failed to find "
+ + new File(repositoriesFolder, repositoryName).getAbsolutePath());
+ }
}
return r;
}
@@ -678,6 +1032,7 @@
* @return list of repository models accessible to user
*/
public List<RepositoryModel> getRepositoryModels(UserModel user) {
+ long methodStart = System.currentTimeMillis();
List<String> list = getRepositoryList();
List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
for (String repo : list) {
@@ -697,9 +1052,15 @@
}
}
long duration = System.currentTimeMillis() - startTime;
- logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
+ if (duration > 250) {
+ // only log calcualtion time if > 250 msecs
+ logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
repoCount, duration));
+ }
}
+ long duration = System.currentTimeMillis() - methodStart;
+ logger.info(MessageFormat.format("{0} repository models loaded for {1} in {2} msecs",
+ repositories.size(), user == null ? "anonymous" : user.username, duration));
return repositories;
}
@@ -716,14 +1077,13 @@
if (model == null) {
return null;
}
- if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
- if (user != null && user.canAccessRepository(model)) {
- return model;
- }
- return null;
- } else {
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+ if (user.canView(model)) {
return model;
}
+ return null;
}
/**
@@ -734,23 +1094,272 @@
* @return repository model or null
*/
public RepositoryModel getRepositoryModel(String repositoryName) {
+ if (!repositoryListCache.containsKey(repositoryName)) {
+ RepositoryModel model = loadRepositoryModel(repositoryName);
+ if (model == null) {
+ return null;
+ }
+ addToCachedRepositoryList(model);
+ return model;
+ }
+
+ // cached model
+ RepositoryModel model = repositoryListCache.get(repositoryName);
+
+ // check for updates
+ Repository r = getRepository(repositoryName);
+ if (r == null) {
+ // repository is missing
+ removeFromCachedRepositoryList(repositoryName);
+ logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName));
+ return null;
+ }
+
+ FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r);
+ if (config.isOutdated()) {
+ // reload model
+ logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
+ model = loadRepositoryModel(repositoryName);
+ removeFromCachedRepositoryList(repositoryName);
+ addToCachedRepositoryList(model);
+ } else {
+ // update a few repository parameters
+ if (!model.hasCommits) {
+ // update hasCommits, assume a repository only gains commits :)
+ model.hasCommits = JGitUtils.hasCommits(r);
+ }
+
+ model.lastChange = JGitUtils.getLastChange(r);
+ }
+ r.close();
+
+ // return a copy of the cached model
+ return DeepCopier.copy(model);
+ }
+
+
+ /**
+ * Returns the map of project config. This map is cached and reloaded if
+ * the underlying projects.conf file changes.
+ *
+ * @return project config map
+ */
+ private Map<String, ProjectModel> getProjectConfigs() {
+ if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
+
+ try {
+ projectConfigs.load();
+ } catch (Exception e) {
+ }
+
+ // project configs
+ String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+ ProjectModel rootProject = new ProjectModel(rootName, true);
+
+ Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>();
+ // cache the root project under its alias and an empty path
+ configs.put("", rootProject);
+ configs.put(rootProject.name.toLowerCase(), rootProject);
+
+ for (String name : projectConfigs.getSubsections("project")) {
+ ProjectModel project;
+ if (name.equalsIgnoreCase(rootName)) {
+ project = rootProject;
+ } else {
+ project = new ProjectModel(name);
+ }
+ project.title = projectConfigs.getString("project", name, "title");
+ project.description = projectConfigs.getString("project", name, "description");
+ // TODO add more interesting metadata
+ // project manager?
+ // commit message regex?
+ // RW+
+ // RW
+ // R
+ configs.put(name.toLowerCase(), project);
+ }
+ projectCache.clear();
+ projectCache.putAll(configs);
+ }
+ return projectCache;
+ }
+
+ /**
+ * Returns a list of project models for the user.
+ *
+ * @param user
+ * @param includeUsers
+ * @return list of projects that are accessible to the user
+ */
+ public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
+ Map<String, ProjectModel> configs = getProjectConfigs();
+
+ // per-user project lists, this accounts for security and visibility
+ Map<String, ProjectModel> map = new TreeMap<String, ProjectModel>();
+ // root project
+ map.put("", configs.get(""));
+
+ for (RepositoryModel model : getRepositoryModels(user)) {
+ String rootPath = StringUtils.getRootPath(model.name).toLowerCase();
+ if (!map.containsKey(rootPath)) {
+ ProjectModel project;
+ if (configs.containsKey(rootPath)) {
+ // clone the project model because it's repository list will
+ // be tailored for the requesting user
+ project = DeepCopier.copy(configs.get(rootPath));
+ } else {
+ project = new ProjectModel(rootPath);
+ }
+ map.put(rootPath, project);
+ }
+ map.get(rootPath).addRepository(model);
+ }
+
+ // sort projects, root project first
+ List<ProjectModel> projects;
+ if (includeUsers) {
+ // all projects
+ projects = new ArrayList<ProjectModel>(map.values());
+ Collections.sort(projects);
+ projects.remove(map.get(""));
+ projects.add(0, map.get(""));
+ } else {
+ // all non-user projects
+ projects = new ArrayList<ProjectModel>();
+ ProjectModel root = map.remove("");
+ for (ProjectModel model : map.values()) {
+ if (!model.isUserProject()) {
+ projects.add(model);
+ }
+ }
+ Collections.sort(projects);
+ projects.add(0, root);
+ }
+ return projects;
+ }
+
+ /**
+ * Returns the project model for the specified user.
+ *
+ * @param name
+ * @param user
+ * @return a project model, or null if it does not exist
+ */
+ public ProjectModel getProjectModel(String name, UserModel user) {
+ for (ProjectModel project : getProjectModels(user, true)) {
+ if (project.name.equalsIgnoreCase(name)) {
+ return project;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a project model for the Gitblit/system user.
+ *
+ * @param name a project name
+ * @return a project model or null if the project does not exist
+ */
+ public ProjectModel getProjectModel(String name) {
+ Map<String, ProjectModel> configs = getProjectConfigs();
+ ProjectModel project = configs.get(name.toLowerCase());
+ if (project == null) {
+ project = new ProjectModel(name);
+ if (name.length() > 0 && name.charAt(0) == '~') {
+ UserModel user = getUserModel(name.substring(1));
+ if (user != null) {
+ project.title = user.getDisplayName();
+ project.description = "personal repositories";
+ }
+ }
+ } else {
+ // clone the object
+ project = DeepCopier.copy(project);
+ }
+ if (StringUtils.isEmpty(name)) {
+ // get root repositories
+ for (String repository : getRepositoryList()) {
+ if (repository.indexOf('/') == -1) {
+ project.addRepository(repository);
+ }
+ }
+ } else {
+ // get repositories in subfolder
+ String folder = name.toLowerCase() + "/";
+ for (String repository : getRepositoryList()) {
+ if (repository.toLowerCase().startsWith(folder)) {
+ project.addRepository(repository);
+ }
+ }
+ }
+ if (project.repositories.size() == 0) {
+ // no repositories == no project
+ return null;
+ }
+ return project;
+ }
+
+ /**
+ * Workaround JGit. I need to access the raw config object directly in order
+ * to see if the config is dirty so that I can reload a repository model.
+ * If I use the stock JGit method to get the config it already reloads the
+ * config. If the config changes are made within Gitblit this is fine as
+ * the returned config will still be flagged as dirty. BUT... if the config
+ * is manipulated outside Gitblit then it fails to recognize this as dirty.
+ *
+ * @param r
+ * @return a config
+ */
+ private StoredConfig getRepositoryConfig(Repository r) {
+ try {
+ Field f = r.getClass().getDeclaredField("repoConfig");
+ f.setAccessible(true);
+ StoredConfig config = (StoredConfig) f.get(r);
+ return config;
+ } catch (Exception e) {
+ logger.error("Failed to retrieve \"repoConfig\" via reflection", e);
+ }
+ return r.getConfig();
+ }
+
+ /**
+ * Create a repository model from the configuration and repository data.
+ *
+ * @param repositoryName
+ * @return a repositoryModel or null if the repository does not exist
+ */
+ private RepositoryModel loadRepositoryModel(String repositoryName) {
Repository r = getRepository(repositoryName);
if (r == null) {
return null;
}
RepositoryModel model = new RepositoryModel();
- model.name = repositoryName;
+ model.isBare = r.isBare();
+ File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "git");
+ if (model.isBare) {
+ model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
+ } else {
+ model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory().getParentFile());
+ }
model.hasCommits = JGitUtils.hasCommits(r);
- model.lastChange = JGitUtils.getLastChange(r, null);
- StoredConfig config = JGitUtils.readConfig(r);
+ model.lastChange = JGitUtils.getLastChange(r);
+ model.projectPath = StringUtils.getFirstPathElement(repositoryName);
+
+ StoredConfig config = r.getConfig();
+ boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
+
if (config != null) {
model.description = getConfig(config, "description", "");
model.owner = getConfig(config, "owner", "");
model.useTickets = getConfig(config, "useTickets", false);
model.useDocs = getConfig(config, "useDocs", false);
+ model.allowForks = getConfig(config, "allowForks", true);
model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
- "accessRestriction", null));
- model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
+ "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));
+ model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
+ "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
+ model.verifyCommitter = getConfig(config, "verifyCommitter", false);
+ model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
model.isFrozen = getConfig(config, "isFrozen", false);
model.showReadme = getConfig(config, "showReadme", false);
model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
@@ -758,18 +1367,175 @@
model.federationStrategy = FederationStrategy.fromName(getConfig(config,
"federationStrategy", null));
model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
- "gitblit", null, "federationSets")));
+ Constants.CONFIG_GITBLIT, null, "federationSets")));
model.isFederated = getConfig(config, "isFederated", false);
model.origin = config.getString("remote", "origin", "url");
+ if (model.origin != null) {
+ model.origin = model.origin.replace('\\', '/');
+ }
model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
- "gitblit", null, "preReceiveScript")));
+ Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
- "gitblit", null, "postReceiveScript")));
+ Constants.CONFIG_GITBLIT, null, "postReceiveScript")));
model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
- "gitblit", null, "mailingList")));
+ Constants.CONFIG_GITBLIT, null, "mailingList")));
+ model.indexedBranches = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "indexBranch")));
+
+ // Custom defined properties
+ model.customFields = new LinkedHashMap<String, String>();
+ for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) {
+ model.customFields.put(aProperty, config.getString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, aProperty));
+ }
+ }
+ model.HEAD = JGitUtils.getHEADRef(r);
+ model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
+ r.close();
+
+ if (model.origin != null && model.origin.startsWith("file://")) {
+ // repository was cloned locally... perhaps as a fork
+ try {
+ File folder = new File(new URI(model.origin));
+ String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
+ if (!StringUtils.isEmpty(originRepo)) {
+ // ensure origin still exists
+ File repoFolder = new File(getRepositoriesFolder(), originRepo);
+ if (repoFolder.exists()) {
+ model.originRepository = originRepo;
+ }
+ }
+ } catch (URISyntaxException e) {
+ logger.error("Failed to determine fork for " + model, e);
+ }
+ }
+ return model;
+ }
+
+ /**
+ * Determines if this server has the requested repository.
+ *
+ * @param name
+ * @return true if the repository exists
+ */
+ public boolean hasRepository(String repositoryName) {
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // if we are caching use the cache to determine availability
+ // otherwise we end up adding a phantom repository to the cache
+ return repositoryListCache.containsKey(repositoryName);
+ }
+ Repository r = getRepository(repositoryName, false);
+ if (r == null) {
+ return false;
}
r.close();
- return model;
+ return true;
+ }
+
+ /**
+ * Determines if the specified user has a fork of the specified origin
+ * repository.
+ *
+ * @param username
+ * @param origin
+ * @return true the if the user has a fork
+ */
+ public boolean hasFork(String username, String origin) {
+ return getFork(username, origin) != null;
+ }
+
+ /**
+ * Gets the name of a user's fork of the specified origin
+ * repository.
+ *
+ * @param username
+ * @param origin
+ * @return the name of the user's fork, null otherwise
+ */
+ public String getFork(String username, String origin) {
+ String userProject = "~" + username.toLowerCase();
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ String userPath = userProject + "/";
+
+ // collect all origin nodes in fork network
+ Set<String> roots = new HashSet<String>();
+ roots.add(origin);
+ RepositoryModel originModel = repositoryListCache.get(origin);
+ while (originModel != null) {
+ if (!ArrayUtils.isEmpty(originModel.forks)) {
+ for (String fork : originModel.forks) {
+ if (!fork.startsWith(userPath)) {
+ roots.add(fork);
+ }
+ }
+ }
+
+ if (originModel.originRepository != null) {
+ roots.add(originModel.originRepository);
+ originModel = repositoryListCache.get(originModel.originRepository);
+ } else {
+ // break
+ originModel = null;
+ }
+ }
+
+ for (String repository : repositoryListCache.keySet()) {
+ if (repository.toLowerCase().startsWith(userPath)) {
+ RepositoryModel model = repositoryListCache.get(repository);
+ if (!StringUtils.isEmpty(model.originRepository)) {
+ if (roots.contains(model.originRepository)) {
+ // user has a fork in this graph
+ return model.name;
+ }
+ }
+ }
+ }
+ } else {
+ // not caching
+ ProjectModel project = getProjectModel(userProject);
+ for (String repository : project.repositories) {
+ if (repository.toLowerCase().startsWith(userProject)) {
+ RepositoryModel model = repositoryListCache.get(repository);
+ if (model.originRepository.equalsIgnoreCase(origin)) {
+ // user has a fork
+ return model.name;
+ }
+ }
+ }
+ }
+ // user does not have a fork
+ return null;
+ }
+
+ /**
+ * Returns the fork network for a repository by traversing up the fork graph
+ * to discover the root and then down through all children of the root node.
+ *
+ * @param repository
+ * @return a ForkModel
+ */
+ public ForkModel getForkNetwork(String repository) {
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // find the root
+ RepositoryModel model = repositoryListCache.get(repository);
+ while (model.originRepository != null) {
+ model = repositoryListCache.get(model.originRepository);
+ }
+ ForkModel root = getForkModel(model.name);
+ return root;
+ }
+ return null;
+ }
+
+ private ForkModel getForkModel(String repository) {
+ RepositoryModel model = repositoryListCache.get(repository);
+ ForkModel fork = new ForkModel(model);
+ if (!ArrayUtils.isEmpty(model.forks)) {
+ for (String aFork : model.forks) {
+ ForkModel fm = getForkModel(aFork);
+ fork.forks.add(fm);
+ }
+ }
+ return fork;
}
/**
@@ -799,6 +1565,11 @@
*/
private void closeRepository(String repositoryName) {
Repository repository = getRepository(repositoryName);
+ if (repository == null) {
+ return;
+ }
+ RepositoryCache.close(repository);
+
// assume 2 uses in case reflection fails
int uses = 2;
try {
@@ -824,6 +1595,9 @@
repository.close();
}
}
+
+ // close any open index writer/searcher in the Lucene executor
+ luceneExecutor.close(repositoryName);
}
/**
@@ -840,7 +1614,7 @@
if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
}
- List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null);
+ List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null, getTimezone());
repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
return new ArrayList<Metric>(metrics);
}
@@ -855,7 +1629,7 @@
* @return field value or defaultValue
*/
private String getConfig(StoredConfig config, String field, String defaultValue) {
- String value = config.getString("gitblit", null, field);
+ String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
if (StringUtils.isEmpty(value)) {
return defaultValue;
}
@@ -863,7 +1637,7 @@
}
/**
- * Returns the gitblit boolean vlaue for the specified key. If key is not
+ * Returns the gitblit boolean value for the specified key. If key is not
* set, returns defaultValue.
*
* @param config
@@ -872,7 +1646,7 @@
* @return field value or defaultValue
*/
private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
- return config.getBoolean("gitblit", field, defaultValue);
+ return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue);
}
/**
@@ -892,6 +1666,13 @@
public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
boolean isCreate) throws GitBlitException {
Repository r = null;
+ String projectPath = StringUtils.getFirstPathElement(repository.name);
+ if (!StringUtils.isEmpty(projectPath)) {
+ if (projectPath.equalsIgnoreCase(getString(Keys.web.repositoryRootGroupName, "main"))) {
+ // strip leading group name
+ repository.name = repository.name.substring(projectPath.length() + 1);
+ }
+ }
if (isCreate) {
// ensure created repository name ends with .git
if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
@@ -942,31 +1723,66 @@
"Failed to rename repository permissions ''{0}'' to ''{1}''.",
repositoryName, repository.name));
}
+
+ // rename fork origins in their configs
+ if (!ArrayUtils.isEmpty(repository.forks)) {
+ for (String fork : repository.forks) {
+ Repository rf = getRepository(fork);
+ try {
+ StoredConfig config = rf.getConfig();
+ String origin = config.getString("remote", "origin", "url");
+ origin = origin.replace(repositoryName, repository.name);
+ config.setString("remote", "origin", "url", origin);
+ config.save();
+ } catch (Exception e) {
+ logger.error("Failed to update repository fork config for " + fork, e);
+ }
+ rf.close();
+ }
+ }
+
+ // remove this repository from any origin model's fork list
+ if (!StringUtils.isEmpty(repository.originRepository)) {
+ RepositoryModel origin = repositoryListCache.get(repository.originRepository);
+ if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
+ origin.forks.remove(repositoryName);
+ }
+ }
// clear the cache
- clearRepositoryCache(repositoryName);
+ clearRepositoryMetadataCache(repositoryName);
+ repository.resetDisplayName();
}
// load repository
logger.info("edit repository " + repository.name);
- try {
- r = repositoryResolver.open(null, repository.name);
- } catch (RepositoryNotFoundException e) {
- logger.error("Repository not found", e);
- } catch (ServiceNotAuthorizedException e) {
- logger.error("Service not authorized", e);
- } catch (ServiceNotEnabledException e) {
- logger.error("Service not enabled", e);
- }
+ r = getRepository(repository.name);
}
// update settings
if (r != null) {
updateConfiguration(r, repository);
+ // only update symbolic head if it changes
+ String currentRef = JGitUtils.getHEADRef(r);
+ if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) {
+ logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}",
+ repository.name, currentRef, repository.HEAD));
+ if (JGitUtils.setHEADtoRef(r, repository.HEAD)) {
+ // clear the cache
+ clearRepositoryMetadataCache(repository.name);
+ }
+ }
+
+ // close the repository object
r.close();
}
+
+ // update repository cache
+ removeFromCachedRepositoryList(repositoryName);
+ // model will actually be replaced on next load because config is stale
+ addToCachedRepositoryList(repository);
}
-
+
/**
* Updates the Gitblit configuration for the specified repository.
*
@@ -976,35 +1792,62 @@
* 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.setBoolean("gitblit", null, "skipSizeCalculation", repository.skipSizeCalculation);
- config.setBoolean("gitblit", null, "skipSummaryMetrics", repository.skipSummaryMetrics);
- config.setStringList("gitblit", null, "federationSets", repository.federationSets);
- config.setString("gitblit", null, "federationStrategy",
+ StoredConfig config = r.getConfig();
+ config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
+ config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
+ config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
+ config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSizeCalculation", repository.skipSizeCalculation);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSummaryMetrics", repository.skipSummaryMetrics);
+ config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy",
repository.federationStrategy.name());
- config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
- if (repository.preReceiveScripts != null) {
- config.setStringList("gitblit", null, "preReceiveScript", repository.preReceiveScripts);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated);
+
+ updateList(config, "federationSets", repository.federationSets);
+ updateList(config, "preReceiveScript", repository.preReceiveScripts);
+ updateList(config, "postReceiveScript", repository.postReceiveScripts);
+ updateList(config, "mailingList", repository.mailingLists);
+ updateList(config, "indexBranch", repository.indexedBranches);
+
+ // User Defined Properties
+ if (repository.customFields != null) {
+ if (repository.customFields.size() == 0) {
+ // clear section
+ config.unsetSection(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS);
+ } else {
+ for (Entry<String, String> property : repository.customFields.entrySet()) {
+ // set field
+ String key = property.getKey();
+ String value = property.getValue();
+ config.setString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, key, value);
+ }
+ }
}
- if (repository.postReceiveScripts != null) {
- config.setStringList("gitblit", null, "postReceiveScript",
- repository.postReceiveScripts);
- }
- if (repository.mailingLists != null) {
- config.setStringList("gitblit", null, "mailingList", repository.mailingLists);
- }
+
try {
config.save();
} catch (IOException e) {
logger.error("Failed to save repository config!", e);
+ }
+ }
+
+ private void updateList(StoredConfig config, String field, List<String> list) {
+ // a null list is skipped, not cleared
+ // this is for RPC administration where an older manager might be used
+ if (list == null) {
+ return;
+ }
+ if (ArrayUtils.isEmpty(list)) {
+ config.unset(Constants.CONFIG_GITBLIT, null, field);
+ } else {
+ config.setStringList(Constants.CONFIG_GITBLIT, null, field, list);
}
}
@@ -1029,16 +1872,22 @@
public boolean deleteRepository(String repositoryName) {
try {
closeRepository(repositoryName);
+ // clear the repository cache
+ clearRepositoryMetadataCache(repositoryName);
+
+ RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
+ if (model != null && !ArrayUtils.isEmpty(model.forks)) {
+ resetRepositoryListCache();
+ }
+
File folder = new File(repositoriesFolder, repositoryName);
if (folder.exists() && folder.isDirectory()) {
FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
if (userService.deleteRepositoryRole(repositoryName)) {
+ logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
return true;
}
}
-
- // clear the repository cache
- clearRepositoryCache(repositoryName);
} catch (Throwable t) {
logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
}
@@ -1589,6 +2438,20 @@
}
return scripts;
}
+
+ /**
+ * Search the specified repositories using the Lucene query.
+ *
+ * @param query
+ * @param page
+ * @param pageSize
+ * @param repositories
+ * @return
+ */
+ public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) {
+ List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);
+ return srs;
+ }
/**
* Notify the administrators by email.
@@ -1649,9 +2512,13 @@
// ensure that the current values are updated in the setting models
for (String key : settings.getAllKeys(null)) {
SettingModel setting = settingsModel.get(key);
- if (setting != null) {
- setting.currentValue = settings.getString(key, "");
+ if (setting == null) {
+ // unreferenced setting, create a setting model
+ setting = new SettingModel();
+ setting.name = key;
+ settingsModel.add(setting);
}
+ setting.currentValue = settings.getString(key, "");
}
settingsModel.pushScripts = getAllScripts();
return settingsModel;
@@ -1661,16 +2528,21 @@
* Parse the properties file and aggregate all the comments by the setting
* key. A setting model tracks the current value, the default value, the
* description of the setting and and directives about the setting.
+ * @param referencePropertiesInputStream
*
* @return Map<String, SettingModel>
*/
- private ServerSettings loadSettingModels() {
+ private ServerSettings loadSettingModels(InputStream referencePropertiesInputStream) {
ServerSettings settingsModel = new ServerSettings();
+ settingsModel.supportsCredentialChanges = userService.supportsCredentialChanges();
+ settingsModel.supportsDisplayNameChanges = userService.supportsDisplayNameChanges();
+ settingsModel.supportsEmailAddressChanges = userService.supportsEmailAddressChanges();
+ settingsModel.supportsTeamMembershipChanges = userService.supportsTeamMembershipChanges();
try {
// Read bundled Gitblit properties to extract setting descriptions.
// This copy is pristine and only used for populating the setting
// models map.
- InputStream is = servletContext.getResourceAsStream("/WEB-INF/reference.properties");
+ InputStream is = referencePropertiesInputStream;
BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
StringBuilder description = new StringBuilder();
SettingModel setting = new SettingModel();
@@ -1734,29 +2606,81 @@
this.settings = settings;
repositoriesFolder = getRepositoriesFolder();
logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
- repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
- serverStatus = new ServerStatus(isGO());
- String realm = settings.getString(Keys.realm.userService, "users.properties");
- IUserService loginService = null;
- try {
- // check to see if this "file" is a login service class
- Class<?> realmClass = Class.forName(realm);
- if (IUserService.class.isAssignableFrom(realmClass)) {
- loginService = (IUserService) realmClass.newInstance();
- }
- } catch (Throwable t) {
- loginService = new GitblitUserService();
+
+ // calculate repository list settings checksum for future config changes
+ repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum());
+
+ // build initial repository list
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ logger.info("Identifying available repositories...");
+ getRepositoryList();
}
- setUserService(loginService);
+
+ logTimezone("JVM", TimeZone.getDefault());
+ logTimezone(Constants.NAME, getTimezone());
+
+ serverStatus = new ServerStatus(isGO());
+
+ if (this.userService == null) {
+ String realm = settings.getString(Keys.realm.userService, "users.properties");
+ IUserService loginService = null;
+ try {
+ // check to see if this "file" is a login service class
+ Class<?> realmClass = Class.forName(realm);
+ loginService = (IUserService) realmClass.newInstance();
+ } catch (Throwable t) {
+ loginService = new GitblitUserService();
+ }
+ setUserService(loginService);
+ }
+
+ // load and cache the project metadata
+ projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect());
+ getProjectConfigs();
mailExecutor = new MailExecutor(settings);
if (mailExecutor.isReady()) {
+ logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
} else {
logger.warn("Mail server is not properly configured. Mail services disabled.");
}
+ luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
+ logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes.");
+ scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2, TimeUnit.MINUTES);
if (startFederation) {
configureFederation();
}
+
+ // Configure JGit
+ WindowCacheConfig cfg = new WindowCacheConfig();
+
+ cfg.setPackedGitWindowSize(settings.getFilesize(Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
+ cfg.setPackedGitLimit(settings.getFilesize(Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
+ cfg.setDeltaBaseCacheLimit(settings.getFilesize(Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
+ cfg.setPackedGitOpenFiles(settings.getFilesize(Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
+ cfg.setStreamFileThreshold(settings.getFilesize(Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
+ cfg.setPackedGitMMAP(settings.getBoolean(Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
+
+ try {
+ WindowCache.reconfigure(cfg);
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
+ logger.debug(MessageFormat.format("{0} = {1}", Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
+ } catch (IllegalArgumentException e) {
+ logger.error("Failed to configure JGit parameters!", e);
+ }
+
+ ContainerUtils.CVE_2007_0450.test();
+ }
+
+ private void logTimezone(String type, TimeZone zone) {
+ SimpleDateFormat df = new SimpleDateFormat("z Z");
+ df.setTimeZone(zone);
+ String offset = df.format(new Date());
+ logger.info(type + " timezone is " + zone.getID() + " (" + offset + ")");
}
/**
@@ -1767,28 +2691,51 @@
*/
@Override
public void contextInitialized(ServletContextEvent contextEvent) {
+ contextInitialized(contextEvent, contextEvent.getServletContext().getResourceAsStream("/WEB-INF/reference.properties"));
+ }
+
+ public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) {
servletContext = contextEvent.getServletContext();
- settingsModel = loadSettingModels();
if (settings == null) {
// Gitblit WAR is running in a servlet container
ServletContext context = contextEvent.getServletContext();
WebXmlSettings webxmlSettings = new WebXmlSettings(context);
// 0.7.0 web.properties in the deployed war folder
- File overrideFile = new File(context.getRealPath("/WEB-INF/web.properties"));
- if (overrideFile.exists()) {
- webxmlSettings.applyOverrides(overrideFile);
+ String webProps = context.getRealPath("/WEB-INF/web.properties");
+ if (!StringUtils.isEmpty(webProps)) {
+ File overrideFile = new File(webProps);
+ if (overrideFile.exists()) {
+ webxmlSettings.applyOverrides(overrideFile);
+ }
}
+
// 0.8.0 gitblit.properties file located outside the deployed war
// folder lie, for example, on RedHat OpenShift.
- overrideFile = getFileOrFolder("gitblit.properties");
+ File overrideFile = getFileOrFolder("gitblit.properties");
if (!overrideFile.getPath().equals("gitblit.properties")) {
webxmlSettings.applyOverrides(overrideFile);
}
configureContext(webxmlSettings, true);
- }
+ // Copy the included scripts to the configured groovy folder
+ File localScripts = getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
+ if (!localScripts.exists()) {
+ File includedScripts = new File(context.getRealPath("/WEB-INF/groovy"));
+ if (!includedScripts.equals(localScripts)) {
+ try {
+ com.gitblit.utils.FileUtils.copy(localScripts, includedScripts.listFiles());
+ } catch (IOException e) {
+ logger.error(MessageFormat.format(
+ "Failed to copy included Groovy scripts from {0} to {1}",
+ includedScripts, localScripts));
+ }
+ }
+ }
+ }
+
+ settingsModel = loadSettingModels(referencePropertiesInputStream);
serverStatus.servletContainer = servletContext.getServerInfo();
}
@@ -1800,5 +2747,85 @@
public void contextDestroyed(ServletContextEvent contextEvent) {
logger.info("Gitblit context destroyed by servlet container.");
scheduledExecutor.shutdownNow();
+ luceneExecutor.close();
+ }
+
+ /**
+ * Creates a personal fork of the specified repository. The clone is view
+ * restricted by default and the owner of the source repository is given
+ * access to the clone.
+ *
+ * @param repository
+ * @param user
+ * @return the repository model of the fork, if successful
+ * @throws GitBlitException
+ */
+ public RepositoryModel fork(RepositoryModel repository, UserModel user) throws GitBlitException {
+ String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
+ String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name);
+
+ // clone the repository
+ try {
+ JGitUtils.cloneRepository(repositoriesFolder, cloneName, fromUrl, true, null);
+ } catch (Exception e) {
+ throw new GitBlitException(e);
+ }
+
+ // create a Gitblit repository model for the clone
+ RepositoryModel cloneModel = repository.cloneAs(cloneName);
+ // owner has REWIND/RW+ permissions
+ cloneModel.owner = user.username;
+ updateRepositoryModel(cloneName, cloneModel, false);
+
+ // add the owner of the source repository to the clone's access list
+ if (!StringUtils.isEmpty(repository.owner)) {
+ UserModel originOwner = getUserModel(repository.owner);
+ if (originOwner != null) {
+ originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ updateUserModel(originOwner.username, originOwner, false);
+ }
+ }
+
+ // grant origin's user list clone permission to fork
+ List<String> users = getRepositoryUsers(repository);
+ List<UserModel> cloneUsers = new ArrayList<UserModel>();
+ for (String name : users) {
+ if (!name.equalsIgnoreCase(user.username)) {
+ UserModel cloneUser = getUserModel(name);
+ if (cloneUser.canClone(repository)) {
+ // origin user can clone origin, grant clone access to fork
+ cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ }
+ cloneUsers.add(cloneUser);
+ }
+ }
+ userService.updateUserModels(cloneUsers);
+
+ // grant origin's team list clone permission to fork
+ List<String> teams = getRepositoryTeams(repository);
+ List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
+ for (String name : teams) {
+ TeamModel cloneTeam = getTeamModel(name);
+ if (cloneTeam.canClone(repository)) {
+ // origin team can clone origin, grant clone access to fork
+ cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ }
+ cloneTeams.add(cloneTeam);
+ }
+ userService.updateTeamModels(cloneTeams);
+
+ // add this clone to the cached model
+ addToCachedRepositoryList(cloneModel);
+ return cloneModel;
+ }
+
+ /**
+ * Allow to understand if GitBlit supports and is configured to allow
+ * cookie-based authentication.
+ *
+ * @return status of Cookie authentication enablement.
+ */
+ public boolean allowCookieAuthentication() {
+ return GitBlit.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
}
}
--
Gitblit v1.9.1