James Moger
2012-02-23 7a1889b343b24aa18ce9440794606f4c08c2741f
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
b75734 18 import java.io.BufferedReader;
fc948c 19 import java.io.File;
831469 20 import java.io.FileFilter;
f97bf0 21 import java.io.IOException;
b75734 22 import java.io.InputStream;
JM 23 import java.io.InputStreamReader;
dd6f08 24 import java.lang.reflect.Field;
166e6a 25 import java.text.MessageFormat;
6c6e7d 26 import java.text.SimpleDateFormat;
fc948c 27 import java.util.ArrayList;
8f73a7 28 import java.util.Arrays;
0b9119 29 import java.util.Collection;
00afd7 30 import java.util.Collections;
6c6e7d 31 import java.util.Date;
8c9a20 32 import java.util.HashMap;
d7905a 33 import java.util.LinkedHashSet;
fc948c 34 import java.util.List;
8c9a20 35 import java.util.Map;
JM 36 import java.util.Map.Entry;
6cc1d4 37 import java.util.Set;
6c6e7d 38 import java.util.TimeZone;
d7905a 39 import java.util.TreeSet;
831469 40 import java.util.concurrent.ConcurrentHashMap;
JM 41 import java.util.concurrent.Executors;
42 import java.util.concurrent.ScheduledExecutorService;
43 import java.util.concurrent.TimeUnit;
dd6f08 44 import java.util.concurrent.atomic.AtomicInteger;
fc948c 45
831469 46 import javax.mail.Message;
JM 47 import javax.mail.MessagingException;
b75734 48 import javax.servlet.ServletContext;
87cc1e 49 import javax.servlet.ServletContextEvent;
JM 50 import javax.servlet.ServletContextListener;
85c2e6 51 import javax.servlet.http.Cookie;
fc948c 52
85c2e6 53 import org.apache.wicket.protocol.http.WebResponse;
fc948c 54 import org.eclipse.jgit.errors.RepositoryNotFoundException;
JM 55 import org.eclipse.jgit.lib.Repository;
5c2841 56 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
f97bf0 57 import org.eclipse.jgit.lib.StoredConfig;
f5d0ad 58 import org.eclipse.jgit.transport.resolver.FileResolver;
892570 59 import org.eclipse.jgit.transport.resolver.RepositoryResolver;
JM 60 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
f5d0ad 61 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
5c2841 62 import org.eclipse.jgit.util.FS;
8a2e9c 63 import org.eclipse.jgit.util.FileUtils;
fc948c 64 import org.slf4j.Logger;
JM 65 import org.slf4j.LoggerFactory;
66
dfb889 67 import com.gitblit.Constants.AccessRestrictionType;
831469 68 import com.gitblit.Constants.FederationRequest;
JM 69 import com.gitblit.Constants.FederationStrategy;
70 import com.gitblit.Constants.FederationToken;
71 import com.gitblit.models.FederationModel;
72 import com.gitblit.models.FederationProposal;
31abc2 73 import com.gitblit.models.FederationSet;
4d44cf 74 import com.gitblit.models.Metric;
1f9dae 75 import com.gitblit.models.RepositoryModel;
d03aff 76 import com.gitblit.models.ServerSettings;
b75734 77 import com.gitblit.models.ServerStatus;
JM 78 import com.gitblit.models.SettingModel;
fe24a0 79 import com.gitblit.models.TeamModel;
1f9dae 80 import com.gitblit.models.UserModel;
0db5c4 81 import com.gitblit.utils.ArrayUtils;
72633d 82 import com.gitblit.utils.ByteFormat;
f6740d 83 import com.gitblit.utils.FederationUtils;
fc948c 84 import com.gitblit.utils.JGitUtils;
93f0b1 85 import com.gitblit.utils.JsonUtils;
4d44cf 86 import com.gitblit.utils.MetricUtils;
9275bd 87 import com.gitblit.utils.ObjectCache;
00afd7 88 import com.gitblit.utils.StringUtils;
fc948c 89
892570 90 /**
JM 91  * GitBlit is the servlet context listener singleton that acts as the core for
92  * the web ui and the servlets. This class is either directly instantiated by
93  * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the
94  * definition in the web.xml file (Gitblit WAR).
95  * 
96  * This class is the central logic processor for Gitblit. All settings, user
97  * object, and repository object operations pass through this class.
98  * 
99  * Repository Resolution. There are two pathways for finding repositories. One
100  * pathway, for web ui display and repository authentication & authorization, is
101  * within this class. The other pathway is through the standard GitServlet.
102  * 
103  * @author James Moger
104  * 
105  */
87cc1e 106 public class GitBlit implements ServletContextListener {
fc948c 107
5450d0 108     private static GitBlit gitblit;
6c6e7d 109     
fc948c 110     private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
JM 111
831469 112     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
JM 113
114     private final List<FederationModel> federationRegistrations = Collections
115             .synchronizedList(new ArrayList<FederationModel>());
116
117     private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
4d44cf 118
JM 119     private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
120
121     private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
831469 122
892570 123     private RepositoryResolver<Void> repositoryResolver;
fc948c 124
b75734 125     private ServletContext servletContext;
JM 126
166e6a 127     private File repositoriesFolder;
fc948c 128
85c2e6 129     private IUserService userService;
fc948c 130
892570 131     private IStoredSettings settings;
b75734 132
84c1d5 133     private ServerSettings settingsModel;
b75734 134
JM 135     private ServerStatus serverStatus;
831469 136
JM 137     private MailExecutor mailExecutor;
6c6e7d 138     
JM 139     private TimeZone timezone;
87cc1e 140
5450d0 141     public GitBlit() {
JM 142         if (gitblit == null) {
892570 143             // set the static singleton reference
5450d0 144             gitblit = this;
JM 145         }
87cc1e 146     }
JM 147
892570 148     /**
JM 149      * Returns the Gitblit singleton.
150      * 
151      * @return gitblit singleton
152      */
2a7306 153     public static GitBlit self() {
5450d0 154         if (gitblit == null) {
892570 155             new GitBlit();
5450d0 156         }
JM 157         return gitblit;
831469 158     }
JM 159
160     /**
161      * Determine if this is the GO variant of Gitblit.
162      * 
163      * @return true if this is the GO variant of Gitblit.
164      */
165     public static boolean isGO() {
166         return self().settings instanceof FileSettings;
6c6e7d 167     }
JM 168     
169     /**
170      * Returns the preferred timezone for the Gitblit instance.
171      * 
172      * @return a timezone
173      */
174     public static TimeZone getTimezone() {
175         if (self().timezone == null) {
176             String tzid = getString("web.timezone", null);
177             if (StringUtils.isEmpty(tzid)) {
178                 self().timezone = TimeZone.getDefault();
179                 return self().timezone;
180             }
181             self().timezone = TimeZone.getTimeZone(tzid);
182         }
183         return self().timezone;
2a7306 184     }
JM 185
892570 186     /**
JM 187      * Returns the boolean value for the specified key. If the key does not
188      * exist or the value for the key can not be interpreted as a boolean, the
189      * defaultValue is returned.
190      * 
191      * @see IStoredSettings.getBoolean(String, boolean)
192      * @param key
193      * @param defaultValue
194      * @return key value or defaultValue
195      */
2a7306 196     public static boolean getBoolean(String key, boolean defaultValue) {
892570 197         return self().settings.getBoolean(key, defaultValue);
2a7306 198     }
JM 199
892570 200     /**
JM 201      * Returns the integer value for the specified key. If the key does not
202      * exist or the value for the key can not be interpreted as an integer, the
203      * defaultValue is returned.
204      * 
205      * @see IStoredSettings.getInteger(String key, int defaultValue)
206      * @param key
207      * @param defaultValue
208      * @return key value or defaultValue
209      */
2a7306 210     public static int getInteger(String key, int defaultValue) {
892570 211         return self().settings.getInteger(key, defaultValue);
2a7306 212     }
JM 213
892570 214     /**
7e5ee5 215      * Returns the char value for the specified key. If the key does not exist
JM 216      * or the value for the key can not be interpreted as a character, the
217      * defaultValue is returned.
218      * 
219      * @see IStoredSettings.getChar(String key, char defaultValue)
220      * @param key
221      * @param defaultValue
222      * @return key value or defaultValue
223      */
224     public static char getChar(String key, char defaultValue) {
225         return self().settings.getChar(key, defaultValue);
226     }
dd6f08 227
7e5ee5 228     /**
892570 229      * Returns the string value for the specified key. If the key does not exist
JM 230      * or the value for the key can not be interpreted as a string, the
231      * defaultValue is returned.
232      * 
233      * @see IStoredSettings.getString(String key, String defaultValue)
234      * @param key
235      * @param defaultValue
236      * @return key value or defaultValue
237      */
2a7306 238     public static String getString(String key, String defaultValue) {
892570 239         return self().settings.getString(key, defaultValue);
2a7306 240     }
JM 241
892570 242     /**
JM 243      * Returns a list of space-separated strings from the specified key.
244      * 
245      * @see IStoredSettings.getStrings(String key)
246      * @param name
247      * @return list of strings
248      */
2a7306 249     public static List<String> getStrings(String key) {
892570 250         return self().settings.getStrings(key);
2a7306 251     }
892570 252
JM 253     /**
254      * Returns the list of keys whose name starts with the specified prefix. If
255      * the prefix is null or empty, all key names are returned.
256      * 
257      * @see IStoredSettings.getAllKeys(String key)
258      * @param startingWith
259      * @return list of keys
260      */
2a7306 261
JM 262     public static List<String> getAllKeys(String startingWith) {
892570 263         return self().settings.getAllKeys(startingWith);
fc948c 264     }
155bf7 265
892570 266     /**
JM 267      * Is Gitblit running in debug mode?
268      * 
269      * @return true if Gitblit is running in debug mode
270      */
8c9a20 271     public static boolean isDebugMode() {
892570 272         return self().settings.getBoolean(Keys.web.debugMode, false);
d03aff 273     }
JM 274
275     /**
b774de 276      * Returns the file object for the specified configuration key.
JM 277      * 
278      * @return the file
279      */
280     public static File getFileOrFolder(String key, String defaultFileOrFolder) {
281         String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
282         return getFileOrFolder(fileOrFolder);
283     }
284
285     /**
286      * Returns the file object which may have it's base-path determined by
287      * environment variables for running on a cloud hosting service. All Gitblit
288      * file or folder retrievals are (at least initially) funneled through this
289      * method so it is the correct point to globally override/alter filesystem
290      * access based on environment or some other indicator.
291      * 
292      * @return the file
293      */
294     public static File getFileOrFolder(String fileOrFolder) {
295         String openShift = System.getenv("OPENSHIFT_DATA_DIR");
296         if (!StringUtils.isEmpty(openShift)) {
297             // running on RedHat OpenShift
298             return new File(openShift, fileOrFolder);
299         }
300         return new File(fileOrFolder);
301     }
302
303     /**
304      * Returns the path of the repositories folder. This method checks to see if
305      * Gitblit is running on a cloud service and may return an adjusted path.
306      * 
307      * @return the repositories folder path
308      */
309     public static File getRepositoriesFolder() {
310         return getFileOrFolder(Keys.git.repositoriesFolder, "git");
311     }
312
313     /**
314      * Returns the path of the proposals folder. This method checks to see if
315      * Gitblit is running on a cloud service and may return an adjusted path.
316      * 
317      * @return the proposals folder path
318      */
319     public static File getProposalsFolder() {
320         return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
6cc1d4 321     }
JM 322
323     /**
324      * Returns the path of the Groovy folder. This method checks to see if
325      * Gitblit is running on a cloud service and may return an adjusted path.
326      * 
327      * @return the Groovy scripts folder path
328      */
329     public static File getGroovyScriptsFolder() {
330         return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
b774de 331     }
JM 332
333     /**
d03aff 334      * Updates the list of server settings.
JM 335      * 
336      * @param settings
337      * @return true if the update succeeded
338      */
97a20e 339     public boolean updateSettings(Map<String, String> updatedSettings) {
486ee1 340         return settings.saveSettings(updatedSettings);
b75734 341     }
JM 342
343     public ServerStatus getStatus() {
344         // update heap memory status
345         serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
346         serverStatus.heapFree = Runtime.getRuntime().freeMemory();
347         return serverStatus;
87cc1e 348     }
JM 349
892570 350     /**
JM 351      * Returns the list of non-Gitblit clone urls. This allows Gitblit to
352      * advertise alternative urls for Git client repository access.
353      * 
354      * @param repositoryName
355      * @return list of non-gitblit clone urls
356      */
8a2e9c 357     public List<String> getOtherCloneUrls(String repositoryName) {
JM 358         List<String> cloneUrls = new ArrayList<String>();
892570 359         for (String url : settings.getStrings(Keys.web.otherUrls)) {
8a2e9c 360             cloneUrls.add(MessageFormat.format(url, repositoryName));
JM 361         }
362         return cloneUrls;
fc948c 363     }
JM 364
892570 365     /**
JM 366      * Set the user service. The user service authenticates all users and is
367      * responsible for managing user permissions.
368      * 
369      * @param userService
370      */
85c2e6 371     public void setUserService(IUserService userService) {
JM 372         logger.info("Setting up user service " + userService.toString());
373         this.userService = userService;
63ee41 374         this.userService.setup(settings);
fc948c 375     }
JM 376
892570 377     /**
JM 378      * Authenticate a user based on a username and password.
379      * 
380      * @see IUserService.authenticate(String, char[])
381      * @param username
382      * @param password
383      * @return a user object or null
384      */
511554 385     public UserModel authenticate(String username, char[] password) {
831469 386         if (StringUtils.isEmpty(username)) {
JM 387             // can not authenticate empty username
388             return null;
389         }
390         String pw = new String(password);
391         if (StringUtils.isEmpty(pw)) {
392             // can not authenticate empty password
393             return null;
394         }
395
396         // check to see if this is the federation user
397         if (canFederate()) {
398             if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
399                 List<String> tokens = getFederationTokens();
400                 if (tokens.contains(pw)) {
401                     // the federation user is an administrator
402                     UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
403                     federationUser.canAdmin = true;
404                     return federationUser;
405                 }
406             }
407         }
408
409         // delegate authentication to the user service
85c2e6 410         if (userService == null) {
fc948c 411             return null;
JM 412         }
85c2e6 413         return userService.authenticate(username, password);
JM 414     }
415
892570 416     /**
JM 417      * Authenticate a user based on their cookie.
418      * 
419      * @param cookies
420      * @return a user object or null
421      */
85c2e6 422     public UserModel authenticate(Cookie[] cookies) {
JM 423         if (userService == null) {
424             return null;
425         }
426         if (userService.supportsCookies()) {
427             if (cookies != null && cookies.length > 0) {
428                 for (Cookie cookie : cookies) {
429                     if (cookie.getName().equals(Constants.NAME)) {
430                         String value = cookie.getValue();
431                         return userService.authenticate(value.toCharArray());
432                     }
433                 }
434             }
435         }
436         return null;
437     }
438
892570 439     /**
JM 440      * Sets a cookie for the specified user.
441      * 
442      * @param response
443      * @param user
444      */
85c2e6 445     public void setCookie(WebResponse response, UserModel user) {
JM 446         if (userService == null) {
447             return;
448         }
449         if (userService.supportsCookies()) {
450             Cookie userCookie;
451             if (user == null) {
452                 // clear cookie for logout
453                 userCookie = new Cookie(Constants.NAME, "");
454             } else {
455                 // set cookie for login
456                 char[] cookie = userService.getCookie(user);
457                 userCookie = new Cookie(Constants.NAME, new String(cookie));
458                 userCookie.setMaxAge(Integer.MAX_VALUE);
459             }
460             userCookie.setPath("/");
461             response.addCookie(userCookie);
462         }
fc948c 463     }
JM 464
892570 465     /**
JM 466      * Returns the list of all users available to the login service.
467      * 
468      * @see IUserService.getAllUsernames()
469      * @return list of all usernames
470      */
f98825 471     public List<String> getAllUsernames() {
85c2e6 472         List<String> names = new ArrayList<String>(userService.getAllUsernames());
00afd7 473         return names;
abeaaf 474     }
e36d4d 475
abeaaf 476     /**
JM 477      * Returns the list of all users available to the login service.
478      * 
479      * @see IUserService.getAllUsernames()
480      * @return list of all usernames
481      */
482     public List<UserModel> getAllUsers() {
483         List<UserModel> users = userService.getAllUsers();
484         return users;
8a2e9c 485     }
JM 486
892570 487     /**
JM 488      * Delete the user object with the specified username
489      * 
490      * @see IUserService.deleteUser(String)
491      * @param username
492      * @return true if successful
493      */
8a2e9c 494     public boolean deleteUser(String username) {
85c2e6 495         return userService.deleteUser(username);
f98825 496     }
dfb889 497
892570 498     /**
JM 499      * Retrieve the user object for the specified username.
500      * 
501      * @see IUserService.getUserModel(String)
502      * @param username
503      * @return a user object or null
504      */
f98825 505     public UserModel getUserModel(String username) {
85c2e6 506         UserModel user = userService.getUserModel(username);
dfb889 507         return user;
f98825 508     }
8a2e9c 509
892570 510     /**
JM 511      * Returns the list of all users who are allowed to bypass the access
512      * restriction placed on the specified repository.
513      * 
514      * @see IUserService.getUsernamesForRepositoryRole(String)
515      * @param repository
516      * @return list of all usernames that can bypass the access restriction
517      */
f98825 518     public List<String> getRepositoryUsers(RepositoryModel repository) {
892570 519         return userService.getUsernamesForRepositoryRole(repository.name);
f98825 520     }
8a2e9c 521
892570 522     /**
JM 523      * Sets the list of all uses who are allowed to bypass the access
524      * restriction placed on the specified repository.
525      * 
526      * @see IUserService.setUsernamesForRepositoryRole(String, List<String>)
527      * @param repository
528      * @param usernames
529      * @return true if successful
530      */
f98825 531     public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
892570 532         return userService.setUsernamesForRepositoryRole(repository.name, repositoryUsers);
dfb889 533     }
JM 534
892570 535     /**
JM 536      * Adds/updates a complete user object keyed by username. This method allows
537      * for renaming a user.
538      * 
539      * @see IUserService.updateUserModel(String, UserModel)
540      * @param username
541      * @param user
542      * @param isCreate
543      * @throws GitBlitException
544      */
545     public void updateUserModel(String username, UserModel user, boolean isCreate)
2a7306 546             throws GitBlitException {
16038c 547         if (!username.equalsIgnoreCase(user.username)) {
JM 548             if (userService.getUserModel(user.username) != null) {
d03aff 549                 throw new GitBlitException(MessageFormat.format(
JM 550                         "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
551                         user.username));
16038c 552             }
JM 553         }
85c2e6 554         if (!userService.updateUserModel(username, user)) {
dfb889 555             throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
JM 556         }
fe24a0 557     }
JM 558
559     /**
560      * Returns the list of available teams that a user or repository may be
561      * assigned to.
562      * 
563      * @return the list of teams
564      */
565     public List<String> getAllTeamnames() {
566         List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
abeaaf 567         return teams;
JM 568     }
e36d4d 569
abeaaf 570     /**
JM 571      * Returns the list of available teams that a user or repository may be
572      * assigned to.
573      * 
574      * @return the list of teams
575      */
576     public List<TeamModel> getAllTeams() {
577         List<TeamModel> teams = userService.getAllTeams();
fe24a0 578         return teams;
JM 579     }
580
581     /**
582      * Returns the TeamModel object for the specified name.
583      * 
584      * @param teamname
585      * @return a TeamModel object or null
586      */
587     public TeamModel getTeamModel(String teamname) {
588         return userService.getTeamModel(teamname);
589     }
590
591     /**
592      * Returns the list of all teams who are allowed to bypass the access
593      * restriction placed on the specified repository.
594      * 
595      * @see IUserService.getTeamnamesForRepositoryRole(String)
596      * @param repository
597      * @return list of all teamnames that can bypass the access restriction
598      */
599     public List<String> getRepositoryTeams(RepositoryModel repository) {
600         return userService.getTeamnamesForRepositoryRole(repository.name);
601     }
602
603     /**
604      * Sets the list of all uses who are allowed to bypass the access
605      * restriction placed on the specified repository.
606      * 
607      * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
608      * @param repository
609      * @param teamnames
610      * @return true if successful
611      */
612     public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
613         return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
614     }
fa54be 615
fe24a0 616     /**
JM 617      * Updates the TeamModel object for the specified name.
618      * 
619      * @param teamname
620      * @param team
621      * @param isCreate
622      */
fa54be 623     public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
JM 624             throws GitBlitException {
fe24a0 625         if (!teamname.equalsIgnoreCase(team.name)) {
JM 626             if (userService.getTeamModel(team.name) != null) {
627                 throw new GitBlitException(MessageFormat.format(
628                         "Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
629                         team.name));
630             }
631         }
632         if (!userService.updateTeamModel(teamname, team)) {
633             throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
634         }
635     }
fa54be 636
fe24a0 637     /**
JM 638      * Delete the team object with the specified teamname
639      * 
640      * @see IUserService.deleteTeam(String)
641      * @param teamname
642      * @return true if successful
643      */
644     public boolean deleteTeam(String teamname) {
645         return userService.deleteTeam(teamname);
dfb889 646     }
JM 647
892570 648     /**
4d44cf 649      * Clears all the cached data for the specified repository.
JM 650      * 
651      * @param repositoryName
652      */
653     public void clearRepositoryCache(String repositoryName) {
654         repositorySizeCache.remove(repositoryName);
655         repositoryMetricsCache.remove(repositoryName);
656     }
657
658     /**
892570 659      * Returns the list of all repositories available to Gitblit. This method
JM 660      * does not consider user access permissions.
661      * 
662      * @return list of all repositories
663      */
166e6a 664     public List<String> getRepositoryList() {
b86562 665         return JGitUtils.getRepositoryList(repositoriesFolder, 
JM 666                 settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
892570 667                 settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true));
166e6a 668     }
155bf7 669
892570 670     /**
JM 671      * Returns the JGit repository for the specified name.
672      * 
673      * @param repositoryName
674      * @return repository or null
675      */
166e6a 676     public Repository getRepository(String repositoryName) {
11924d 677         return getRepository(repositoryName, true);
JM 678     }
679
680     /**
681      * Returns the JGit repository for the specified name.
682      * 
683      * @param repositoryName
684      * @param logError
685      * @return repository or null
686      */
687     public Repository getRepository(String repositoryName, boolean logError) {
166e6a 688         Repository r = null;
JM 689         try {
690             r = repositoryResolver.open(null, repositoryName);
691         } catch (RepositoryNotFoundException e) {
692             r = null;
11924d 693             if (logError) {
JM 694                 logger.error("GitBlit.getRepository(String) failed to find "
695                         + new File(repositoriesFolder, repositoryName).getAbsolutePath());
696             }
892570 697         } catch (ServiceNotAuthorizedException e) {
JM 698             r = null;
11924d 699             if (logError) {
JM 700                 logger.error("GitBlit.getRepository(String) failed to find "
701                         + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
702             }
166e6a 703         } catch (ServiceNotEnabledException e) {
JM 704             r = null;
11924d 705             if (logError) {
JM 706                 logger.error("GitBlit.getRepository(String) failed to find "
707                         + new File(repositoriesFolder, repositoryName).getAbsolutePath(), e);
708             }
166e6a 709         }
JM 710         return r;
711     }
dfb889 712
892570 713     /**
JM 714      * Returns the list of repository models that are accessible to the user.
715      * 
716      * @param user
717      * @return list of repository models accessible to user
718      */
511554 719     public List<RepositoryModel> getRepositoryModels(UserModel user) {
166e6a 720         List<String> list = getRepositoryList();
JM 721         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
722         for (String repo : list) {
dfb889 723             RepositoryModel model = getRepositoryModel(user, repo);
JM 724             if (model != null) {
725                 repositories.add(model);
726             }
166e6a 727         }
3d293a 728         if (getBoolean(Keys.web.showRepositorySizes, true)) {
JM 729             int repoCount = 0;
730             long startTime = System.currentTimeMillis();
731             ByteFormat byteFormat = new ByteFormat();
732             for (RepositoryModel model : repositories) {
733                 if (!model.skipSizeCalculation) {
734                     repoCount++;
735                     model.size = byteFormat.format(calculateSize(model));
736                 }
737             }
738             long duration = System.currentTimeMillis() - startTime;
739             logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
740                     repoCount, duration));
741         }
166e6a 742         return repositories;
JM 743     }
8a2e9c 744
892570 745     /**
JM 746      * Returns a repository model if the repository exists and the user may
747      * access the repository.
748      * 
749      * @param user
750      * @param repositoryName
751      * @return repository model or null
752      */
511554 753     public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
dfb889 754         RepositoryModel model = getRepositoryModel(repositoryName);
85c2e6 755         if (model == null) {
JM 756             return null;
757         }
dfb889 758         if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
efe8ec 759             if (user != null && user.canAccessRepository(model)) {
dfb889 760                 return model;
JM 761             }
762             return null;
763         } else {
764             return model;
765         }
766     }
767
892570 768     /**
JM 769      * Returns the repository model for the specified repository. This method
770      * does not consider user access permissions.
771      * 
772      * @param repositoryName
773      * @return repository model or null
774      */
166e6a 775     public RepositoryModel getRepositoryModel(String repositoryName) {
JM 776         Repository r = getRepository(repositoryName);
1f9dae 777         if (r == null) {
JM 778             return null;
779         }
166e6a 780         RepositoryModel model = new RepositoryModel();
JM 781         model.name = repositoryName;
bc9d4a 782         model.hasCommits = JGitUtils.hasCommits(r);
ed21d2 783         model.lastChange = JGitUtils.getLastChange(r, null);
b74031 784         model.isBare = r.isBare();
166e6a 785         StoredConfig config = JGitUtils.readConfig(r);
JM 786         if (config != null) {
00afd7 787             model.description = getConfig(config, "description", "");
JM 788             model.owner = getConfig(config, "owner", "");
789             model.useTickets = getConfig(config, "useTickets", false);
790             model.useDocs = getConfig(config, "useDocs", false);
2a7306 791             model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
JM 792                     "accessRestriction", null));
00afd7 793             model.showRemoteBranches = getConfig(config, "showRemoteBranches", false);
JM 794             model.isFrozen = getConfig(config, "isFrozen", false);
a1ea87 795             model.showReadme = getConfig(config, "showReadme", false);
3d293a 796             model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
fe3262 797             model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
831469 798             model.federationStrategy = FederationStrategy.fromName(getConfig(config,
JM 799                     "federationStrategy", null));
8f73a7 800             model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 801                     "gitblit", null, "federationSets")));
831469 802             model.isFederated = getConfig(config, "isFederated", false);
JM 803             model.origin = config.getString("remote", "origin", "url");
fa54be 804             model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 805                     "gitblit", null, "preReceiveScript")));
806             model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
807                     "gitblit", null, "postReceiveScript")));
eb96ea 808             model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
JM 809                     "gitblit", null, "mailingList")));
166e6a 810         }
90b8d7 811         model.HEAD = JGitUtils.getHEADRef(r);
JM 812         model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
166e6a 813         r.close();
JM 814         return model;
00afd7 815     }
8a2e9c 816
892570 817     /**
4d44cf 818      * Returns the size in bytes of the repository. Gitblit caches the
JM 819      * repository sizes to reduce the performance penalty of recursive
820      * calculation. The cache is updated if the repository has been changed
821      * since the last calculation.
5c2841 822      * 
JM 823      * @param model
824      * @return size in bytes
825      */
826     public long calculateSize(RepositoryModel model) {
4d44cf 827         if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
JM 828             return repositorySizeCache.getObject(model.name);
829         }
5c2841 830         File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
4d44cf 831         long size = com.gitblit.utils.FileUtils.folderSize(gitDir);
JM 832         repositorySizeCache.updateObject(model.name, model.lastChange, size);
833         return size;
5c2841 834     }
JM 835
836     /**
dd6f08 837      * Ensure that a cached repository is completely closed and its resources
JM 838      * are properly released.
839      * 
840      * @param repositoryName
841      */
842     private void closeRepository(String repositoryName) {
843         Repository repository = getRepository(repositoryName);
844         // assume 2 uses in case reflection fails
845         int uses = 2;
846         try {
847             // The FileResolver caches repositories which is very useful
848             // for performance until you want to delete a repository.
849             // I have to use reflection to call close() the correct
850             // number of times to ensure that the object and ref databases
851             // are properly closed before I can delete the repository from
852             // the filesystem.
853             Field useCnt = Repository.class.getDeclaredField("useCnt");
854             useCnt.setAccessible(true);
855             uses = ((AtomicInteger) useCnt.get(repository)).get();
856         } catch (Exception e) {
857             logger.warn(MessageFormat
858                     .format("Failed to reflectively determine use count for repository {0}",
859                             repositoryName), e);
860         }
861         if (uses > 0) {
862             logger.info(MessageFormat
863                     .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
864                             repositoryName, uses, uses));
865             for (int i = 0; i < uses; i++) {
866                 repository.close();
867             }
868         }
869     }
870
871     /**
4d44cf 872      * Returns the metrics for the default branch of the specified repository.
JM 873      * This method builds a metrics cache. The cache is updated if the
874      * repository is updated. A new copy of the metrics list is returned on each
875      * call so that modifications to the list are non-destructive.
876      * 
877      * @param model
878      * @param repository
879      * @return a new array list of metrics
880      */
881     public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
882         if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
883             return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
884         }
885         List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null);
886         repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
887         return new ArrayList<Metric>(metrics);
888     }
889
890     /**
891      * Returns the gitblit string value for the specified key. If key is not
892570 892      * set, returns defaultValue.
JM 893      * 
894      * @param config
895      * @param field
896      * @param defaultValue
897      * @return field value or defaultValue
898      */
00afd7 899     private String getConfig(StoredConfig config, String field, String defaultValue) {
JM 900         String value = config.getString("gitblit", null, field);
901         if (StringUtils.isEmpty(value)) {
902             return defaultValue;
903         }
904         return value;
905     }
8a2e9c 906
892570 907     /**
JM 908      * Returns the gitblit boolean vlaue for the specified key. If key is not
909      * set, returns defaultValue.
910      * 
911      * @param config
912      * @param field
913      * @param defaultValue
914      * @return field value or defaultValue
915      */
00afd7 916     private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
JM 917         return config.getBoolean("gitblit", field, defaultValue);
166e6a 918     }
JM 919
892570 920     /**
JM 921      * Creates/updates the repository model keyed by reopsitoryName. Saves all
922      * repository settings in .git/config. This method allows for renaming
923      * repositories and will update user access permissions accordingly.
924      * 
925      * All repositories created by this method are bare and automatically have
926      * .git appended to their names, which is the standard convention for bare
927      * repositories.
928      * 
929      * @param repositoryName
930      * @param repository
931      * @param isCreate
932      * @throws GitBlitException
933      */
934     public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
2a7306 935             boolean isCreate) throws GitBlitException {
f5d0ad 936         Repository r = null;
JM 937         if (isCreate) {
a3bde6 938             // ensure created repository name ends with .git
168566 939             if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
a3bde6 940                 repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
JM 941             }
166e6a 942             if (new File(repositoriesFolder, repository.name).exists()) {
2a7306 943                 throw new GitBlitException(MessageFormat.format(
JM 944                         "Can not create repository ''{0}'' because it already exists.",
945                         repository.name));
166e6a 946             }
dfb889 947             // create repository
f5d0ad 948             logger.info("create repository " + repository.name);
168566 949             r = JGitUtils.createRepository(repositoriesFolder, repository.name);
f5d0ad 950         } else {
8a2e9c 951             // rename repository
JM 952             if (!repositoryName.equalsIgnoreCase(repository.name)) {
16038c 953                 if (!repository.name.toLowerCase().endsWith(
JM 954                         org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
955                     repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
956                 }
957                 if (new File(repositoriesFolder, repository.name).exists()) {
d03aff 958                     throw new GitBlitException(MessageFormat.format(
JM 959                             "Failed to rename ''{0}'' because ''{1}'' already exists.",
960                             repositoryName, repository.name));
16038c 961                 }
dd6f08 962                 closeRepository(repositoryName);
8a2e9c 963                 File folder = new File(repositoriesFolder, repositoryName);
JM 964                 File destFolder = new File(repositoriesFolder, repository.name);
965                 if (destFolder.exists()) {
2a7306 966                     throw new GitBlitException(
JM 967                             MessageFormat
968                                     .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
969                                             repositoryName, repository.name));
8a2e9c 970                 }
c1a4cc 971                 File parentFile = destFolder.getParentFile();
JM 972                 if (!parentFile.exists() && !parentFile.mkdirs()) {
973                     throw new GitBlitException(MessageFormat.format(
974                             "Failed to create folder ''{0}''", parentFile.getAbsolutePath()));
975                 }
8a2e9c 976                 if (!folder.renameTo(destFolder)) {
2a7306 977                     throw new GitBlitException(MessageFormat.format(
JM 978                             "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
979                             repository.name));
8a2e9c 980                 }
JM 981                 // rename the roles
85c2e6 982                 if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
2a7306 983                     throw new GitBlitException(MessageFormat.format(
JM 984                             "Failed to rename repository permissions ''{0}'' to ''{1}''.",
985                             repositoryName, repository.name));
8a2e9c 986                 }
4d44cf 987
JM 988                 // clear the cache
989                 clearRepositoryCache(repositoryName);
8a2e9c 990             }
JM 991
f5d0ad 992             // load repository
JM 993             logger.info("edit repository " + repository.name);
994             try {
995                 r = repositoryResolver.open(null, repository.name);
996             } catch (RepositoryNotFoundException e) {
997                 logger.error("Repository not found", e);
892570 998             } catch (ServiceNotAuthorizedException e) {
JM 999                 logger.error("Service not authorized", e);
f5d0ad 1000             } catch (ServiceNotEnabledException e) {
JM 1001                 logger.error("Service not enabled", e);
1002             }
f97bf0 1003         }
JM 1004
f5d0ad 1005         // update settings
2a7306 1006         if (r != null) {
831469 1007             updateConfiguration(r, repository);
2cdb73 1008             // only update symbolic head if it changes
90b8d7 1009             String currentRef = JGitUtils.getHEADRef(r);
JM 1010             if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) {
d394d9 1011                 logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}", 
90b8d7 1012                         repository.name, currentRef, repository.HEAD));
JM 1013                 if (JGitUtils.setHEADtoRef(r, repository.HEAD)) {
1014                     // clear the cache
1015                     clearRepositoryCache(repository.name);
1016                 }
912938 1017             }
d394d9 1018
JM 1019             // close the repository object
2a7306 1020             r.close();
831469 1021         }
JM 1022     }
1023
1024     /**
1025      * Updates the Gitblit configuration for the specified repository.
1026      * 
1027      * @param r
1028      *            the Git repository
1029      * @param repository
1030      *            the Gitblit repository model
1031      */
1032     public void updateConfiguration(Repository r, RepositoryModel repository) {
1033         StoredConfig config = JGitUtils.readConfig(r);
1034         config.setString("gitblit", null, "description", repository.description);
1035         config.setString("gitblit", null, "owner", repository.owner);
1036         config.setBoolean("gitblit", null, "useTickets", repository.useTickets);
1037         config.setBoolean("gitblit", null, "useDocs", repository.useDocs);
1038         config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name());
1039         config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches);
1040         config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen);
1041         config.setBoolean("gitblit", null, "showReadme", repository.showReadme);
3d293a 1042         config.setBoolean("gitblit", null, "skipSizeCalculation", repository.skipSizeCalculation);
fe3262 1043         config.setBoolean("gitblit", null, "skipSummaryMetrics", repository.skipSummaryMetrics);
8f73a7 1044         config.setStringList("gitblit", null, "federationSets", repository.federationSets);
831469 1045         config.setString("gitblit", null, "federationStrategy",
JM 1046                 repository.federationStrategy.name());
1047         config.setBoolean("gitblit", null, "isFederated", repository.isFederated);
0db5c4 1048         if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
3a2c57 1049             config.setStringList("gitblit", null, "preReceiveScript", repository.preReceiveScripts);
JM 1050         }
0db5c4 1051         if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
3a2c57 1052             config.setStringList("gitblit", null, "postReceiveScript",
JM 1053                     repository.postReceiveScripts);
1054         }
0db5c4 1055         if (!ArrayUtils.isEmpty(repository.mailingLists)) {
eb96ea 1056             config.setStringList("gitblit", null, "mailingList", repository.mailingLists);
3a2c57 1057         }
831469 1058         try {
JM 1059             config.save();
1060         } catch (IOException e) {
1061             logger.error("Failed to save repository config!", e);
f97bf0 1062         }
f5d0ad 1063     }
JM 1064
892570 1065     /**
JM 1066      * Deletes the repository from the file system and removes the repository
1067      * permission from all repository users.
1068      * 
1069      * @param model
1070      * @return true if successful
1071      */
8a2e9c 1072     public boolean deleteRepositoryModel(RepositoryModel model) {
JM 1073         return deleteRepository(model.name);
1074     }
1075
892570 1076     /**
JM 1077      * Deletes the repository from the file system and removes the repository
1078      * permission from all repository users.
1079      * 
1080      * @param repositoryName
1081      * @return true if successful
1082      */
8a2e9c 1083     public boolean deleteRepository(String repositoryName) {
JM 1084         try {
dd6f08 1085             closeRepository(repositoryName);
8a2e9c 1086             File folder = new File(repositoriesFolder, repositoryName);
JM 1087             if (folder.exists() && folder.isDirectory()) {
892570 1088                 FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
85c2e6 1089                 if (userService.deleteRepositoryRole(repositoryName)) {
8a2e9c 1090                     return true;
JM 1091                 }
1092             }
4d44cf 1093
JM 1094             // clear the repository cache
1095             clearRepositoryCache(repositoryName);
8a2e9c 1096         } catch (Throwable t) {
JM 1097             logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
1098         }
1099         return false;
1100     }
1101
892570 1102     /**
JM 1103      * Returns an html version of the commit message with any global or
1104      * repository-specific regular expression substitution applied.
1105      * 
1106      * @param repositoryName
1107      * @param text
1108      * @return html version of the commit message
1109      */
8c9a20 1110     public String processCommitMessage(String repositoryName, String text) {
JM 1111         String html = StringUtils.breakLinesForHtml(text);
1112         Map<String, String> map = new HashMap<String, String>();
1113         // global regex keys
892570 1114         if (settings.getBoolean(Keys.regex.global, false)) {
JM 1115             for (String key : settings.getAllKeys(Keys.regex.global)) {
8c9a20 1116                 if (!key.equals(Keys.regex.global)) {
JM 1117                     String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1118                     map.put(subKey, settings.getString(key, ""));
8c9a20 1119                 }
JM 1120             }
1121         }
1122
1123         // repository-specific regex keys
892570 1124         List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "."
8c9a20 1125                 + repositoryName.toLowerCase());
JM 1126         for (String key : keys) {
1127             String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1128             map.put(subKey, settings.getString(key, ""));
8c9a20 1129         }
JM 1130
1131         for (Entry<String, String> entry : map.entrySet()) {
1132             String definition = entry.getValue().trim();
1133             String[] chunks = definition.split("!!!");
1134             if (chunks.length == 2) {
1135                 html = html.replaceAll(chunks[0], chunks[1]);
1136             } else {
1137                 logger.warn(entry.getKey()
1138                         + " improperly formatted.  Use !!! to separate match from replacement: "
1139                         + definition);
1140             }
1141         }
1142         return html;
1143     }
1144
892570 1145     /**
831469 1146      * Returns Gitblit's scheduled executor service for scheduling tasks.
JM 1147      * 
1148      * @return scheduledExecutor
1149      */
1150     public ScheduledExecutorService executor() {
1151         return scheduledExecutor;
1152     }
1153
1154     public static boolean canFederate() {
2c32fd 1155         String passphrase = getString(Keys.federation.passphrase, "");
JM 1156         return !StringUtils.isEmpty(passphrase);
831469 1157     }
JM 1158
1159     /**
1160      * Configures this Gitblit instance to pull any registered federated gitblit
1161      * instances.
1162      */
1163     private void configureFederation() {
2c32fd 1164         boolean validPassphrase = true;
JM 1165         String passphrase = settings.getString(Keys.federation.passphrase, "");
1166         if (StringUtils.isEmpty(passphrase)) {
1167             logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
1168             validPassphrase = false;
831469 1169         }
2c32fd 1170         if (validPassphrase) {
8f73a7 1171             // standard tokens
831469 1172             for (FederationToken tokenType : FederationToken.values()) {
JM 1173                 logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
1174                         getFederationToken(tokenType)));
8f73a7 1175             }
JM 1176
1177             // federation set tokens
1178             for (String set : settings.getStrings(Keys.federation.sets)) {
1179                 logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
1180                         getFederationToken(set)));
831469 1181             }
JM 1182         }
1183
1184         // Schedule the federation executor
1185         List<FederationModel> registrations = getFederationRegistrations();
1186         if (registrations.size() > 0) {
f6740d 1187             FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
JM 1188             scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
831469 1189         }
JM 1190     }
1191
1192     /**
1193      * Returns the list of federated gitblit instances that this instance will
1194      * try to pull.
1195      * 
1196      * @return list of registered gitblit instances
1197      */
1198     public List<FederationModel> getFederationRegistrations() {
1199         if (federationRegistrations.isEmpty()) {
f6740d 1200             federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
831469 1201         }
JM 1202         return federationRegistrations;
1203     }
1204
1205     /**
1206      * Retrieve the specified federation registration.
1207      * 
1208      * @param name
1209      *            the name of the registration
1210      * @return a federation registration
1211      */
1212     public FederationModel getFederationRegistration(String url, String name) {
1213         // check registrations
1214         for (FederationModel r : getFederationRegistrations()) {
1215             if (r.name.equals(name) && r.url.equals(url)) {
1216                 return r;
1217             }
1218         }
1219
1220         // check the results
1221         for (FederationModel r : getFederationResultRegistrations()) {
1222             if (r.name.equals(name) && r.url.equals(url)) {
1223                 return r;
1224             }
1225         }
1226         return null;
1227     }
1228
1229     /**
31abc2 1230      * Returns the list of federation sets.
JM 1231      * 
1232      * @return list of federation sets
1233      */
1234     public List<FederationSet> getFederationSets(String gitblitUrl) {
1235         List<FederationSet> list = new ArrayList<FederationSet>();
1236         // generate standard tokens
1237         for (FederationToken type : FederationToken.values()) {
1238             FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
1239             fset.repositories = getRepositories(gitblitUrl, fset.token);
1240             list.add(fset);
1241         }
1242         // generate tokens for federation sets
1243         for (String set : settings.getStrings(Keys.federation.sets)) {
1244             FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
1245                     getFederationToken(set));
1246             fset.repositories = getRepositories(gitblitUrl, fset.token);
1247             list.add(fset);
1248         }
1249         return list;
1250     }
1251
1252     /**
831469 1253      * Returns the list of possible federation tokens for this Gitblit instance.
JM 1254      * 
1255      * @return list of federation tokens
1256      */
1257     public List<String> getFederationTokens() {
1258         List<String> tokens = new ArrayList<String>();
8f73a7 1259         // generate standard tokens
831469 1260         for (FederationToken type : FederationToken.values()) {
JM 1261             tokens.add(getFederationToken(type));
8f73a7 1262         }
JM 1263         // generate tokens for federation sets
1264         for (String set : settings.getStrings(Keys.federation.sets)) {
1265             tokens.add(getFederationToken(set));
831469 1266         }
JM 1267         return tokens;
1268     }
1269
1270     /**
1271      * Returns the specified federation token for this Gitblit instance.
1272      * 
1273      * @param type
1274      * @return a federation token
1275      */
1276     public String getFederationToken(FederationToken type) {
8f73a7 1277         return getFederationToken(type.name());
JM 1278     }
1279
1280     /**
1281      * Returns the specified federation token for this Gitblit instance.
1282      * 
1283      * @param value
1284      * @return a federation token
1285      */
1286     public String getFederationToken(String value) {
2c32fd 1287         String passphrase = settings.getString(Keys.federation.passphrase, "");
8f73a7 1288         return StringUtils.getSHA1(passphrase + "-" + value);
831469 1289     }
JM 1290
1291     /**
1292      * Compares the provided token with this Gitblit instance's tokens and
1293      * determines if the requested permission may be granted to the token.
1294      * 
1295      * @param req
1296      * @param token
1297      * @return true if the request can be executed
1298      */
1299     public boolean validateFederationRequest(FederationRequest req, String token) {
1300         String all = getFederationToken(FederationToken.ALL);
1301         String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
1302         String jur = getFederationToken(FederationToken.REPOSITORIES);
1303         switch (req) {
1304         case PULL_REPOSITORIES:
1305             return token.equals(all) || token.equals(unr) || token.equals(jur);
1306         case PULL_USERS:
fe24a0 1307         case PULL_TEAMS:
831469 1308             return token.equals(all) || token.equals(unr);
JM 1309         case PULL_SETTINGS:
df162c 1310         case PULL_SCRIPTS:
831469 1311             return token.equals(all);
JM 1312         }
1313         return false;
1314     }
1315
1316     /**
1317      * Acknowledge and cache the status of a remote Gitblit instance.
1318      * 
1319      * @param identification
1320      *            the identification of the pulling Gitblit instance
1321      * @param registration
1322      *            the registration from the pulling Gitblit instance
1323      * @return true if acknowledged
1324      */
1325     public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
1326         // reset the url to the identification of the pulling Gitblit instance
1327         registration.url = identification;
1328         String id = identification;
1329         if (!StringUtils.isEmpty(registration.folder)) {
1330             id += "-" + registration.folder;
1331         }
1332         federationPullResults.put(id, registration);
1333         return true;
1334     }
1335
1336     /**
1337      * Returns the list of registration results.
1338      * 
1339      * @return the list of registration results
1340      */
1341     public List<FederationModel> getFederationResultRegistrations() {
1342         return new ArrayList<FederationModel>(federationPullResults.values());
1343     }
1344
1345     /**
1346      * Submit a federation proposal. The proposal is cached locally and the
1347      * Gitblit administrator(s) are notified via email.
1348      * 
1349      * @param proposal
1350      *            the proposal
1351      * @param gitblitUrl
4aafd4 1352      *            the url of your gitblit instance to send an email to
JM 1353      *            administrators
831469 1354      * @return true if the proposal was submitted
JM 1355      */
1356     public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
1357         // convert proposal to json
93f0b1 1358         String json = JsonUtils.toJsonString(proposal);
831469 1359
JM 1360         try {
1361             // make the proposals folder
b774de 1362             File proposalsFolder = getProposalsFolder();
831469 1363             proposalsFolder.mkdirs();
JM 1364
1365             // cache json to a file
1366             File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
1367             com.gitblit.utils.FileUtils.writeContent(file, json);
1368         } catch (Exception e) {
1369             logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
1370         }
1371
1372         // send an email, if possible
1373         try {
1374             Message message = mailExecutor.createMessageForAdministrators();
1375             if (message != null) {
1376                 message.setSubject("Federation proposal from " + proposal.url);
1377                 message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
1378                         + proposal.token);
1379                 mailExecutor.queue(message);
1380             }
1381         } catch (Throwable t) {
1382             logger.error("Failed to notify administrators of proposal", t);
1383         }
1384         return true;
1385     }
1386
1387     /**
1388      * Returns the list of pending federation proposals
1389      * 
1390      * @return list of federation proposals
1391      */
1392     public List<FederationProposal> getPendingFederationProposals() {
1393         List<FederationProposal> list = new ArrayList<FederationProposal>();
b774de 1394         File folder = getProposalsFolder();
831469 1395         if (folder.exists()) {
JM 1396             File[] files = folder.listFiles(new FileFilter() {
1397                 @Override
1398                 public boolean accept(File file) {
1399                     return file.isFile()
1400                             && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
1401                 }
1402             });
1403             for (File file : files) {
1404                 String json = com.gitblit.utils.FileUtils.readContent(file, null);
31abc2 1405                 FederationProposal proposal = JsonUtils.fromJsonString(json,
JM 1406                         FederationProposal.class);
831469 1407                 list.add(proposal);
JM 1408             }
1409         }
1410         return list;
1411     }
1412
1413     /**
dd9ae7 1414      * Get repositories for the specified token.
JM 1415      * 
1416      * @param gitblitUrl
1417      *            the base url of this gitblit instance
1418      * @param token
1419      *            the federation token
1420      * @return a map of <cloneurl, RepositoryModel>
1421      */
1422     public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
1423         Map<String, String> federationSets = new HashMap<String, String>();
1424         for (String set : getStrings(Keys.federation.sets)) {
1425             federationSets.put(getFederationToken(set), set);
1426         }
1427
1428         // Determine the Gitblit clone url
1429         StringBuilder sb = new StringBuilder();
1430         sb.append(gitblitUrl);
1431         sb.append(Constants.GIT_PATH);
1432         sb.append("{0}");
1433         String cloneUrl = sb.toString();
1434
1435         // Retrieve all available repositories
1436         UserModel user = new UserModel(Constants.FEDERATION_USER);
1437         user.canAdmin = true;
1438         List<RepositoryModel> list = getRepositoryModels(user);
1439
1440         // create the [cloneurl, repositoryModel] map
1441         Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
1442         for (RepositoryModel model : list) {
1443             // by default, setup the url for THIS repository
1444             String url = MessageFormat.format(cloneUrl, model.name);
1445             switch (model.federationStrategy) {
1446             case EXCLUDE:
1447                 // skip this repository
1448                 continue;
1449             case FEDERATE_ORIGIN:
1450                 // federate the origin, if it is defined
1451                 if (!StringUtils.isEmpty(model.origin)) {
1452                     url = model.origin;
1453                 }
1454                 break;
1455             }
1456
1457             if (federationSets.containsKey(token)) {
1458                 // include repositories only for federation set
1459                 String set = federationSets.get(token);
1460                 if (model.federationSets.contains(set)) {
1461                     repositories.put(url, model);
1462                 }
1463             } else {
1464                 // standard federation token for ALL
1465                 repositories.put(url, model);
1466             }
1467         }
1468         return repositories;
1469     }
1470
1471     /**
1472      * Creates a proposal from the token.
1473      * 
1474      * @param gitblitUrl
1475      *            the url of this Gitblit instance
1476      * @param token
1477      * @return a potential proposal
1478      */
1479     public FederationProposal createFederationProposal(String gitblitUrl, String token) {
1480         FederationToken tokenType = FederationToken.REPOSITORIES;
1481         for (FederationToken type : FederationToken.values()) {
1482             if (token.equals(getFederationToken(type))) {
1483                 tokenType = type;
1484                 break;
1485             }
1486         }
1487         Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
1488         FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
1489                 repositories);
1490         return proposal;
1491     }
1492
1493     /**
831469 1494      * Returns the proposal identified by the supplied token.
JM 1495      * 
1496      * @param token
1497      * @return the specified proposal or null
1498      */
1499     public FederationProposal getPendingFederationProposal(String token) {
1500         List<FederationProposal> list = getPendingFederationProposals();
1501         for (FederationProposal proposal : list) {
1502             if (proposal.token.equals(token)) {
1503                 return proposal;
1504             }
1505         }
1506         return null;
1507     }
1508
1509     /**
1510      * Deletes a pending federation proposal.
1511      * 
1512      * @param a
1513      *            proposal
1514      * @return true if the proposal was deleted
1515      */
1516     public boolean deletePendingFederationProposal(FederationProposal proposal) {
b774de 1517         File folder = getProposalsFolder();
831469 1518         File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
JM 1519         return file.delete();
1520     }
1521
1522     /**
d7905a 1523      * Returns the list of all Groovy push hook scripts. Script files must have
6cc1d4 1524      * .groovy extension
JM 1525      * 
1526      * @return list of available hook scripts
1527      */
d7905a 1528     public List<String> getAllScripts() {
6cc1d4 1529         File groovyFolder = getGroovyScriptsFolder();
JM 1530         File[] files = groovyFolder.listFiles(new FileFilter() {
1531             @Override
1532             public boolean accept(File pathname) {
1533                 return pathname.isFile() && pathname.getName().endsWith(".groovy");
1534             }
1535         });
1536         List<String> scripts = new ArrayList<String>();
1537         if (files != null) {
1538             for (File file : files) {
1539                 String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
d7905a 1540                 scripts.add(script);
6cc1d4 1541             }
JM 1542         }
1543         return scripts;
1544     }
d7905a 1545
JM 1546     /**
1547      * Returns the list of pre-receive scripts the repository inherited from the
1548      * global settings and team affiliations.
1549      * 
1550      * @param repository
1551      *            if null only the globally specified scripts are returned
1552      * @return a list of scripts
1553      */
1554     public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
1555         Set<String> scripts = new LinkedHashSet<String>();
1556         // Globals
0d013a 1557         for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
JM 1558             if (script.endsWith(".groovy")) {
d7905a 1559                 scripts.add(script.substring(0, script.lastIndexOf('.')));
0d013a 1560             } else {
d7905a 1561                 scripts.add(script);
0d013a 1562             }
JM 1563         }
d7905a 1564
JM 1565         // Team Scripts
1566         if (repository != null) {
1567             for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
1568                 TeamModel team = userService.getTeamModel(teamname);
1569                 scripts.addAll(team.preReceiveScripts);
1570             }
1571         }
1572         return new ArrayList<String>(scripts);
0d013a 1573     }
d7905a 1574
JM 1575     /**
1576      * Returns the list of all available Groovy pre-receive push hook scripts
1577      * that are not already inherited by the repository. Script files must have
1578      * .groovy extension
1579      * 
1580      * @param repository
1581      *            optional parameter
1582      * @return list of available hook scripts
1583      */
1584     public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
1585         Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
1586
1587         // create list of available scripts by excluding inherited scripts
1588         List<String> scripts = new ArrayList<String>();
1589         for (String script : getAllScripts()) {
1590             if (!inherited.contains(script)) {
1591                 scripts.add(script);
1592             }
1593         }
1594         return scripts;
1595     }
1596
1597     /**
1598      * Returns the list of post-receive scripts the repository inherited from
1599      * the global settings and team affiliations.
1600      * 
1601      * @param repository
1602      *            if null only the globally specified scripts are returned
1603      * @return a list of scripts
1604      */
1605     public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
1606         Set<String> scripts = new LinkedHashSet<String>();
1607         // Global Scripts
0d013a 1608         for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
JM 1609             if (script.endsWith(".groovy")) {
d7905a 1610                 scripts.add(script.substring(0, script.lastIndexOf('.')));
0d013a 1611             } else {
d7905a 1612                 scripts.add(script);
0d013a 1613             }
JM 1614         }
d7905a 1615         // Team Scripts
JM 1616         if (repository != null) {
1617             for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
1618                 TeamModel team = userService.getTeamModel(teamname);
1619                 scripts.addAll(team.postReceiveScripts);
1620             }
1621         }
1622         return new ArrayList<String>(scripts);
1623     }
1624
1625     /**
1626      * Returns the list of unused Groovy post-receive push hook scripts that are
1627      * not already inherited by the repository. Script files must have .groovy
1628      * extension
1629      * 
1630      * @param repository
1631      *            optional parameter
1632      * @return list of available hook scripts
1633      */
1634     public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
1635         Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
1636
1637         // create list of available scripts by excluding inherited scripts
1638         List<String> scripts = new ArrayList<String>();
1639         for (String script : getAllScripts()) {
1640             if (!inherited.contains(script)) {
1641                 scripts.add(script);
1642             }
1643         }
1644         return scripts;
0d013a 1645     }
JM 1646
6cc1d4 1647     /**
831469 1648      * Notify the administrators by email.
JM 1649      * 
1650      * @param subject
1651      * @param message
1652      */
eb96ea 1653     public void sendMailToAdministrators(String subject, String message) {
831469 1654         try {
JM 1655             Message mail = mailExecutor.createMessageForAdministrators();
1656             if (mail != null) {
1657                 mail.setSubject(subject);
1658                 mail.setText(message);
1659                 mailExecutor.queue(mail);
1660             }
1661         } catch (MessagingException e) {
1662             logger.error("Messaging error", e);
1663         }
1664     }
1665
1666     /**
fa54be 1667      * Notify users by email of something.
JM 1668      * 
1669      * @param subject
1670      * @param message
1671      * @param toAddresses
1672      */
0b9119 1673     public void sendMail(String subject, String message, Collection<String> toAddresses) {
eb96ea 1674         this.sendMail(subject, message, toAddresses.toArray(new String[0]));
fa54be 1675     }
JM 1676
1677     /**
1678      * Notify users by email of something.
1679      * 
1680      * @param subject
1681      * @param message
1682      * @param toAddresses
1683      */
eb96ea 1684     public void sendMail(String subject, String message, String... toAddresses) {
fa54be 1685         try {
JM 1686             Message mail = mailExecutor.createMessage(toAddresses);
1687             if (mail != null) {
1688                 mail.setSubject(subject);
1689                 mail.setText(message);
1690                 mailExecutor.queue(mail);
1691             }
1692         } catch (MessagingException e) {
1693             logger.error("Messaging error", e);
1694         }
1695     }
1696
1697     /**
b75734 1698      * Returns the descriptions/comments of the Gitblit config settings.
JM 1699      * 
84c1d5 1700      * @return SettingsModel
b75734 1701      */
84c1d5 1702     public ServerSettings getSettingsModel() {
b75734 1703         // ensure that the current values are updated in the setting models
773bb6 1704         for (String key : settings.getAllKeys(null)) {
JM 1705             SettingModel setting = settingsModel.get(key);
1706             if (setting != null) {
1707                 setting.currentValue = settings.getString(key, "");
1708             }
1709         }
d7905a 1710         settingsModel.pushScripts = getAllScripts();
84c1d5 1711         return settingsModel;
b75734 1712     }
JM 1713
1714     /**
1715      * Parse the properties file and aggregate all the comments by the setting
1716      * key. A setting model tracks the current value, the default value, the
1717      * description of the setting and and directives about the setting.
1718      * 
1719      * @return Map<String, SettingModel>
1720      */
84c1d5 1721     private ServerSettings loadSettingModels() {
JM 1722         ServerSettings settingsModel = new ServerSettings();
b75734 1723         try {
JM 1724             // Read bundled Gitblit properties to extract setting descriptions.
1725             // This copy is pristine and only used for populating the setting
1726             // models map.
97a20e 1727             InputStream is = servletContext.getResourceAsStream("/WEB-INF/reference.properties");
b75734 1728             BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
JM 1729             StringBuilder description = new StringBuilder();
1730             SettingModel setting = new SettingModel();
1731             String line = null;
1732             while ((line = propertiesReader.readLine()) != null) {
1733                 if (line.length() == 0) {
1734                     description.setLength(0);
1735                     setting = new SettingModel();
1736                 } else {
1737                     if (line.charAt(0) == '#') {
1738                         if (line.length() > 1) {
1739                             String text = line.substring(1).trim();
1740                             if (SettingModel.CASE_SENSITIVE.equals(text)) {
1741                                 setting.caseSensitive = true;
1742                             } else if (SettingModel.RESTART_REQUIRED.equals(text)) {
1743                                 setting.restartRequired = true;
1744                             } else if (SettingModel.SPACE_DELIMITED.equals(text)) {
1745                                 setting.spaceDelimited = true;
1746                             } else if (text.startsWith(SettingModel.SINCE)) {
1747                                 try {
1748                                     setting.since = text.split(" ")[1];
1749                                 } catch (Exception e) {
1750                                     setting.since = text;
1751                                 }
1752                             } else {
1753                                 description.append(text);
1754                                 description.append('\n');
1755                             }
1756                         }
1757                     } else {
1758                         String[] kvp = line.split("=", 2);
1759                         String key = kvp[0].trim();
1760                         setting.name = key;
1761                         setting.defaultValue = kvp[1].trim();
1762                         setting.currentValue = setting.defaultValue;
1763                         setting.description = description.toString().trim();
84c1d5 1764                         settingsModel.add(setting);
b75734 1765                         description.setLength(0);
JM 1766                         setting = new SettingModel();
1767                     }
1768                 }
1769             }
1770             propertiesReader.close();
1771         } catch (NullPointerException e) {
1772             logger.error("Failed to find resource copy of gitblit.properties");
1773         } catch (IOException e) {
1774             logger.error("Failed to load resource copy of gitblit.properties");
1775         }
84c1d5 1776         return settingsModel;
b75734 1777     }
JM 1778
1779     /**
892570 1780      * Configure the Gitblit singleton with the specified settings source. This
JM 1781      * source may be file settings (Gitblit GO) or may be web.xml settings
1782      * (Gitblit WAR).
1783      * 
1784      * @param settings
1785      */
f6740d 1786     public void configureContext(IStoredSettings settings, boolean startFederation) {
1f9dae 1787         logger.info("Reading configuration from " + settings.toString());
892570 1788         this.settings = settings;
b774de 1789         repositoriesFolder = getRepositoriesFolder();
5450d0 1790         logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
b86562 1791         repositoryResolver = new FileResolver<Void>(repositoriesFolder, true);
6c6e7d 1792         
JM 1793         logTimezone("JVM", TimeZone.getDefault());
1794         logTimezone(Constants.NAME, getTimezone());
1795
486ee1 1796         serverStatus = new ServerStatus(isGO());
85c2e6 1797         String realm = settings.getString(Keys.realm.userService, "users.properties");
JM 1798         IUserService loginService = null;
5450d0 1799         try {
892570 1800             // check to see if this "file" is a login service class
5450d0 1801             Class<?> realmClass = Class.forName(realm);
d25c59 1802             loginService = (IUserService) realmClass.newInstance();
5450d0 1803         } catch (Throwable t) {
eb96ea 1804             loginService = new GitblitUserService();
5450d0 1805         }
85c2e6 1806         setUserService(loginService);
831469 1807         mailExecutor = new MailExecutor(settings);
JM 1808         if (mailExecutor.isReady()) {
1809             scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
1810         } else {
1811             logger.warn("Mail server is not properly configured.  Mail services disabled.");
f6740d 1812         }
JM 1813         if (startFederation) {
1814             configureFederation();
6c6e7d 1815         }        
JM 1816     }
1817     
1818     private void logTimezone(String type, TimeZone zone) {
1819         SimpleDateFormat df = new SimpleDateFormat("z Z");
1820         df.setTimeZone(zone);
1821         String offset = df.format(new Date());
1822         logger.info(type + " timezone is " + zone.getID() + " (" + offset + ")");
87cc1e 1823     }
JM 1824
892570 1825     /**
JM 1826      * Configure Gitblit from the web.xml, if no configuration has already been
1827      * specified.
1828      * 
1829      * @see ServletContextListener.contextInitialize(ServletContextEvent)
1830      */
87cc1e 1831     @Override
JM 1832     public void contextInitialized(ServletContextEvent contextEvent) {
b75734 1833         servletContext = contextEvent.getServletContext();
773bb6 1834         settingsModel = loadSettingModels();
892570 1835         if (settings == null) {
JM 1836             // Gitblit WAR is running in a servlet container
b774de 1837             ServletContext context = contextEvent.getServletContext();
JM 1838             WebXmlSettings webxmlSettings = new WebXmlSettings(context);
1839
1840             // 0.7.0 web.properties in the deployed war folder
7a1889 1841             String webProps = context.getRealPath("/WEB-INF/web.properties");
JM 1842             if (!StringUtils.isEmpty(webProps)) {
1843                 File overrideFile = new File(webProps);
1844                 if (overrideFile.exists()) {
1845                     webxmlSettings.applyOverrides(overrideFile);
1846                 }
b774de 1847             }
7a1889 1848             
b774de 1849
JM 1850             // 0.8.0 gitblit.properties file located outside the deployed war
1851             // folder lie, for example, on RedHat OpenShift.
7a1889 1852             File overrideFile = getFileOrFolder("gitblit.properties");
b774de 1853             if (!overrideFile.getPath().equals("gitblit.properties")) {
JM 1854                 webxmlSettings.applyOverrides(overrideFile);
1855             }
f6740d 1856             configureContext(webxmlSettings, true);
e36d4d 1857
JM 1858             // Copy the included scripts to the configured groovy folder
1859             File localScripts = getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
1860             if (!localScripts.exists()) {
1861                 File includedScripts = new File(context.getRealPath("/WEB-INF/groovy"));
1862                 if (!includedScripts.equals(localScripts)) {
1863                     try {
1864                         com.gitblit.utils.FileUtils.copy(localScripts, includedScripts.listFiles());
1865                     } catch (IOException e) {
1866                         logger.error(MessageFormat.format(
1867                                 "Failed to copy included Groovy scripts from {0} to {1}",
1868                                 includedScripts, localScripts));
1869                     }
1870                 }
1871             }
87cc1e 1872         }
773bb6 1873
486ee1 1874         serverStatus.servletContainer = servletContext.getServerInfo();
87cc1e 1875     }
JM 1876
892570 1877     /**
JM 1878      * Gitblit is being shutdown either because the servlet container is
1879      * shutting down or because the servlet container is re-deploying Gitblit.
1880      */
87cc1e 1881     @Override
JM 1882     public void contextDestroyed(ServletContextEvent contextEvent) {
f339f5 1883         logger.info("Gitblit context destroyed by servlet container.");
831469 1884         scheduledExecutor.shutdownNow();
87cc1e 1885     }
fc948c 1886 }