From 0e44acbb2fec928a1606dc60f427a148fff405c9 Mon Sep 17 00:00:00 2001
From: Mohamed Ragab <moragab@gmail.com>
Date: Wed, 02 May 2012 11:15:01 -0400
Subject: [PATCH] Added a script to facilitate setting the proxy host and port and no proxy hosts, and then it concatenates all the java system properties for setting the java proxy configurations and puts the resulting string in an environment variable JAVA_PROXY_CONFIG, modified the scirpts gitblit,  gitblit-ubuntu, and gitblit-centos to source the java-proxy-config.sh script and then include the resulting java proxy configuration in the java command

---
 src/com/gitblit/GitBlit.java |  589 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 550 insertions(+), 39 deletions(-)

diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 8db72af..565b024 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -23,13 +23,20 @@
 import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 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.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.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -66,10 +73,13 @@
 import com.gitblit.models.FederationSet;
 import com.gitblit.models.Metric;
 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.FederationUtils;
 import com.gitblit.utils.JGitUtils;
@@ -97,7 +107,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);
@@ -117,8 +127,6 @@
 
 	private File repositoriesFolder;
 
-	private boolean exportAll = true;
-
 	private IUserService userService;
 
 	private IStoredSettings settings;
@@ -128,6 +136,10 @@
 	private ServerStatus serverStatus;
 
 	private MailExecutor mailExecutor;
+	
+	private LuceneExecutor luceneExecutor;
+	
+	private TimeZone timezone;
 
 	public GitBlit() {
 		if (gitblit == null) {
@@ -156,6 +168,24 @@
 	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 boolean value for the specified key. If the key does not
@@ -247,6 +277,64 @@
 	}
 
 	/**
+	 * Returns the file object for the specified configuration key.
+	 * 
+	 * @return the file
+	 */
+	public static File getFileOrFolder(String key, String defaultFileOrFolder) {
+		String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
+		return getFileOrFolder(fileOrFolder);
+	}
+
+	/**
+	 * Returns the file object which may have it's base-path determined by
+	 * environment variables for running on a cloud hosting service. All Gitblit
+	 * file or folder retrievals are (at least initially) funneled through this
+	 * method so it is the correct point to globally override/alter filesystem
+	 * access based on environment or some other indicator.
+	 * 
+	 * @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);
+	}
+
+	/**
+	 * Returns the path of the repositories folder. This method checks to see if
+	 * Gitblit is running on a cloud service and may return an adjusted path.
+	 * 
+	 * @return the repositories folder path
+	 */
+	public static File getRepositoriesFolder() {
+		return getFileOrFolder(Keys.git.repositoriesFolder, "git");
+	}
+
+	/**
+	 * Returns the path of the proposals folder. This method checks to see if
+	 * Gitblit is running on a cloud service and may return an adjusted path.
+	 * 
+	 * @return the proposals folder path
+	 */
+	public static File getProposalsFolder() {
+		return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
+	}
+
+	/**
+	 * Returns the path of the Groovy folder. This method checks to see if
+	 * Gitblit is running on a cloud service and may return an adjusted path.
+	 * 
+	 * @return the Groovy scripts folder path
+	 */
+	public static File getGroovyScriptsFolder() {
+		return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
+	}
+
+	/**
 	 * Updates the list of server settings.
 	 * 
 	 * @param settings
@@ -288,6 +376,38 @@
 		logger.info("Setting up user service " + userService.toString());
 		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();
 	}
 
 	/**
@@ -377,6 +497,18 @@
 			response.addCookie(userCookie);
 		}
 	}
+	
+	/**
+	 * Logout a user.
+	 * 
+	 * @param user
+	 */
+	public void logout(UserModel user) {
+		if (userService == null) {
+			return;
+		}
+		userService.logout(user);
+	}
 
 	/**
 	 * Returns the list of all users available to the login service.
@@ -386,8 +518,18 @@
 	 */
 	public List<String> getAllUsernames() {
 		List<String> names = new ArrayList<String>(userService.getAllUsernames());
-		Collections.sort(names);
 		return names;
+	}
+
+	/**
+	 * Returns the list of all users available to the login service.
+	 * 
+	 * @see IUserService.getAllUsernames()
+	 * @return list of all usernames
+	 */
+	public List<UserModel> getAllUsers() {
+		List<UserModel> users = userService.getAllUsers();
+		return users;
 	}
 
 	/**
@@ -463,6 +605,95 @@
 	}
 
 	/**
+	 * Returns the list of available teams that a user or repository may be
+	 * assigned to.
+	 * 
+	 * @return the list of teams
+	 */
+	public List<String> getAllTeamnames() {
+		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.
+	 * 
+	 * @return the list of teams
+	 */
+	public List<TeamModel> getAllTeams() {
+		List<TeamModel> teams = userService.getAllTeams();
+		return teams;
+	}
+
+	/**
+	 * Returns the TeamModel object for the specified name.
+	 * 
+	 * @param teamname
+	 * @return a TeamModel object or null
+	 */
+	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.
+	 * 
+	 * @see IUserService.getTeamnamesForRepositoryRole(String)
+	 * @param repository
+	 * @return list of all teamnames that can bypass the access restriction
+	 */
+	public List<String> getRepositoryTeams(RepositoryModel repository) {
+		return userService.getTeamnamesForRepositoryRole(repository.name);
+	}
+
+	/**
+	 * Sets the list of all uses who are allowed to bypass the access
+	 * restriction placed on the specified repository.
+	 * 
+	 * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
+	 * @param repository
+	 * @param teamnames
+	 * @return true if successful
+	 */
+	public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
+		return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
+	}
+
+	/**
+	 * Updates the TeamModel object for the specified name.
+	 * 
+	 * @param teamname
+	 * @param team
+	 * @param isCreate
+	 */
+	public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
+			throws GitBlitException {
+		if (!teamname.equalsIgnoreCase(team.name)) {
+			if (userService.getTeamModel(team.name) != null) {
+				throw new GitBlitException(MessageFormat.format(
+						"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
+						team.name));
+			}
+		}
+		if (!userService.updateTeamModel(teamname, team)) {
+			throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
+		}
+	}
+
+	/**
+	 * Delete the team object with the specified teamname
+	 * 
+	 * @see IUserService.deleteTeam(String)
+	 * @param teamname
+	 * @return true if successful
+	 */
+	public boolean deleteTeam(String teamname) {
+		return userService.deleteTeam(teamname);
+	}
+
+	/**
 	 * Clears all the cached data for the specified repository.
 	 * 
 	 * @param repositoryName
@@ -479,7 +710,8 @@
 	 * @return list of all repositories
 	 */
 	public List<String> getRepositoryList() {
-		return JGitUtils.getRepositoryList(repositoriesFolder, exportAll,
+		return JGitUtils.getRepositoryList(repositoriesFolder, 
+				settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
 				settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
 	}
 
@@ -490,21 +722,38 @@
 	 * @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) {
 		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());
+			if (logError) {
+				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);
+			if (logError) {
+				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);
+			if (logError) {
+				logger.error("GitBlit.getRepository(String) failed to find "
+						+ new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
+			}
 		}
 		return r;
 	}
@@ -555,7 +804,7 @@
 			return null;
 		}
 		if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
