From 06ae63123c94038b90153f4847de2c57c0193db8 Mon Sep 17 00:00:00 2001 From: Rafael Cavazin <rafaelcavazin@gmail.com> Date: Sun, 27 Jan 2013 09:46:50 -0500 Subject: [PATCH] updating current development --- src/com/gitblit/GitBlit.java | 683 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 files changed, 548 insertions(+), 135 deletions(-) diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 6e587ca..6bf75d7 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -24,6 +24,8 @@ import java.lang.reflect.Field; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.security.Principal; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -58,6 +60,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import org.apache.wicket.RequestCycle; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.resource.ContextRelativeResource; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; @@ -75,12 +78,16 @@ import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.Constants.AuthenticationType; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.FederationRequest; import com.gitblit.Constants.FederationStrategy; import com.gitblit.Constants.FederationToken; import com.gitblit.Constants.PermissionType; import com.gitblit.Constants.RegistrantType; +import com.gitblit.fanout.FanoutNioService; +import com.gitblit.fanout.FanoutService; +import com.gitblit.fanout.FanoutSocketService; import com.gitblit.models.FederationModel; import com.gitblit.models.FederationProposal; import com.gitblit.models.FederationSet; @@ -96,16 +103,20 @@ import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.Base64; import com.gitblit.utils.ByteFormat; import com.gitblit.utils.ContainerUtils; import com.gitblit.utils.DeepCopier; import com.gitblit.utils.FederationUtils; +import com.gitblit.utils.HttpUtils; 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.utils.TimeUtils; +import com.gitblit.utils.X509Utils.X509Metadata; +import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; /** @@ -146,8 +157,14 @@ private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>(); private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>(""); + + private final ObjectCache<String> projectMarkdownCache = new ObjectCache<String>(); + + private final ObjectCache<String> projectRepositoriesMarkdownCache = new ObjectCache<String>(); private ServletContext servletContext; + + private File baseFolder; private File repositoriesFolder; @@ -168,6 +185,8 @@ private TimeZone timezone; private FileBasedConfig projectConfigs; + + private FanoutService fanoutService; public GitBlit() { if (gitblit == null) { @@ -377,12 +396,8 @@ * @return the file */ public static File getFileOrFolder(String fileOrFolder) { - String openShift = System.getenv("OPENSHIFT_DATA_DIR"); - if (!StringUtils.isEmpty(openShift)) { - // running on RedHat OpenShift - return new File(openShift, fileOrFolder); - } - return new File(fileOrFolder); + return com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, + self().baseFolder, fileOrFolder); } /** @@ -392,7 +407,7 @@ * @return the repositories folder path */ public static File getRepositoriesFolder() { - return getFileOrFolder(Keys.git.repositoriesFolder, "git"); + return getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git"); } /** @@ -402,7 +417,7 @@ * @return the proposals folder path */ public static File getProposalsFolder() { - return getFileOrFolder(Keys.federation.proposalsFolder, "proposals"); + return getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals"); } /** @@ -412,9 +427,9 @@ * @return the Groovy scripts folder path */ public static File getGroovyScriptsFolder() { - return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy"); + return getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy"); } - + /** * Updates the list of server settings. * @@ -459,36 +474,48 @@ this.userService.setup(settings); } + public boolean supportsAddUser() { + return supportsCredentialChanges(new UserModel("")); + } + /** + * Returns true if the user's credentials can be changed. * + * @param user * @return true if the user service supports credential changes */ - public boolean supportsCredentialChanges() { - return userService.supportsCredentialChanges(); + public boolean supportsCredentialChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || userService.supportsCredentialChanges(); } /** + * Returns true if the user's display name can be changed. * + * @param user * @return true if the user service supports display name changes */ - public boolean supportsDisplayNameChanges() { - return userService.supportsDisplayNameChanges(); + public boolean supportsDisplayNameChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges(); } /** + * Returns true if the user's email address can be changed. * + * @param user * @return true if the user service supports email address changes */ - public boolean supportsEmailAddressChanges() { - return userService.supportsEmailAddressChanges(); + public boolean supportsEmailAddressChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges(); } /** + * Returns true if the user's team memberships can be changed. * + * @param user * @return true if the user service supports team membership changes */ - public boolean supportsTeamMembershipChanges() { - return userService.supportsTeamMembershipChanges(); + public boolean supportsTeamMembershipChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges(); } /** @@ -536,7 +563,7 @@ * @param cookies * @return a user object or null */ - public UserModel authenticate(Cookie[] cookies) { + protected UserModel authenticate(Cookie[] cookies) { if (userService == null) { return null; } @@ -554,14 +581,113 @@ } /** - * 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). + * Authenticate a user based on HTTP request parameters. + * + * Authentication by X509Certificate is tried first and then by cookie. + * * @param httpRequest * @return a user object or null */ public UserModel authenticate(HttpServletRequest httpRequest) { + return authenticate(httpRequest, false); + } + + /** + * Authenticate a user based on HTTP request parameters. + * + * Authentication by X509Certificate, servlet container principal, cookie, + * and BASIC header. + * + * @param httpRequest + * @param requiresCertificate + * @return a user object or null + */ + public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) { + // try to authenticate by certificate + boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true); + String [] oids = getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]); + UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids); + if (model != null) { + // grab real user model and preserve certificate serial number + UserModel user = getUserModel(model.username); + X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest); + if (user != null) { + flagWicketSession(AuthenticationType.CERTIFICATE); + logger.info(MessageFormat.format("{0} authenticated by client certificate {1} from {2}", + user.username, metadata.serialNumber, httpRequest.getRemoteAddr())); + return user; + } else { + logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted client certificate ({1}) authentication from {2}", + model.username, metadata.serialNumber, httpRequest.getRemoteAddr())); + } + } + + if (requiresCertificate) { + // caller requires client certificate authentication (e.g. git servlet) + return null; + } + + // try to authenticate by servlet container principal + Principal principal = httpRequest.getUserPrincipal(); + if (principal != null) { + UserModel user = getUserModel(principal.getName()); + if (user != null) { + flagWicketSession(AuthenticationType.CONTAINER); + logger.info(MessageFormat.format("{0} authenticated by servlet container principal from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } else { + logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}", + principal.getName(), httpRequest.getRemoteAddr())); + } + } + + // try to authenticate by cookie + if (allowCookieAuthentication()) { + UserModel user = authenticate(httpRequest.getCookies()); + if (user != null) { + flagWicketSession(AuthenticationType.COOKIE); + logger.info(MessageFormat.format("{0} authenticated by cookie from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } + } + + // try to authenticate by BASIC + final String authorization = httpRequest.getHeader("Authorization"); + if (authorization != null && authorization.startsWith("Basic")) { + // Authorization: Basic base64credentials + String base64Credentials = authorization.substring("Basic".length()).trim(); + String credentials = new String(Base64.decode(base64Credentials), + Charset.forName("UTF-8")); + // credentials = username:password + final String[] values = credentials.split(":",2); + + if (values.length == 2) { + String username = values[0]; + char[] password = values[1].toCharArray(); + UserModel user = authenticate(username, password); + if (user != null) { + flagWicketSession(AuthenticationType.CREDENTIALS); + logger.info(MessageFormat.format("{0} authenticated by BASIC request header from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } else { + logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials ({1}) from {2}", + username, credentials, httpRequest.getRemoteAddr())); + } + } + } return null; + } + + protected void flagWicketSession(AuthenticationType authenticationType) { + RequestCycle requestCycle = RequestCycle.get(); + if (requestCycle != null) { + // flag the Wicket session, if this is a Wicket request + GitBlitWebSession session = GitBlitWebSession.get(); + session.authenticationType = authenticationType; + } } /** @@ -649,6 +775,9 @@ * @return true if successful */ public boolean deleteUser(String username) { + if (StringUtils.isEmpty(username)) { + return false; + } return userService.deleteUser(username); } @@ -660,46 +789,83 @@ * @return a user object or null */ public UserModel getUserModel(String username) { - UserModel user = userService.getUserModel(username); + if (StringUtils.isEmpty(username)) { + return null; + } + UserModel user = userService.getUserModel(username); return user; + } + + /** + * Returns the effective list of permissions for this user, taking into account + * team memberships, ownerships. + * + * @param user + * @return the effective list of permissions for the user + */ + public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) { + if (StringUtils.isEmpty(user.username)) { + // new user + return new ArrayList<RegistrantAccessPermission>(); + } + Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>(); + set.addAll(user.getRepositoryPermissions()); + // Flag missing repositories + for (RegistrantAccessPermission permission : set) { + if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) { + RepositoryModel rm = GitBlit.self().getRepositoryModel(permission.registrant); + if (rm == null) { + permission.permissionType = PermissionType.MISSING; + permission.mutable = false; + continue; + } + } + } + + // TODO reconsider ownership as a user property + // manually specify personal repository ownerships + for (RepositoryModel rm : repositoryListCache.values()) { + if (rm.isUsersPersonalRepository(user.username) || rm.isOwner(user.username)) { + RegistrantAccessPermission rp = new RegistrantAccessPermission(rm.name, AccessPermission.REWIND, + PermissionType.OWNER, RegistrantType.REPOSITORY, null, false); + // user may be owner of a repository to which they've inherited + // a team permission, replace any existing perm with owner perm + set.remove(rp); + set.add(rp); + } + } + + List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set); + Collections.sort(list); + return list; } /** - * Returns the list of users and their access permissions for the specified repository. + * Returns the list of users and their access permissions for the specified + * repository including permission source information such as the team or + * regular expression which sets the permission. * * @param repository - * @return a list of User-AccessPermission tuples + * @return a list of RegistrantAccessPermissions */ public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) { - Set<RegistrantAccessPermission> permissions = new LinkedHashSet<RegistrantAccessPermission>(); - if (!StringUtils.isEmpty(repository.owner)) { - UserModel owner = userService.getUserModel(repository.owner); - if (owner != null) { - permissions.add(new RegistrantAccessPermission(owner.username, AccessPermission.REWIND, PermissionType.OWNER, RegistrantType.USER, false)); + List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(); + if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) { + // no permissions needed, REWIND for everyone! + return list; + } + if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl)) { + // no permissions needed, REWIND for authenticated! + return list; + } + // NAMED users and teams + for (UserModel user : userService.getAllUsers()) { + RegistrantAccessPermission ap = user.getRepositoryPermission(repository); + if (ap.permission.exceeds(AccessPermission.NONE)) { + list.add(ap); } } - if (repository.isPersonalRepository()) { - UserModel owner = userService.getUserModel(repository.projectPath.substring(1)); - if (owner != null) { - permissions.add(new RegistrantAccessPermission(owner.username, AccessPermission.REWIND, PermissionType.OWNER, RegistrantType.USER, false)); - } - } - for (String user : userService.getUsernamesForRepositoryRole(repository.name)) { - UserModel model = userService.getUserModel(user); - AccessPermission ap = model.getRepositoryPermission(repository); - PermissionType pType = PermissionType.REGEX; - boolean editable = false; - if (repository.isOwner(model.username)) { - pType = PermissionType.OWNER; - } else if (repository.isUsersPersonalRepository(model.username)) { - pType = PermissionType.OWNER; - } else if (model.hasExplicitRepositoryPermission(repository.name)) { - pType = PermissionType.EXPLICIT; - editable = true; - } - permissions.add(new RegistrantAccessPermission(user, ap, pType, RegistrantType.USER, editable)); - } - return new ArrayList<RegistrantAccessPermission>(permissions); + return list; } /** @@ -712,7 +878,7 @@ public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) { List<UserModel> users = new ArrayList<UserModel>(); for (RegistrantAccessPermission up : permissions) { - if (up.isEditable) { + if (up.mutable) { // only set editable defined permissions UserModel user = userService.getUserModel(up.registrant); user.setRepositoryPermission(repository.name, up.permission); @@ -773,14 +939,14 @@ for (RepositoryModel model : getRepositoryModels(user)) { if (model.isUsersPersonalRepository(username)) { // personal repository - model.owner = user.username; + model.addOwner(user.username); String oldRepositoryName = model.name; model.name = "~" + user.username + model.name.substring(model.projectPath.length()); model.projectPath = "~" + user.username; updateRepositoryModel(oldRepositoryName, model, false); } else if (model.isOwner(username)) { // common/shared repo - model.owner = user.username; + model.addOwner(user.username); updateRepositoryModel(model.name, model, false); } } @@ -823,25 +989,23 @@ } /** - * Returns the list of teams and their access permissions for the specified repository. + * Returns the list of teams and their access permissions for the specified + * repository including the source of the permission such as the admin flag + * or a regular expression. * * @param repository - * @return a list of Team-AccessPermission tuples + * @return a list of RegistrantAccessPermissions */ public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) { - List<RegistrantAccessPermission> permissions = new ArrayList<RegistrantAccessPermission>(); - for (String team : userService.getTeamnamesForRepositoryRole(repository.name)) { - TeamModel model = userService.getTeamModel(team); - AccessPermission ap = model.getRepositoryPermission(repository); - PermissionType pType = PermissionType.REGEX; - boolean editable = false; - if (model.hasExplicitRepositoryPermission(repository.name)) { - pType = PermissionType.EXPLICIT; - editable = true; + List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(); + for (TeamModel team : userService.getAllTeams()) { + RegistrantAccessPermission ap = team.getRepositoryPermission(repository); + if (ap.permission.exceeds(AccessPermission.NONE)) { + list.add(ap); } - permissions.add(new RegistrantAccessPermission(team, ap, pType, RegistrantType.TEAM, editable)); } - return permissions; + Collections.sort(list); + return list; } /** @@ -854,7 +1018,7 @@ public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) { List<TeamModel> teams = new ArrayList<TeamModel>(); for (RegistrantAccessPermission tp : permissions) { - if (tp.isEditable) { + if (tp.mutable) { // only set explicitly defined access permissions TeamModel team = userService.getTeamModel(tp.registrant); team.setRepositoryPermission(repository.name, tp.permission); @@ -932,7 +1096,7 @@ */ private void addToCachedRepositoryList(RepositoryModel model) { if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { - repositoryListCache.put(model.name, model); + repositoryListCache.put(model.name.toLowerCase(), model); // update the fork origin repository with this repository clone if (!StringUtils.isEmpty(model.originRepository)) { @@ -954,7 +1118,7 @@ if (StringUtils.isEmpty(name)) { return null; } - return repositoryListCache.remove(name); + return repositoryListCache.remove(name.toLowerCase()); } /** @@ -1045,6 +1209,16 @@ // update cache for (String repository : repositories) { getRepositoryModel(repository); + } + } + + // rebuild fork networks + for (RepositoryModel model : repositoryListCache.values()) { + if (!StringUtils.isEmpty(model.originRepository)) { + if (repositoryListCache.containsKey(model.originRepository)) { + RepositoryModel origin = repositoryListCache.get(model.originRepository); + origin.addFork(model.name); + } } } @@ -1178,7 +1352,7 @@ } // cached model - RepositoryModel model = repositoryListCache.get(repositoryName); + RepositoryModel model = repositoryListCache.get(repositoryName.toLowerCase()); if (gcExecutor.isCollectingGarbage(model.name)) { // Gitblit is busy collecting garbage, use our cached model @@ -1188,7 +1362,7 @@ } // check for updates - Repository r = getRepository(repositoryName); + Repository r = getRepository(model.name); if (r == null) { // repository is missing removeFromCachedRepositoryList(repositoryName); @@ -1200,8 +1374,8 @@ if (config.isOutdated()) { // reload model logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName)); - model = loadRepositoryModel(repositoryName); - removeFromCachedRepositoryList(repositoryName); + model = loadRepositoryModel(model.name); + removeFromCachedRepositoryList(model.name); addToCachedRepositoryList(model); } else { // update a few repository parameters @@ -1251,7 +1425,30 @@ } project.title = projectConfigs.getString("project", name, "title"); project.description = projectConfigs.getString("project", name, "description"); - configs.put(name.toLowerCase(), project); + + // project markdown + File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/project.mkd"); + if (pmkd.exists()) { + Date lm = new Date(pmkd.lastModified()); + if (!projectMarkdownCache.hasCurrent(name, lm)) { + String mkd = com.gitblit.utils.FileUtils.readContent(pmkd, "\n"); + projectMarkdownCache.updateObject(name, lm, mkd); + } + project.projectMarkdown = projectMarkdownCache.getObject(name); + } + + // project repositories markdown + File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/repositories.mkd"); + if (rmkd.exists()) { + Date lm = new Date(rmkd.lastModified()); + if (!projectRepositoriesMarkdownCache.hasCurrent(name, lm)) { + String mkd = com.gitblit.utils.FileUtils.readContent(rmkd, "\n"); + projectRepositoriesMarkdownCache.updateObject(name, lm, mkd); + } + project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(name); + } + + configs.put(name.toLowerCase(), project); } projectCache.clear(); projectCache.putAll(configs); @@ -1375,6 +1572,49 @@ } /** + * Returns the list of project models that are referenced by the supplied + * repository model list. This is an alternative method exists to ensure + * Gitblit does not call getRepositoryModels(UserModel) twice in a request. + * + * @param repositoryModels + * @param includeUsers + * @return a list of project models + */ + public List<ProjectModel> getProjectModels(List<RepositoryModel> repositoryModels, boolean includeUsers) { + Map<String, ProjectModel> projects = new LinkedHashMap<String, ProjectModel>(); + for (RepositoryModel repository : repositoryModels) { + if (!includeUsers && repository.isPersonalRepository()) { + // exclude personal repositories + continue; + } + if (!projects.containsKey(repository.projectPath)) { + ProjectModel project = getProjectModel(repository.projectPath); + if (project == null) { + logger.warn(MessageFormat.format("excluding project \"{0}\" from project list because it is empty!", + repository.projectPath)); + continue; + } + projects.put(repository.projectPath, project); + // clear the repo list in the project because that is the system + // list, not the user-accessible list and start building the + // user-accessible list + project.repositories.clear(); + project.repositories.add(repository.name); + project.lastChange = repository.lastChange; + } else { + // update the user-accessible list + // this is used for repository count + ProjectModel project = projects.get(repository.projectPath); + project.repositories.add(repository.name); + if (project.lastChange.before(repository.lastChange)) { + project.lastChange = repository.lastChange; + } + } + } + return new ArrayList<ProjectModel>(projects.values()); + } + + /** * Workaround JGit. I need to access the raw config object directly in order * to see if the config is dirty so that I can reload a repository model. * If I use the stock JGit method to get the config it already reloads the @@ -1410,7 +1650,7 @@ } RepositoryModel model = new RepositoryModel(); model.isBare = r.isBare(); - File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "git"); + File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git"); if (model.isBare) { model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory()); } else { @@ -1425,7 +1665,7 @@ if (config != null) { model.description = getConfig(config, "description", ""); - model.owner = getConfig(config, "owner", ""); + model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", ""))); model.useTickets = getConfig(config, "useTickets", false); model.useDocs = getConfig(config, "useDocs", false); model.allowForks = getConfig(config, "allowForks", true); @@ -1445,12 +1685,13 @@ Constants.CONFIG_GITBLIT, null, "federationSets"))); model.isFederated = getConfig(config, "isFederated", false); model.gcThreshold = getConfig(config, "gcThreshold", settings.getString(Keys.git.defaultGarbageCollectionThreshold, "500KB")); - model.gcPeriod = getConfig(config, "gcPeriod", settings.getString(Keys.git.defaultGarbageCollectionPeriod, "7 days")); + model.gcPeriod = getConfig(config, "gcPeriod", settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7)); try { model.lastGC = new SimpleDateFormat(Constants.ISO8601).parse(getConfig(config, "lastGC", "1970-01-01'T'00:00:00Z")); } catch (Exception e) { model.lastGC = new Date(0); } + model.maxActivityCommits = getConfig(config, "maxActivityCommits", settings.getInteger(Keys.web.maxActivityCommits, 0)); model.origin = config.getString("remote", "origin", "url"); if (model.origin != null) { model.origin = model.origin.replace('\\', '/'); @@ -1472,6 +1713,7 @@ } model.HEAD = JGitUtils.getHEADRef(r); model.availableRefs = JGitUtils.getAvailableHeadTargets(r); + model.sparkleshareId = JGitUtils.getSparkleshareId(r); r.close(); if (model.origin != null && model.origin.startsWith("file://")) { @@ -1483,7 +1725,7 @@ // ensure origin still exists File repoFolder = new File(getRepositoriesFolder(), originRepo); if (repoFolder.exists()) { - model.originRepository = originRepo; + model.originRepository = originRepo.toLowerCase(); } } } catch (URISyntaxException e) { @@ -1500,10 +1742,21 @@ * @return true if the repository exists */ public boolean hasRepository(String repositoryName) { - if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { + return hasRepository(repositoryName, false); + } + + /** + * Determines if this server has the requested repository. + * + * @param name + * @param caseInsensitive + * @return true if the repository exists + */ + public boolean hasRepository(String repositoryName, boolean caseSensitiveCheck) { + if (!caseSensitiveCheck && settings.getBoolean(Keys.git.cacheRepositoryList, true)) { // if we are caching use the cache to determine availability // otherwise we end up adding a phantom repository to the cache - return repositoryListCache.containsKey(repositoryName); + return repositoryListCache.containsKey(repositoryName.toLowerCase()); } Repository r = getRepository(repositoryName, false); if (r == null) { @@ -1561,7 +1814,7 @@ } for (String repository : repositoryListCache.keySet()) { - if (repository.toLowerCase().startsWith(userPath)) { + if (repository.startsWith(userPath)) { RepositoryModel model = repositoryListCache.get(repository); if (!StringUtils.isEmpty(model.originRepository)) { if (roots.contains(model.originRepository)) { @@ -1575,8 +1828,8 @@ // not caching ProjectModel project = getProjectModel(userProject); for (String repository : project.repositories) { - if (repository.toLowerCase().startsWith(userProject)) { - RepositoryModel model = repositoryListCache.get(repository); + if (repository.startsWith(userProject)) { + RepositoryModel model = getRepositoryModel(repository); if (model.originRepository.equalsIgnoreCase(origin)) { // user has a fork return model.name; @@ -1597,24 +1850,53 @@ */ public ForkModel getForkNetwork(String repository) { if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) { - // find the root - RepositoryModel model = repositoryListCache.get(repository); + // find the root, cached + RepositoryModel model = repositoryListCache.get(repository.toLowerCase()); while (model.originRepository != null) { model = repositoryListCache.get(model.originRepository); + } + ForkModel root = getForkModelFromCache(model.name); + return root; + } else { + // find the root, non-cached + RepositoryModel model = getRepositoryModel(repository.toLowerCase()); + while (model.originRepository != null) { + model = getRepositoryModel(model.originRepository); } ForkModel root = getForkModel(model.name); return root; } - return null; + } + + private ForkModel getForkModelFromCache(String repository) { + RepositoryModel model = repositoryListCache.get(repository.toLowerCase()); + if (model == null) { + return null; + } + ForkModel fork = new ForkModel(model); + if (!ArrayUtils.isEmpty(model.forks)) { + for (String aFork : model.forks) { + ForkModel fm = getForkModelFromCache(aFork); + if (fm != null) { + fork.forks.add(fm); + } + } + } + return fork; } private ForkModel getForkModel(String repository) { - RepositoryModel model = repositoryListCache.get(repository); + RepositoryModel model = getRepositoryModel(repository.toLowerCase()); + if (model == null) { + return null; + } ForkModel fork = new ForkModel(model); if (!ArrayUtils.isEmpty(model.forks)) { for (String aFork : model.forks) { ForkModel fm = getForkModel(aFork); - fork.forks.add(fm); + if (fm != null) { + fork.forks.add(fm); + } } } return fork; @@ -1730,6 +2012,27 @@ private boolean getConfig(StoredConfig config, String field, boolean defaultValue) { return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue); } + + /** + * Returns the gitblit string value for the specified key. If key is not + * set, returns defaultValue. + * + * @param config + * @param field + * @param defaultValue + * @return field value or defaultValue + */ + private int getConfig(StoredConfig config, String field, int defaultValue) { + String value = config.getString(Constants.CONFIG_GITBLIT, null, field); + if (StringUtils.isEmpty(value)) { + return defaultValue; + } + try { + return Integer.parseInt(value); + } catch (Exception e) { + } + return defaultValue; + } /** * Creates/updates the repository model keyed by reopsitoryName. Saves all @@ -1764,7 +2067,7 @@ if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) { repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT; } - if (new File(repositoriesFolder, repository.name).exists()) { + if (hasRepository(repository.name)) { throw new GitBlitException(MessageFormat.format( "Can not create repository ''{0}'' because it already exists.", repository.name)); @@ -1880,7 +2183,7 @@ public void updateConfiguration(Repository r, RepositoryModel repository) { StoredConfig config = r.getConfig(); config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description); - config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner); + config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners)); config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets); config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs); config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks); @@ -1896,9 +2199,20 @@ repository.federationStrategy.name()); config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated); config.setString(Constants.CONFIG_GITBLIT, null, "gcThreshold", repository.gcThreshold); - config.setString(Constants.CONFIG_GITBLIT, null, "gcPeriod", repository.gcPeriod); + if (repository.gcPeriod == settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7)) { + // use default from config + config.unset(Constants.CONFIG_GITBLIT, null, "gcPeriod"); + } else { + config.setInt(Constants.CONFIG_GITBLIT, null, "gcPeriod", repository.gcPeriod); + } if (repository.lastGC != null) { config.setString(Constants.CONFIG_GITBLIT, null, "lastGC", new SimpleDateFormat(Constants.ISO8601).format(repository.lastGC)); + } + if (repository.maxActivityCommits == settings.getInteger(Keys.web.maxActivityCommits, 0)) { + // use default from config + config.unset(Constants.CONFIG_GITBLIT, null, "maxActivityCommits"); + } else { + config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits); } updateList(config, "federationSets", repository.federationSets); @@ -2195,6 +2509,8 @@ case PULL_SETTINGS: case PULL_SCRIPTS: return token.equals(all); + default: + break; } return false; } @@ -2337,6 +2653,8 @@ if (!StringUtils.isEmpty(model.origin)) { url = model.origin; } + break; + default: break; } @@ -2595,6 +2913,37 @@ } /** + * Notify users by email of something. + * + * @param subject + * @param message + * @param toAddresses + */ + public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) { + this.sendHtmlMail(subject, message, toAddresses.toArray(new String[0])); + } + + /** + * Notify users by email of something. + * + * @param subject + * @param message + * @param toAddresses + */ + public void sendHtmlMail(String subject, String message, String... toAddresses) { + try { + Message mail = mailExecutor.createMessage(toAddresses); + if (mail != null) { + mail.setSubject(subject); + mail.setContent(message, "text/html"); + mailExecutor.queue(mail); + } + } catch (MessagingException e) { + logger.error("Messaging error", e); + } + } + + /** * Returns the descriptions/comments of the Gitblit config settings. * * @return SettingsModel @@ -2692,18 +3041,21 @@ * * @param settings */ - public void configureContext(IStoredSettings settings, boolean startFederation) { - logger.info("Reading configuration from " + settings.toString()); + public void configureContext(IStoredSettings settings, File folder, boolean startFederation) { this.settings = settings; - + this.baseFolder = folder; + + repositoriesFolder = getRepositoriesFolder(); + + logger.info("Gitblit base folder = " + folder.getAbsolutePath()); + logger.info("Git repositories folder = " + repositoriesFolder.getAbsolutePath()); + logger.info("Gitblit settings = " + settings.toString()); + // prepare service executors mailExecutor = new MailExecutor(settings); luceneExecutor = new LuceneExecutor(settings, repositoriesFolder); gcExecutor = new GCExecutor(settings); - repositoriesFolder = getRepositoriesFolder(); - logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath()); - // calculate repository list settings checksum for future config changes repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum()); @@ -2719,7 +3071,7 @@ serverStatus = new ServerStatus(isGO()); if (this.userService == null) { - String realm = settings.getString(Keys.realm.userService, "users.properties"); + String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties"); IUserService loginService = null; try { // check to see if this "file" is a login service class @@ -2732,7 +3084,7 @@ } // load and cache the project metadata - projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect()); + projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "${baseFolder}/projects.conf"), FS.detect()); getProjectConfigs(); // schedule mail engine @@ -2798,6 +3150,32 @@ } ContainerUtils.CVE_2007_0450.test(); + + // startup Fanout PubSub service + if (settings.getInteger(Keys.fanout.port, 0) > 0) { + String bindInterface = settings.getString(Keys.fanout.bindInterface, null); + int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT); + boolean useNio = settings.getBoolean(Keys.fanout.useNio, true); + int limit = settings.getInteger(Keys.fanout.connectionLimit, 0); + + if (useNio) { + if (StringUtils.isEmpty(bindInterface)) { + fanoutService = new FanoutNioService(port); + } else { + fanoutService = new FanoutNioService(bindInterface, port); + } + } else { + if (StringUtils.isEmpty(bindInterface)) { + fanoutService = new FanoutSocketService(port); + } else { + fanoutService = new FanoutSocketService(bindInterface, port); + } + } + + fanoutService.setConcurrentConnectionLimit(limit); + fanoutService.setAllowAllChannelAnnouncements(false); + fanoutService.start(); + } } private void logTimezone(String type, TimeZone zone) { @@ -2821,41 +3199,63 @@ public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) { servletContext = contextEvent.getServletContext(); if (settings == null) { - // Gitblit WAR is running in a servlet container + // Gitblit is running in a servlet container ServletContext context = contextEvent.getServletContext(); WebXmlSettings webxmlSettings = new WebXmlSettings(context); - - // 0.7.0 web.properties in the deployed war folder - String webProps = context.getRealPath("/WEB-INF/web.properties"); - if (!StringUtils.isEmpty(webProps)) { - File overrideFile = new File(webProps); - if (overrideFile.exists()) { - webxmlSettings.applyOverrides(overrideFile); - } - } + File contextFolder = new File(context.getRealPath("/")); + String openShift = System.getenv("OPENSHIFT_DATA_DIR"); + if (!StringUtils.isEmpty(openShift)) { + // Gitblit is running in OpenShift/JBoss + File base = new File(openShift); - // 0.8.0 gitblit.properties file located outside the deployed war - // folder lie, for example, on RedHat OpenShift. - File overrideFile = getFileOrFolder("gitblit.properties"); - if (!overrideFile.getPath().equals("gitblit.properties")) { + // gitblit.properties setting overrides + File overrideFile = new File(base, "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)); + + // Copy the included scripts to the configured groovy folder + File localScripts = new File(base, webxmlSettings.getString(Keys.groovy.scriptsFolder, "groovy")); + if (!localScripts.exists()) { + File warScripts = new File(contextFolder, "/WEB-INF/data/groovy"); + if (!warScripts.equals(localScripts)) { + try { + com.gitblit.utils.FileUtils.copy(localScripts, warScripts.listFiles()); + } catch (IOException e) { + logger.error(MessageFormat.format( + "Failed to copy included Groovy scripts from {0} to {1}", + warScripts, localScripts)); + } } } + + // configure context using the web.xml + configureContext(webxmlSettings, base, true); + } else { + // Gitblit is running in a standard servlet container + logger.info("WAR contextFolder is " + contextFolder.getAbsolutePath()); + + String path = webxmlSettings.getString(Constants.baseFolder, Constants.contextFolder$ + "/WEB-INF/data"); + File base = com.gitblit.utils.FileUtils.resolveParameter(Constants.contextFolder$, contextFolder, path); + base.mkdirs(); + + // try to copy the data folder contents to the baseFolder + File localSettings = new File(base, "gitblit.properties"); + if (!localSettings.exists()) { + File contextData = new File(contextFolder, "/WEB-INF/data"); + if (!base.equals(contextData)) { + try { + com.gitblit.utils.FileUtils.copy(base, contextData.listFiles()); + } catch (IOException e) { + logger.error(MessageFormat.format( + "Failed to copy included data from {0} to {1}", + contextData, base)); + } + } + } + + // delegate all config to baseFolder/gitblit.properties file + FileSettings settings = new FileSettings(localSettings.getAbsolutePath()); + configureContext(settings, base, true); } } @@ -2873,6 +3273,17 @@ scheduledExecutor.shutdownNow(); luceneExecutor.close(); gcExecutor.close(); + if (fanoutService != null) { + fanoutService.stop(); + } + } + + /** + * + * @return true if we are running the gc executor + */ + public boolean isCollectingGarbage() { + return gcExecutor.isRunning(); } /** @@ -2909,15 +3320,17 @@ // create a Gitblit repository model for the clone RepositoryModel cloneModel = repository.cloneAs(cloneName); // owner has REWIND/RW+ permissions - cloneModel.owner = user.username; + cloneModel.addOwner(user.username); updateRepositoryModel(cloneName, cloneModel, false); // add the owner of the source repository to the clone's access list - if (!StringUtils.isEmpty(repository.owner)) { - UserModel originOwner = getUserModel(repository.owner); - if (originOwner != null) { - originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE); - updateUserModel(originOwner.username, originOwner, false); + if (!ArrayUtils.isEmpty(repository.owners)) { + for (String owner : repository.owners) { + UserModel originOwner = getUserModel(owner); + if (originOwner != null) { + originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE); + updateUserModel(originOwner.username, originOwner, false); + } } } -- Gitblit v1.9.1