James Moger
2011-10-02 f762b160efd5cafd919a6fd7f9587f578eceb454
commit | author | age
f13c4c 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
fc948c 16 package com.gitblit;
JM 17
18 import java.io.File;
831469 19 import java.io.FileFilter;
f97bf0 20 import java.io.IOException;
dd6f08 21 import java.lang.reflect.Field;
166e6a 22 import java.text.MessageFormat;
fc948c 23 import java.util.ArrayList;
8f73a7 24 import java.util.Arrays;
00afd7 25 import java.util.Collections;
8c9a20 26 import java.util.HashMap;
fc948c 27 import java.util.List;
8c9a20 28 import java.util.Map;
JM 29 import java.util.Map.Entry;
831469 30 import java.util.concurrent.ConcurrentHashMap;
JM 31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.TimeUnit;
dd6f08 34 import java.util.concurrent.atomic.AtomicInteger;
fc948c 35
831469 36 import javax.mail.Message;
JM 37 import javax.mail.MessagingException;
87cc1e 38 import javax.servlet.ServletContextEvent;
JM 39 import javax.servlet.ServletContextListener;
85c2e6 40 import javax.servlet.http.Cookie;
fc948c 41
85c2e6 42 import org.apache.wicket.protocol.http.WebResponse;
fc948c 43 import org.eclipse.jgit.errors.RepositoryNotFoundException;
JM 44 import org.eclipse.jgit.lib.Repository;
5c2841 45 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
f97bf0 46 import org.eclipse.jgit.lib.StoredConfig;
f5d0ad 47 import org.eclipse.jgit.transport.resolver.FileResolver;
892570 48 import org.eclipse.jgit.transport.resolver.RepositoryResolver;
JM 49 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
f5d0ad 50 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
5c2841 51 import org.eclipse.jgit.util.FS;
8a2e9c 52 import org.eclipse.jgit.util.FileUtils;
fc948c 53 import org.slf4j.Logger;
JM 54 import org.slf4j.LoggerFactory;
55
dfb889 56 import com.gitblit.Constants.AccessRestrictionType;
831469 57 import com.gitblit.Constants.FederationRequest;
JM 58 import com.gitblit.Constants.FederationStrategy;
59 import com.gitblit.Constants.FederationToken;
60 import com.gitblit.models.FederationModel;
61 import com.gitblit.models.FederationProposal;
31abc2 62 import com.gitblit.models.FederationSet;
1f9dae 63 import com.gitblit.models.RepositoryModel;
JM 64 import com.gitblit.models.UserModel;
72633d 65 import com.gitblit.utils.ByteFormat;
f6740d 66 import com.gitblit.utils.FederationUtils;
fc948c 67 import com.gitblit.utils.JGitUtils;
93f0b1 68 import com.gitblit.utils.JsonUtils;
00afd7 69 import com.gitblit.utils.StringUtils;
fc948c 70
892570 71 /**
JM 72  * GitBlit is the servlet context listener singleton that acts as the core for
73  * the web ui and the servlets. This class is either directly instantiated by
74  * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the
75  * definition in the web.xml file (Gitblit WAR).
76  * 
77  * This class is the central logic processor for Gitblit. All settings, user
78  * object, and repository object operations pass through this class.
79  * 
80  * Repository Resolution. There are two pathways for finding repositories. One
81  * pathway, for web ui display and repository authentication & authorization, is
82  * within this class. The other pathway is through the standard GitServlet.
83  * 
84  * @author James Moger
85  * 
86  */
87cc1e 87 public class GitBlit implements ServletContextListener {
fc948c 88
5450d0 89     private static GitBlit gitblit;
fc948c 90
JM 91     private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
92
831469 93     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
JM 94
95     private final List<FederationModel> federationRegistrations = Collections
96             .synchronizedList(new ArrayList<FederationModel>());
97
98     private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
99
892570 100     private RepositoryResolver<Void> repositoryResolver;
fc948c 101
166e6a 102     private File repositoriesFolder;
fc948c 103
5450d0 104     private boolean exportAll = true;
fc948c 105
85c2e6 106     private IUserService userService;
fc948c 107
892570 108     private IStoredSettings settings;
831469 109
JM 110     private MailExecutor mailExecutor;
87cc1e 111
5450d0 112     public GitBlit() {
JM 113         if (gitblit == null) {
892570 114             // set the static singleton reference
5450d0 115             gitblit = this;
JM 116         }
87cc1e 117     }
JM 118
892570 119     /**
JM 120      * Returns the Gitblit singleton.
121      * 
122      * @return gitblit singleton
123      */
2a7306 124     public static GitBlit self() {
5450d0 125         if (gitblit == null) {
892570 126             new GitBlit();
5450d0 127         }
JM 128         return gitblit;
831469 129     }
JM 130
131     /**
132      * Determine if this is the GO variant of Gitblit.
133      * 
134      * @return true if this is the GO variant of Gitblit.
135      */
136     public static boolean isGO() {
137         return self().settings instanceof FileSettings;
2a7306 138     }
JM 139
892570 140     /**
JM 141      * Returns the boolean value for the specified key. If the key does not
142      * exist or the value for the key can not be interpreted as a boolean, the
143      * defaultValue is returned.
144      * 
145      * @see IStoredSettings.getBoolean(String, boolean)
146      * @param key
147      * @param defaultValue
148      * @return key value or defaultValue
149      */
2a7306 150     public static boolean getBoolean(String key, boolean defaultValue) {
892570 151         return self().settings.getBoolean(key, defaultValue);
2a7306 152     }
JM 153
892570 154     /**
JM 155      * Returns the integer value for the specified key. If the key does not
156      * exist or the value for the key can not be interpreted as an integer, the
157      * defaultValue is returned.
158      * 
159      * @see IStoredSettings.getInteger(String key, int defaultValue)
160      * @param key
161      * @param defaultValue
162      * @return key value or defaultValue
163      */
2a7306 164     public static int getInteger(String key, int defaultValue) {
892570 165         return self().settings.getInteger(key, defaultValue);
2a7306 166     }
JM 167
892570 168     /**
7e5ee5 169      * Returns the char value for the specified key. If the key does not exist
JM 170      * or the value for the key can not be interpreted as a character, the
171      * defaultValue is returned.
172      * 
173      * @see IStoredSettings.getChar(String key, char defaultValue)
174      * @param key
175      * @param defaultValue
176      * @return key value or defaultValue
177      */
178     public static char getChar(String key, char defaultValue) {
179         return self().settings.getChar(key, defaultValue);
180     }
dd6f08 181
7e5ee5 182     /**
892570 183      * Returns the string value for the specified key. If the key does not exist
JM 184      * or the value for the key can not be interpreted as a string, the
185      * defaultValue is returned.
186      * 
187      * @see IStoredSettings.getString(String key, String defaultValue)
188      * @param key
189      * @param defaultValue
190      * @return key value or defaultValue
191      */
2a7306 192     public static String getString(String key, String defaultValue) {
892570 193         return self().settings.getString(key, defaultValue);
2a7306 194     }
JM 195
892570 196     /**
JM 197      * Returns a list of space-separated strings from the specified key.
198      * 
199      * @see IStoredSettings.getStrings(String key)
200      * @param name
201      * @return list of strings
202      */
2a7306 203     public static List<String> getStrings(String key) {
892570 204         return self().settings.getStrings(key);
2a7306 205     }
892570 206
JM 207     /**
208      * Returns the list of keys whose name starts with the specified prefix. If
209      * the prefix is null or empty, all key names are returned.
210      * 
211      * @see IStoredSettings.getAllKeys(String key)
212      * @param startingWith
213      * @return list of keys
214      */
2a7306 215
JM 216     public static List<String> getAllKeys(String startingWith) {
892570 217         return self().settings.getAllKeys(startingWith);
fc948c 218     }
155bf7 219
892570 220     /**
JM 221      * Is Gitblit running in debug mode?
222      * 
223      * @return true if Gitblit is running in debug mode
224      */
8c9a20 225     public static boolean isDebugMode() {
892570 226         return self().settings.getBoolean(Keys.web.debugMode, false);
87cc1e 227     }
JM 228
892570 229     /**
JM 230      * Returns the list of non-Gitblit clone urls. This allows Gitblit to
231      * advertise alternative urls for Git client repository access.
232      * 
233      * @param repositoryName
234      * @return list of non-gitblit clone urls
235      */
8a2e9c 236     public List<String> getOtherCloneUrls(String repositoryName) {
JM 237         List<String> cloneUrls = new ArrayList<String>();
892570 238         for (String url : settings.getStrings(Keys.web.otherUrls)) {
8a2e9c 239             cloneUrls.add(MessageFormat.format(url, repositoryName));
JM 240         }
241         return cloneUrls;
fc948c 242     }
JM 243
892570 244     /**
JM 245      * Set the user service. The user service authenticates all users and is
246      * responsible for managing user permissions.
247      * 
248      * @param userService
249      */
85c2e6 250     public void setUserService(IUserService userService) {
JM 251         logger.info("Setting up user service " + userService.toString());
252         this.userService = userService;
63ee41 253         this.userService.setup(settings);
fc948c 254     }
JM 255
892570 256     /**
JM 257      * Authenticate a user based on a username and password.
258      * 
259      * @see IUserService.authenticate(String, char[])
260      * @param username
261      * @param password
262      * @return a user object or null
263      */
511554 264     public UserModel authenticate(String username, char[] password) {
831469 265         if (StringUtils.isEmpty(username)) {
JM 266             // can not authenticate empty username
267             return null;
268         }
269         String pw = new String(password);
270         if (StringUtils.isEmpty(pw)) {
271             // can not authenticate empty password
272             return null;
273         }
274
275         // check to see if this is the federation user
276         if (canFederate()) {
277             if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
278                 List<String> tokens = getFederationTokens();
279                 if (tokens.contains(pw)) {
280                     // the federation user is an administrator
281                     UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
282                     federationUser.canAdmin = true;
283                     return federationUser;
284                 }
285             }
286         }
287
288         // delegate authentication to the user service
85c2e6 289         if (userService == null) {
fc948c 290             return null;
JM 291         }
85c2e6 292         return userService.authenticate(username, password);
JM 293     }
294
892570 295     /**
JM 296      * Authenticate a user based on their cookie.
297      * 
298      * @param cookies
299      * @return a user object or null
300      */
85c2e6 301     public UserModel authenticate(Cookie[] cookies) {
JM 302         if (userService == null) {
303             return null;
304         }
305         if (userService.supportsCookies()) {
306             if (cookies != null && cookies.length > 0) {
307                 for (Cookie cookie : cookies) {
308                     if (cookie.getName().equals(Constants.NAME)) {
309                         String value = cookie.getValue();
310                         return userService.authenticate(value.toCharArray());
311                     }
312                 }
313             }
314         }
315         return null;
316     }
317
892570 318     /**
JM 319      * Sets a cookie for the specified user.
320      * 
321      * @param response
322      * @param user
323      */
85c2e6 324     public void setCookie(WebResponse response, UserModel user) {
JM 325         if (userService == null) {
326             return;
327         }
328         if (userService.supportsCookies()) {
329             Cookie userCookie;
330             if (user == null) {
331                 // clear cookie for logout
332                 userCookie = new Cookie(Constants.NAME, "");
333             } else {
334                 // set cookie for login
335                 char[] cookie = userService.getCookie(user);
336                 userCookie = new Cookie(Constants.NAME, new String(cookie));
337                 userCookie.setMaxAge(Integer.MAX_VALUE);
338             }
339             userCookie.setPath("/");
340             response.addCookie(userCookie);
341         }
fc948c 342     }
JM 343
892570 344     /**
JM 345      * Returns the list of all users available to the login service.
346      * 
347      * @see IUserService.getAllUsernames()
348      * @return list of all usernames
349      */
f98825 350     public List<String> getAllUsernames() {
85c2e6 351         List<String> names = new ArrayList<String>(userService.getAllUsernames());
00afd7 352         Collections.sort(names);
JM 353         return names;
8a2e9c 354     }
JM 355
892570 356     /**
JM 357      * Delete the user object with the specified username
358      * 
359      * @see IUserService.deleteUser(String)
360      * @param username
361      * @return true if successful
362      */
8a2e9c 363     public boolean deleteUser(String username) {
85c2e6 364         return userService.deleteUser(username);
f98825 365     }
dfb889 366
892570 367     /**
JM 368      * Retrieve the user object for the specified username.
369      * 
370      * @see IUserService.getUserModel(String)
371      * @param username
372      * @return a user object or null
373      */
f98825 374     public UserModel getUserModel(String username) {
85c2e6 375         UserModel user = userService.getUserModel(username);
dfb889 376         return user;
f98825 377     }
8a2e9c 378
892570 379     /**
JM 380      * Returns the list of all users who are allowed to bypass the access
381      * restriction placed on the specified repository.
382      * 
383      * @see IUserService.getUsernamesForRepositoryRole(String)
384      * @param repository
385      * @return list of all usernames that can bypass the access restriction
386      */
f98825 387     public List<String> getRepositoryUsers(RepositoryModel repository) {
892570 388         return userService.getUsernamesForRepositoryRole(repository.name);
f98825 389     }
8a2e9c 390
892570 391     /**
JM 392      * Sets the list of all uses who are allowed to bypass the access
393      * restriction placed on the specified repository.
394      * 
395      * @see IUserService.setUsernamesForRepositoryRole(String, List<String>)
396      * @param repository
397      * @param usernames
398      * @return true if successful
399      */
f98825 400     public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
892570 401         return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers);
dfb889 402     }
JM 403
892570 404     /**
JM 405      * Adds/updates a complete user object keyed by username. This method allows
406      * for renaming a user.
407      * 
408      * @see IUserService.updateUserModel(String, UserModel)
409      * @param username
410      * @param user
411      * @param isCreate
412      * @throws GitBlitException
413      */
414     public void updateUserModel(String username, UserModel user, boolean isCreate)
2a7306 415             throws GitBlitException {
85c2e6 416         if (!userService.updateUserModel(username, user)) {
dfb889 417             throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
JM 418         }
419     }
420
892570 421     /**
JM 422      * Returns the list of all repositories available to Gitblit. This method
423      * does not consider user access permissions.
424      * 
425      * @return list of all repositories
426      */
166e6a 427     public List<String> getRepositoryList() {
2a7306 428         return JGitUtils.getRepositoryList(repositoriesFolder, exportAll,
892570 429                 settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
166e6a 430     }
155bf7 431
892570 432     /**
JM 433      * Returns the JGit repository for the specified name.
434      * 
435      * @param repositoryName
436      * @return repository or null
437      */
166e6a 438     public Repository getRepository(String repositoryName) {
JM 439         Repository r = null;
440         try {
441             r = repositoryResolver.open(null, repositoryName);
442         } catch (RepositoryNotFoundException e) {
443             r = null;
1f9dae 444             logger.error("GitBlit.getRepository(String) failed to find "
JM 445                     + new File(repositoriesFolder, repositoryName).getAbsolutePath());
892570 446         } catch (ServiceNotAuthorizedException e) {
JM 447             r = null;
448             logger.error("GitBlit.getRepository(String) failed to find "
449                     + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
166e6a 450         } catch (ServiceNotEnabledException e) {
JM 451             r = null;
892570 452             logger.error("GitBlit.getRepository(String) failed to find "
JM 453                     + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
166e6a 454         }
JM 455         return r;
456     }
dfb889 457
892570 458     /**
JM 459      * Returns the list of repository models that are accessible to the user.
460      * 
461      * @param user
462      * @return list of repository models accessible to user
463      */
511554 464     public List<RepositoryModel> getRepositoryModels(UserModel user) {
166e6a 465         List<String> list = getRepositoryList();
JM 466         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
467         for (String repo : list) {
dfb889 468             RepositoryModel model = getRepositoryModel(user, repo);
JM 469             if (model != null) {
470                 repositories.add(model);
471             }
166e6a 472         }
JM 473         return repositories;
474     }
8a2e9c 475
892570 476     /**
JM 477      * Returns a repository model if the repository exists and the user may
478      * access the repository.
479      * 
480      * @param user
481      * @param repositoryName
482      * @return repository model or null
483      */
511554 484     public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
dfb889 485         RepositoryModel model = getRepositoryModel(repositoryName);
85c2e6 486         if (model == null) {
JM 487             return null;
488         }
dfb889 489         if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
d0d438 490             if (user != null && user.canAccessRepository(model.name)) {
dfb889 491                 return model;
JM 492             }
493             return null;
494         } else {
495             return model;
496         }
497     }
498
892570 499     /**
JM 500      * Returns the repository model for the specified repository. This method
501      * does not consider user access permissions.
502      * 
503      * @param repositoryName
504      * @return repository model or null
505      */
166e6a 506     public RepositoryModel getRepositoryModel(String repositoryName) {
JM 507         Repository r = getRepository(repositoryName);
1f9dae 508         if (r == null) {
JM 509             return null;
510         }
166e6a 511         RepositoryModel model = new RepositoryModel();
JM 512         model.name = repositoryName;
bc9d4a 513         model.hasCommits = JGitUtils.hasCommits(r);
ed21d2 514         model.lastChange = JGitUtils.getLastChange(r, null);
166e6a 515         StoredConfig config = JGitUtils.readConfig(r);
JM 516         if (config != null) {
00afd7 517             model.description = getConfig(config, "description", "");
JM 518             model.owner = getConfig(config, "owner", "");
519             model.useTickets = getConfig(config, "useTickets", false);
520             model.useDocs = getConfig(config, "useDocs", false);
2a7306 521             model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
JM 522                     "accessRestriction", null));
00afd7 523             model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
JM 524             model.isFrozen = getConfig(config, "isFrozen", false);
a1ea87 525             model.showReadme = getConfig(config, "showReadme", false);
831469 526             model.federationStrategy = FederationStrategy.fromName(getConfig(config,
JM 527                     "federationStrategy", null));
8f73a7 528             model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 529                     "gitblit", null, "federationSets")));
831469 530             model.isFederated = getConfig(config, "isFederated", false);
JM 531             model.origin = config.getString("remote", "origin", "url");
166e6a 532         }
JM 533         r.close();
72633d 534         if (getBoolean(Keys.web.showRepositorySizes, true)) {
JM 535             ByteFormat byteFormat = new ByteFormat();
536             model.size = byteFormat.format(calculateSize(model));            
537         }
166e6a 538         return model;
00afd7 539     }
8a2e9c 540
892570 541     /**
5c2841 542      * Returns the size in bytes of the repository.
JM 543      * 
544      * @param model
545      * @return size in bytes
546      */
547     public long calculateSize(RepositoryModel model) {
548         File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
549         return com.gitblit.utils.FileUtils.folderSize(gitDir);
550     }
551
552     /**
dd6f08 553      * Ensure that a cached repository is completely closed and its resources
JM 554      * are properly released.
555      * 
556      * @param repositoryName
557      */
558     private void closeRepository(String repositoryName) {
559         Repository repository = getRepository(repositoryName);
560         // assume 2 uses in case reflection fails
561         int uses = 2;
562         try {
563             // The FileResolver caches repositories which is very useful
564             // for performance until you want to delete a repository.
565             // I have to use reflection to call close() the correct
566             // number of times to ensure that the object and ref databases
567             // are properly closed before I can delete the repository from
568             // the filesystem.
569             Field useCnt = Repository.class.getDeclaredField("useCnt");
570             useCnt.setAccessible(true);
571             uses = ((AtomicInteger) useCnt.get(repository)).get();
572         } catch (Exception e) {
573             logger.warn(MessageFormat
574                     .format("Failed to reflectively determine use count for repository {0}",
575                             repositoryName), e);
576         }
577         if (uses > 0) {
578             logger.info(MessageFormat
579                     .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
580                             repositoryName, uses, uses));
581             for (int i = 0; i < uses; i++) {
582                 repository.close();
583             }
584         }
585     }
586
587     /**
892570 588      * Returns the gitblit string vlaue for the specified key. If key is not
JM 589      * set, returns defaultValue.
590      * 
591      * @param config
592      * @param field
593      * @param defaultValue
594      * @return field value or defaultValue
595      */
00afd7 596     private String getConfig(StoredConfig config, String field, String defaultValue) {
JM 597         String value = config.getString("gitblit", null, field);
598         if (StringUtils.isEmpty(value)) {
599             return defaultValue;
600         }
601         return value;
602     }
8a2e9c 603
892570 604     /**
JM 605      * Returns the gitblit boolean vlaue for the specified key. If key is not
606      * set, returns defaultValue.
607      * 
608      * @param config
609      * @param field
610      * @param defaultValue
611      * @return field value or defaultValue
612      */
00afd7 613     private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
JM 614         return config.getBoolean("gitblit", field, defaultValue);
166e6a 615     }
JM 616
892570 617     /**
JM 618      * Creates/updates the repository model keyed by reopsitoryName. Saves all
619      * repository settings in .git/config. This method allows for renaming
620      * repositories and will update user access permissions accordingly.
621      * 
622      * All repositories created by this method are bare and automatically have
623      * .git appended to their names, which is the standard convention for bare
624      * repositories.
625      * 
626      * @param repositoryName
627      * @param repository
628      * @param isCreate
629      * @throws GitBlitException
630      */
631     public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
2a7306 632             boolean isCreate) throws GitBlitException {
f5d0ad 633         Repository r = null;
JM 634         if (isCreate) {
a3bde6 635             // ensure created repository name ends with .git
168566 636             if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
a3bde6 637                 repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
JM 638             }
166e6a 639             if (new File(repositoriesFolder, repository.name).exists()) {
2a7306 640                 throw new GitBlitException(MessageFormat.format(
JM 641                         "Can not create repository ''{0}'' because it already exists.",
642                         repository.name));
166e6a 643             }
dfb889 644             // create repository
f5d0ad 645             logger.info("create repository " + repository.name);
168566 646             r = JGitUtils.createRepository(repositoriesFolder, repository.name);
f5d0ad 647         } else {
8a2e9c 648             // rename repository
JM 649             if (!repositoryName.equalsIgnoreCase(repository.name)) {
dd6f08 650                 closeRepository(repositoryName);
8a2e9c 651                 File folder = new File(repositoriesFolder, repositoryName);
JM 652                 File destFolder = new File(repositoriesFolder, repository.name);
653                 if (destFolder.exists()) {
2a7306 654                     throw new GitBlitException(
JM 655                             MessageFormat
656                                     .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
657                                             repositoryName, repository.name));
8a2e9c 658                 }
JM 659                 if (!folder.renameTo(destFolder)) {
2a7306 660                     throw new GitBlitException(MessageFormat.format(
JM 661                             "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
662                             repository.name));
8a2e9c 663                 }
JM 664                 // rename the roles
85c2e6 665                 if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
2a7306 666                     throw new GitBlitException(MessageFormat.format(
JM 667                             "Failed to rename repository permissions ''{0}'' to ''{1}''.",
668                             repositoryName, repository.name));
8a2e9c 669                 }
JM 670             }
671
f5d0ad 672             // load repository
JM 673             logger.info("edit repository " + repository.name);
674             try {
675                 r = repositoryResolver.open(null, repository.name);
676             } catch (RepositoryNotFoundException e) {
677                 logger.error("Repository not found", e);
892570 678             } catch (ServiceNotAuthorizedException e) {
JM 679                 logger.error("Service not authorized", e);
f5d0ad 680             } catch (ServiceNotEnabledException e) {
JM 681                 logger.error("Service not enabled", e);
682             }
f97bf0 683         }
JM 684
f5d0ad 685         // update settings
2a7306 686         if (r != null) {
831469 687             updateConfiguration(r, repository);
2a7306 688             r.close();
831469 689         }
JM 690     }
691
692     /**
693      * Updates the Gitblit configuration for the specified repository.
694      * 
695      * @param r
696      *            the Git repository
697      * @param repository
698      *            the Gitblit repository model
699      */
700     public void updateConfiguration(Repository r, RepositoryModel repository) {
701         StoredConfig config = JGitUtils.readConfig(r);
702         config.setString("gitblit", null, "description", repository.description);
703         config.setString("gitblit", null, "owner", repository.owner);
704         config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
705         config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
706         config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
707         config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
708         config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
709         config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
8f73a7 710         config.setStringList("gitblit", null, "federationSets", repository.federationSets);
831469 711         config.setString("gitblit", null, "federationStrategy",
JM 712                 repository.federationStrategy.name());
713         config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
714         try {
715             config.save();
716         } catch (IOException e) {
717             logger.error("Failed to save repository config!", e);
f97bf0 718         }
f5d0ad 719     }
JM 720
892570 721     /**
JM 722      * Deletes the repository from the file system and removes the repository
723      * permission from all repository users.
724      * 
725      * @param model
726      * @return true if successful
727      */
8a2e9c 728     public boolean deleteRepositoryModel(RepositoryModel model) {
JM 729         return deleteRepository(model.name);
730     }
731
892570 732     /**
JM 733      * Deletes the repository from the file system and removes the repository
734      * permission from all repository users.
735      * 
736      * @param repositoryName
737      * @return true if successful
738      */
8a2e9c 739     public boolean deleteRepository(String repositoryName) {
JM 740         try {
dd6f08 741             closeRepository(repositoryName);
8a2e9c 742             File folder = new File(repositoriesFolder, repositoryName);
JM 743             if (folder.exists() && folder.isDirectory()) {
892570 744                 FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
85c2e6 745                 if (userService.deleteRepositoryRole(repositoryName)) {
8a2e9c 746                     return true;
JM 747                 }
748             }
749         } catch (Throwable t) {
750             logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
751         }
752         return false;
753     }
754
892570 755     /**
JM 756      * Returns an html version of the commit message with any global or
757      * repository-specific regular expression substitution applied.
758      * 
759      * @param repositoryName
760      * @param text
761      * @return html version of the commit message
762      */
8c9a20 763     public String processCommitMessage(String repositoryName, String text) {
JM 764         String html = StringUtils.breakLinesForHtml(text);
765         Map<String, String> map = new HashMap<String, String>();
766         // global regex keys
892570 767         if (settings.getBoolean(Keys.regex.global, false)) {
JM 768             for (String key : settings.getAllKeys(Keys.regex.global)) {
8c9a20 769                 if (!key.equals(Keys.regex.global)) {
JM 770                     String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 771                     map.put(subKey, settings.getString(key, ""));
8c9a20 772                 }
JM 773             }
774         }
775
776         // repository-specific regex keys
892570 777         List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "."
8c9a20 778                 + repositoryName.toLowerCase());
JM 779         for (String key : keys) {
780             String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 781             map.put(subKey, settings.getString(key, ""));
8c9a20 782         }
JM 783
784         for (Entry<String, String> entry : map.entrySet()) {
785             String definition = entry.getValue().trim();
786             String[] chunks = definition.split("!!!");
787             if (chunks.length == 2) {
788                 html = html.replaceAll(chunks[0], chunks[1]);
789             } else {
790                 logger.warn(entry.getKey()
791                         + " improperly formatted.  Use !!! to separate match from replacement: "
792                         + definition);
793             }
794         }
795         return html;
796     }
797
892570 798     /**
831469 799      * Returns Gitblit's scheduled executor service for scheduling tasks.
JM 800      * 
801      * @return scheduledExecutor
802      */
803     public ScheduledExecutorService executor() {
804         return scheduledExecutor;
805     }
806
807     public static boolean canFederate() {
2c32fd 808         String passphrase = getString(Keys.federation.passphrase, "");
JM 809         return !StringUtils.isEmpty(passphrase);
831469 810     }
JM 811
812     /**
813      * Configures this Gitblit instance to pull any registered federated gitblit
814      * instances.
815      */
816     private void configureFederation() {
2c32fd 817         boolean validPassphrase = true;
JM 818         String passphrase = settings.getString(Keys.federation.passphrase, "");
819         if (StringUtils.isEmpty(passphrase)) {
820             logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
821             validPassphrase = false;
831469 822         }
2c32fd 823         if (validPassphrase) {
8f73a7 824             // standard tokens
831469 825             for (FederationToken tokenType : FederationToken.values()) {
JM 826                 logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
827                         getFederationToken(tokenType)));
8f73a7 828             }
JM 829
830             // federation set tokens
831             for (String set : settings.getStrings(Keys.federation.sets)) {
832                 logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
833                         getFederationToken(set)));
831469 834             }
JM 835         }
836
837         // Schedule the federation executor
838         List<FederationModel> registrations = getFederationRegistrations();
839         if (registrations.size() > 0) {
f6740d 840             FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
JM 841             scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
831469 842         }
JM 843     }
844
845     /**
846      * Returns the list of federated gitblit instances that this instance will
847      * try to pull.
848      * 
849      * @return list of registered gitblit instances
850      */
851     public List<FederationModel> getFederationRegistrations() {
852         if (federationRegistrations.isEmpty()) {
f6740d 853             federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
831469 854         }
JM 855         return federationRegistrations;
856     }
857
858     /**
859      * Retrieve the specified federation registration.
860      * 
861      * @param name
862      *            the name of the registration
863      * @return a federation registration
864      */
865     public FederationModel getFederationRegistration(String url, String name) {
866         // check registrations
867         for (FederationModel r : getFederationRegistrations()) {
868             if (r.name.equals(name) && r.url.equals(url)) {
869                 return r;
870             }
871         }
872
873         // check the results
874         for (FederationModel r : getFederationResultRegistrations()) {
875             if (r.name.equals(name) && r.url.equals(url)) {
876                 return r;
877             }
878         }
879         return null;
880     }
881
882     /**
31abc2 883      * Returns the list of federation sets.
JM 884      * 
885      * @return list of federation sets
886      */
887     public List<FederationSet> getFederationSets(String gitblitUrl) {
888         List<FederationSet> list = new ArrayList<FederationSet>();
889         // generate standard tokens
890         for (FederationToken type : FederationToken.values()) {
891             FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
892             fset.repositories = getRepositories(gitblitUrl, fset.token);
893             list.add(fset);
894         }
895         // generate tokens for federation sets
896         for (String set : settings.getStrings(Keys.federation.sets)) {
897             FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
898                     getFederationToken(set));
899             fset.repositories = getRepositories(gitblitUrl, fset.token);
900             list.add(fset);
901         }
902         return list;
903     }
904
905     /**
831469 906      * Returns the list of possible federation tokens for this Gitblit instance.
JM 907      * 
908      * @return list of federation tokens
909      */
910     public List<String> getFederationTokens() {
911         List<String> tokens = new ArrayList<String>();
8f73a7 912         // generate standard tokens
831469 913         for (FederationToken type : FederationToken.values()) {
JM 914             tokens.add(getFederationToken(type));
8f73a7 915         }
JM 916         // generate tokens for federation sets
917         for (String set : settings.getStrings(Keys.federation.sets)) {
918             tokens.add(getFederationToken(set));
831469 919         }
JM 920         return tokens;
921     }
922
923     /**
924      * Returns the specified federation token for this Gitblit instance.
925      * 
926      * @param type
927      * @return a federation token
928      */
929     public String getFederationToken(FederationToken type) {
8f73a7 930         return getFederationToken(type.name());
JM 931     }
932
933     /**
934      * Returns the specified federation token for this Gitblit instance.
935      * 
936      * @param value
937      * @return a federation token
938      */
939     public String getFederationToken(String value) {
2c32fd 940         String passphrase = settings.getString(Keys.federation.passphrase, "");
8f73a7 941         return StringUtils.getSHA1(passphrase + "-" + value);
831469 942     }
JM 943
944     /**
945      * Compares the provided token with this Gitblit instance's tokens and
946      * determines if the requested permission may be granted to the token.
947      * 
948      * @param req
949      * @param token
950      * @return true if the request can be executed
951      */
952     public boolean validateFederationRequest(FederationRequest req, String token) {
953         String all = getFederationToken(FederationToken.ALL);
954         String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
955         String jur = getFederationToken(FederationToken.REPOSITORIES);
956         switch (req) {
957         case PULL_REPOSITORIES:
958             return token.equals(all) || token.equals(unr) || token.equals(jur);
959         case PULL_USERS:
960             return token.equals(all) || token.equals(unr);
961         case PULL_SETTINGS:
962             return token.equals(all);
963         }
964         return false;
965     }
966
967     /**
968      * Acknowledge and cache the status of a remote Gitblit instance.
969      * 
970      * @param identification
971      *            the identification of the pulling Gitblit instance
972      * @param registration
973      *            the registration from the pulling Gitblit instance
974      * @return true if acknowledged
975      */
976     public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
977         // reset the url to the identification of the pulling Gitblit instance
978         registration.url = identification;
979         String id = identification;
980         if (!StringUtils.isEmpty(registration.folder)) {
981             id += "-" + registration.folder;
982         }
983         federationPullResults.put(id, registration);
984         return true;
985     }
986
987     /**
988      * Returns the list of registration results.
989      * 
990      * @return the list of registration results
991      */
992     public List<FederationModel> getFederationResultRegistrations() {
993         return new ArrayList<FederationModel>(federationPullResults.values());
994     }
995
996     /**
997      * Submit a federation proposal. The proposal is cached locally and the
998      * Gitblit administrator(s) are notified via email.
999      * 
1000      * @param proposal
1001      *            the proposal
1002      * @param gitblitUrl
4aafd4 1003      *            the url of your gitblit instance to send an email to
JM 1004      *            administrators
831469 1005      * @return true if the proposal was submitted
JM 1006      */
1007     public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
1008         // convert proposal to json
93f0b1 1009         String json = JsonUtils.toJsonString(proposal);
831469 1010
JM 1011         try {
1012             // make the proposals folder
1013             File proposalsFolder = new File(getString(Keys.federation.proposalsFolder, "proposals")
1014                     .trim());
1015             proposalsFolder.mkdirs();
1016
1017             // cache json to a file
1018             File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
1019             com.gitblit.utils.FileUtils.writeContent(file, json);
1020         } catch (Exception e) {
1021             logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
1022         }
1023
1024         // send an email, if possible
1025         try {
1026             Message message = mailExecutor.createMessageForAdministrators();
1027             if (message != null) {
1028                 message.setSubject("Federation proposal from " + proposal.url);
1029                 message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
1030                         + proposal.token);
1031                 mailExecutor.queue(message);
1032             }
1033         } catch (Throwable t) {
1034             logger.error("Failed to notify administrators of proposal", t);
1035         }
1036         return true;
1037     }
1038
1039     /**
1040      * Returns the list of pending federation proposals
1041      * 
1042      * @return list of federation proposals
1043      */
1044     public List<FederationProposal> getPendingFederationProposals() {
1045         List<FederationProposal> list = new ArrayList<FederationProposal>();
1046         File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
1047         if (folder.exists()) {
1048             File[] files = folder.listFiles(new FileFilter() {
1049                 @Override
1050                 public boolean accept(File file) {
1051                     return file.isFile()
1052                             && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
1053                 }
1054             });
1055             for (File file : files) {
1056                 String json = com.gitblit.utils.FileUtils.readContent(file, null);
31abc2 1057                 FederationProposal proposal = JsonUtils.fromJsonString(json,
JM 1058                         FederationProposal.class);
831469 1059                 list.add(proposal);
JM 1060             }
1061         }
1062         return list;
1063     }
1064
1065     /**
dd9ae7 1066      * Get repositories for the specified token.
JM 1067      * 
1068      * @param gitblitUrl
1069      *            the base url of this gitblit instance
1070      * @param token
1071      *            the federation token
1072      * @return a map of <cloneurl, RepositoryModel>
1073      */
1074     public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
1075         Map<String, String> federationSets = new HashMap<String, String>();
1076         for (String set : getStrings(Keys.federation.sets)) {
1077             federationSets.put(getFederationToken(set), set);
1078         }
1079
1080         // Determine the Gitblit clone url
1081         StringBuilder sb = new StringBuilder();
1082         sb.append(gitblitUrl);
1083         sb.append(Constants.GIT_PATH);
1084         sb.append("{0}");
1085         String cloneUrl = sb.toString();
1086
1087         // Retrieve all available repositories
1088         UserModel user = new UserModel(Constants.FEDERATION_USER);
1089         user.canAdmin = true;
1090         List<RepositoryModel> list = getRepositoryModels(user);
1091
1092         // create the [cloneurl, repositoryModel] map
1093         Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
1094         for (RepositoryModel model : list) {
1095             // by default, setup the url for THIS repository
1096             String url = MessageFormat.format(cloneUrl, model.name);
1097             switch (model.federationStrategy) {
1098             case EXCLUDE:
1099                 // skip this repository
1100                 continue;
1101             case FEDERATE_ORIGIN:
1102                 // federate the origin, if it is defined
1103                 if (!StringUtils.isEmpty(model.origin)) {
1104                     url = model.origin;
1105                 }
1106                 break;
1107             }
1108
1109             if (federationSets.containsKey(token)) {
1110                 // include repositories only for federation set
1111                 String set = federationSets.get(token);
1112                 if (model.federationSets.contains(set)) {
1113                     repositories.put(url, model);
1114                 }
1115             } else {
1116                 // standard federation token for ALL
1117                 repositories.put(url, model);
1118             }
1119         }
1120         return repositories;
1121     }
1122
1123     /**
1124      * Creates a proposal from the token.
1125      * 
1126      * @param gitblitUrl
1127      *            the url of this Gitblit instance
1128      * @param token
1129      * @return a potential proposal
1130      */
1131     public FederationProposal createFederationProposal(String gitblitUrl, String token) {
1132         FederationToken tokenType = FederationToken.REPOSITORIES;
1133         for (FederationToken type : FederationToken.values()) {
1134             if (token.equals(getFederationToken(type))) {
1135                 tokenType = type;
1136                 break;
1137             }
1138         }
1139         Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
1140         FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
1141                 repositories);
1142         return proposal;
1143     }
1144
1145     /**
831469 1146      * Returns the proposal identified by the supplied token.
JM 1147      * 
1148      * @param token
1149      * @return the specified proposal or null
1150      */
1151     public FederationProposal getPendingFederationProposal(String token) {
1152         List<FederationProposal> list = getPendingFederationProposals();
1153         for (FederationProposal proposal : list) {
1154             if (proposal.token.equals(token)) {
1155                 return proposal;
1156             }
1157         }
1158         return null;
1159     }
1160
1161     /**
1162      * Deletes a pending federation proposal.
1163      * 
1164      * @param a
1165      *            proposal
1166      * @return true if the proposal was deleted
1167      */
1168     public boolean deletePendingFederationProposal(FederationProposal proposal) {
1169         File folder = new File(getString(Keys.federation.proposalsFolder, "proposals").trim());
1170         File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
1171         return file.delete();
1172     }
1173
1174     /**
1175      * Notify the administrators by email.
1176      * 
1177      * @param subject
1178      * @param message
1179      */
1180     public void notifyAdministrators(String subject, String message) {
1181         try {
1182             Message mail = mailExecutor.createMessageForAdministrators();
1183             if (mail != null) {
1184                 mail.setSubject(subject);
1185                 mail.setText(message);
1186                 mailExecutor.queue(mail);
1187             }
1188         } catch (MessagingException e) {
1189             logger.error("Messaging error", e);
1190         }
1191     }
1192
1193     /**
892570 1194      * Configure the Gitblit singleton with the specified settings source. This
JM 1195      * source may be file settings (Gitblit GO) or may be web.xml settings
1196      * (Gitblit WAR).
1197      * 
1198      * @param settings
1199      */
f6740d 1200     public void configureContext(IStoredSettings settings, boolean startFederation) {
1f9dae 1201         logger.info("Reading configuration from " + settings.toString());
892570 1202         this.settings = settings;
5450d0 1203         repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git"));
JM 1204         logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
166e6a 1205         repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
85c2e6 1206         String realm = settings.getString(Keys.realm.userService, "users.properties");
JM 1207         IUserService loginService = null;
5450d0 1208         try {
892570 1209             // check to see if this "file" is a login service class
5450d0 1210             Class<?> realmClass = Class.forName(realm);
85c2e6 1211             if (IUserService.class.isAssignableFrom(realmClass)) {
JM 1212                 loginService = (IUserService) realmClass.newInstance();
5450d0 1213             }
JM 1214         } catch (Throwable t) {
892570 1215             // not a login service class or class could not be instantiated.
JM 1216             // try to use default file login service
5450d0 1217             File realmFile = new File(realm);
JM 1218             if (!realmFile.exists()) {
1219                 try {
1220                     realmFile.createNewFile();
1221                 } catch (IOException x) {
1222                     logger.error(
1223                             MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
1224                 }
1225             }
85c2e6 1226             loginService = new FileUserService(realmFile);
5450d0 1227         }
85c2e6 1228         setUserService(loginService);
831469 1229         mailExecutor = new MailExecutor(settings);
JM 1230         if (mailExecutor.isReady()) {
1231             scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
1232         } else {
1233             logger.warn("Mail server is not properly configured.  Mail services disabled.");
f6740d 1234         }
JM 1235         if (startFederation) {
1236             configureFederation();
831469 1237         }
87cc1e 1238     }
JM 1239
892570 1240     /**
JM 1241      * Configure Gitblit from the web.xml, if no configuration has already been
1242      * specified.
1243      * 
1244      * @see ServletContextListener.contextInitialize(ServletContextEvent)
1245      */
87cc1e 1246     @Override
JM 1247     public void contextInitialized(ServletContextEvent contextEvent) {
892570 1248         if (settings == null) {
JM 1249             // Gitblit WAR is running in a servlet container
87cc1e 1250             WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
f6740d 1251             configureContext(webxmlSettings, true);
87cc1e 1252         }
JM 1253     }
1254
892570 1255     /**
JM 1256      * Gitblit is being shutdown either because the servlet container is
1257      * shutting down or because the servlet container is re-deploying Gitblit.
1258      */
87cc1e 1259     @Override
JM 1260     public void contextDestroyed(ServletContextEvent contextEvent) {
f339f5 1261         logger.info("Gitblit context destroyed by servlet container.");
831469 1262         scheduledExecutor.shutdownNow();
87cc1e 1263     }
fc948c 1264 }