-			if (user != null && user.canAccessRepository(model.name)) {
+			if (user != null && user.canAccessRepository(model)) {
 				return model;
 			}
 			return null;
@@ -579,7 +828,8 @@
 		RepositoryModel model = new RepositoryModel();
 		model.name = repositoryName;
 		model.hasCommits = JGitUtils.hasCommits(r);
-		model.lastChange = JGitUtils.getLastChange(r, null);
+		model.lastChange = JGitUtils.getLastChange(r);
+		model.isBare = r.isBare();
 		StoredConfig config = JGitUtils.readConfig(r);
 		if (config != null) {
 			model.description = getConfig(config, "description", "");
@@ -599,7 +849,17 @@
 					"gitblit", null, "federationSets")));
 			model.isFederated = getConfig(config, "isFederated", false);
 			model.origin = config.getString("remote", "origin", "url");
+			model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
+					"gitblit", null, "preReceiveScript")));
+			model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
+					"gitblit", null, "postReceiveScript")));
+			model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
+					"gitblit", null, "mailingList")));
+			model.indexedBranches = new ArrayList<String>(Arrays.asList(config.getStringList(
+					"gitblit", null, "indexBranch")));
 		}
+		model.HEAD = JGitUtils.getHEADRef(r);
+		model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
 		r.close();
 		return model;
 	}
@@ -656,6 +916,9 @@
 				repository.close();
 			}
 		}
+		
+		// close any open index writer/searcher in the Lucene executor
+		luceneExecutor.close(repositoryName);
 	}
 
 	/**
@@ -672,7 +935,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);
 	}
@@ -758,6 +1021,11 @@
 									.format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
 											repositoryName, repository.name));
 				}
+				File parentFile = destFolder.getParentFile();
+				if (!parentFile.exists() && !parentFile.mkdirs()) {
+					throw new GitBlitException(MessageFormat.format(
+							"Failed to create folder ''{0}''", parentFile.getAbsolutePath()));
+				}
 				if (!folder.renameTo(destFolder)) {
 					throw new GitBlitException(MessageFormat.format(
 							"Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
@@ -790,6 +1058,18 @@
 		// 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
+					clearRepositoryCache(repository.name);
+				}
+			}
+
+			// close the repository object
 			r.close();
 		}
 	}
@@ -814,14 +1094,33 @@
 		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",
 				repository.federationStrategy.name());
 		config.setBoolean("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);
+
 		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("gitblit", null, field);
+		} else {
+			config.setStringList("gitblit", null, field, list);
 		}
 	}
 
@@ -1067,8 +1366,10 @@
 		case PULL_REPOSITORIES:
 			return token.equals(all) || token.equals(unr) || token.equals(jur);
 		case PULL_USERS:
+		case PULL_TEAMS:
 			return token.equals(all) || token.equals(unr);
 		case PULL_SETTINGS:
+		case PULL_SCRIPTS:
 			return token.equals(all);
 		}
 		return false;
@@ -1120,8 +1421,7 @@
 
 		try {
 			// make the proposals folder
-			File proposalsFolder = new File(getString(Keys.federation.proposalsFolder, "proposals")
-					.trim());
+			File proposalsFolder = getProposalsFolder();
 			proposalsFolder.mkdirs();
 
 			// cache json to a file
@@ -1153,7 +1453,7 @@
 	 */
 	public List<FederationProposal> getPendingFederationProposals() {
 		List<FederationProposal> list = new ArrayList<FederationProposal>();
-		File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
+		File folder = getProposalsFolder();
 		if (folder.exists()) {
 			File[] files = folder.listFiles(new FileFilter() {
 				@Override
@@ -1276,9 +1576,148 @@
 	 * @return true if the proposal was deleted
 	 */
 	public boolean deletePendingFederationProposal(FederationProposal proposal) {
-		File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
+		File folder = getProposalsFolder();
 		File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
 		return file.delete();
+	}
+
+	/**
+	 * Returns the list of all Groovy push hook scripts. Script files must have
+	 * .groovy extension
+	 * 
+	 * @return list of available hook scripts
+	 */
+	public List<String> getAllScripts() {
+		File groovyFolder = getGroovyScriptsFolder();
+		File[] files = groovyFolder.listFiles(new FileFilter() {
+			@Override
+			public boolean accept(File pathname) {
+				return pathname.isFile() && pathname.getName().endsWith(".groovy");
+			}
+		});
+		List<String> scripts = new ArrayList<String>();
+		if (files != null) {
+			for (File file : files) {
+				String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
+				scripts.add(script);
+			}
+		}
+		return scripts;
+	}
+
+	/**
+	 * Returns the list of pre-receive scripts the repository inherited from the
+	 * global settings and team affiliations.
+	 * 
+	 * @param repository
+	 *            if null only the globally specified scripts are returned
+	 * @return a list of scripts
+	 */
+	public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
+		Set<String> scripts = new LinkedHashSet<String>();
+		// Globals
+		for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
+			if (script.endsWith(".groovy")) {
+				scripts.add(script.substring(0, script.lastIndexOf('.')));
+			} else {
+				scripts.add(script);
+			}
+		}
+
+		// Team Scripts
+		if (repository != null) {
+			for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
+				TeamModel team = userService.getTeamModel(teamname);
+				scripts.addAll(team.preReceiveScripts);
+			}
+		}
+		return new ArrayList<String>(scripts);
+	}
+
+	/**
+	 * Returns the list of all available Groovy pre-receive push hook scripts
+	 * that are not already inherited by the repository. Script files must have
+	 * .groovy extension
+	 * 
+	 * @param repository
+	 *            optional parameter
+	 * @return list of available hook scripts
+	 */
+	public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
+		Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
+
+		// create list of available scripts by excluding inherited scripts
+		List<String> scripts = new ArrayList<String>();
+		for (String script : getAllScripts()) {
+			if (!inherited.contains(script)) {
+				scripts.add(script);
+			}
+		}
+		return scripts;
+	}
+
+	/**
+	 * Returns the list of post-receive scripts the repository inherited from
+	 * the global settings and team affiliations.
+	 * 
+	 * @param repository
+	 *            if null only the globally specified scripts are returned
+	 * @return a list of scripts
+	 */
+	public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
+		Set<String> scripts = new LinkedHashSet<String>();
+		// Global Scripts
+		for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
+			if (script.endsWith(".groovy")) {
+				scripts.add(script.substring(0, script.lastIndexOf('.')));
+			} else {
+				scripts.add(script);
+			}
+		}
+		// Team Scripts
+		if (repository != null) {
+			for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
+				TeamModel team = userService.getTeamModel(teamname);
+				scripts.addAll(team.postReceiveScripts);
+			}
+		}
+		return new ArrayList<String>(scripts);
+	}
+
+	/**
+	 * Returns the list of unused Groovy post-receive push hook scripts that are
+	 * not already inherited by the repository. Script files must have .groovy
+	 * extension
+	 * 
+	 * @param repository
+	 *            optional parameter
+	 * @return list of available hook scripts
+	 */
+	public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
+		Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
+
+		// create list of available scripts by excluding inherited scripts
+		List<String> scripts = new ArrayList<String>();
+		for (String script : getAllScripts()) {
+			if (!inherited.contains(script)) {
+				scripts.add(script);
+			}
+		}
+		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;
 	}
 
 	/**
@@ -1287,9 +1726,40 @@
 	 * @param subject
 	 * @param message
 	 */
-	public void notifyAdministrators(String subject, String message) {
+	public void sendMailToAdministrators(String subject, String message) {
 		try {
 			Message mail = mailExecutor.createMessageForAdministrators();
+			if (mail != null) {
+				mail.setSubject(subject);
+				mail.setText(message);
+				mailExecutor.queue(mail);
+			}
+		} catch (MessagingException e) {
+			logger.error("Messaging error", e);
+		}
+	}
+
+	/**
+	 * Notify users by email of something.
+	 * 
+	 * @param subject
+	 * @param message
+	 * @param toAddresses
+	 */
+	public void sendMail(String subject, String message, Collection<String> toAddresses) {
+		this.sendMail(subject, message, toAddresses.toArray(new String[0]));
+	}
+
+	/**
+	 * Notify users by email of something.
+	 * 
+	 * @param subject
+	 * @param message
+	 * @param toAddresses
+	 */
+	public void sendMail(String subject, String message, String... toAddresses) {
+		try {
+			Message mail = mailExecutor.createMessage(toAddresses);
 			if (mail != null) {
 				mail.setSubject(subject);
 				mail.setText(message);
@@ -1313,6 +1783,7 @@
 				setting.currentValue = settings.getString(key, "");
 			}
 		}
+		settingsModel.pushScripts = getAllScripts();
 		return settingsModel;
 	}
 
@@ -1325,6 +1796,10 @@
 	 */
 	private ServerSettings loadSettingModels() {
 		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
@@ -1391,42 +1866,44 @@
 	public void configureContext(IStoredSettings settings, boolean startFederation) {
 		logger.info("Reading configuration from " + settings.toString());
 		this.settings = settings;
-		repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git"));
+		repositoriesFolder = getRepositoriesFolder();
 		logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
-		repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
+		repositoryResolver = new FileResolver<Void>(repositoriesFolder, true);
+		
+		logTimezone("JVM", TimeZone.getDefault());
+		logTimezone(Constants.NAME, getTimezone());
+
 		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();
-			}
+			loginService = (IUserService) realmClass.newInstance();
 		} catch (Throwable t) {
-			// not a login service class or class could not be instantiated.
-			// try to use default file login service
-			File realmFile = new File(realm);
-			if (!realmFile.exists()) {
-				try {
-					realmFile.createNewFile();
-				} catch (IOException x) {
-					logger.error(
-							MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
-				}
-			}
-			loginService = new FileUserService(realmFile);
+			loginService = new GitblitUserService();
 		}
 		setUserService(loginService);
 		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();
-		}
+		}		
+	}
+	
+	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 + ")");
 	}
 
 	/**
@@ -1441,8 +1918,41 @@
 		settingsModel = loadSettingModels();
 		if (settings == null) {
 			// Gitblit WAR is running in a servlet container
-			WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
+			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);
+				}
+			}
+			
+
+			// 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")) {
+				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));
+					}
+				}
+			}
 		}
 
 		serverStatus.servletContainer = servletContext.getServerInfo();
@@ -1456,5 +1966,6 @@
 	public void contextDestroyed(ServletContextEvent contextEvent) {
 		logger.info("Gitblit context destroyed by servlet container.");
 		scheduledExecutor.shutdownNow();
+		luceneExecutor.close();
 	}
 }

--
Gitblit v1.9.1