James Moger
2012-10-22 eba89539a29deba954035056437279088c3e047b
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;
1e1b85 25 import java.net.URI;
JM 26 import java.net.URISyntaxException;
166e6a 27 import java.text.MessageFormat;
6c6e7d 28 import java.text.SimpleDateFormat;
fc948c 29 import java.util.ArrayList;
8f73a7 30 import java.util.Arrays;
0b9119 31 import java.util.Collection;
00afd7 32 import java.util.Collections;
6c6e7d 33 import java.util.Date;
8c9a20 34 import java.util.HashMap;
eb1405 35 import java.util.HashSet;
7c1cdc 36 import java.util.LinkedHashMap;
d7905a 37 import java.util.LinkedHashSet;
fc948c 38 import java.util.List;
8c9a20 39 import java.util.Map;
JM 40 import java.util.Map.Entry;
6cc1d4 41 import java.util.Set;
6c6e7d 42 import java.util.TimeZone;
13a3f5 43 import java.util.TreeMap;
d7905a 44 import java.util.TreeSet;
831469 45 import java.util.concurrent.ConcurrentHashMap;
JM 46 import java.util.concurrent.Executors;
47 import java.util.concurrent.ScheduledExecutorService;
48 import java.util.concurrent.TimeUnit;
dd6f08 49 import java.util.concurrent.atomic.AtomicInteger;
fee060 50 import java.util.concurrent.atomic.AtomicReference;
fc948c 51
831469 52 import javax.mail.Message;
JM 53 import javax.mail.MessagingException;
b75734 54 import javax.servlet.ServletContext;
87cc1e 55 import javax.servlet.ServletContextEvent;
JM 56 import javax.servlet.ServletContextListener;
85c2e6 57 import javax.servlet.http.Cookie;
85f639 58 import javax.servlet.http.HttpServletRequest;
fc948c 59
85c2e6 60 import org.apache.wicket.protocol.http.WebResponse;
85f639 61 import org.apache.wicket.resource.ContextRelativeResource;
LM 62 import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
fc948c 63 import org.eclipse.jgit.lib.Repository;
2ef0a3 64 import org.eclipse.jgit.lib.RepositoryCache;
5c2841 65 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
f97bf0 66 import org.eclipse.jgit.lib.StoredConfig;
cdf77b 67 import org.eclipse.jgit.storage.file.FileBasedConfig;
478678 68 import org.eclipse.jgit.storage.file.WindowCache;
JM 69 import org.eclipse.jgit.storage.file.WindowCacheConfig;
5c2841 70 import org.eclipse.jgit.util.FS;
8a2e9c 71 import org.eclipse.jgit.util.FileUtils;
fc948c 72 import org.slf4j.Logger;
JM 73 import org.slf4j.LoggerFactory;
74
20714a 75 import com.gitblit.Constants.AccessPermission;
dfb889 76 import com.gitblit.Constants.AccessRestrictionType;
6adf56 77 import com.gitblit.Constants.AuthorizationControl;
831469 78 import com.gitblit.Constants.FederationRequest;
JM 79 import com.gitblit.Constants.FederationStrategy;
80 import com.gitblit.Constants.FederationToken;
822dfe 81 import com.gitblit.Constants.RegistrantType;
831469 82 import com.gitblit.models.FederationModel;
JM 83 import com.gitblit.models.FederationProposal;
31abc2 84 import com.gitblit.models.FederationSet;
22b181 85 import com.gitblit.models.ForkModel;
4d44cf 86 import com.gitblit.models.Metric;
13a3f5 87 import com.gitblit.models.ProjectModel;
822dfe 88 import com.gitblit.models.RegistrantAccessPermission;
1f9dae 89 import com.gitblit.models.RepositoryModel;
d896e6 90 import com.gitblit.models.SearchResult;
d03aff 91 import com.gitblit.models.ServerSettings;
b75734 92 import com.gitblit.models.ServerStatus;
JM 93 import com.gitblit.models.SettingModel;
fe24a0 94 import com.gitblit.models.TeamModel;
1f9dae 95 import com.gitblit.models.UserModel;
0db5c4 96 import com.gitblit.utils.ArrayUtils;
72633d 97 import com.gitblit.utils.ByteFormat;
165254 98 import com.gitblit.utils.ContainerUtils;
cdf77b 99 import com.gitblit.utils.DeepCopier;
f6740d 100 import com.gitblit.utils.FederationUtils;
fc948c 101 import com.gitblit.utils.JGitUtils;
93f0b1 102 import com.gitblit.utils.JsonUtils;
4d44cf 103 import com.gitblit.utils.MetricUtils;
9275bd 104 import com.gitblit.utils.ObjectCache;
00afd7 105 import com.gitblit.utils.StringUtils;
85f639 106 import com.gitblit.wicket.WicketUtils;
fc948c 107
892570 108 /**
JM 109  * GitBlit is the servlet context listener singleton that acts as the core for
110  * the web ui and the servlets. This class is either directly instantiated by
111  * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the
112  * definition in the web.xml file (Gitblit WAR).
113  * 
114  * This class is the central logic processor for Gitblit. All settings, user
115  * object, and repository object operations pass through this class.
116  * 
117  * Repository Resolution. There are two pathways for finding repositories. One
118  * pathway, for web ui display and repository authentication & authorization, is
119  * within this class. The other pathway is through the standard GitServlet.
120  * 
121  * @author James Moger
122  * 
123  */
87cc1e 124 public class GitBlit implements ServletContextListener {
fc948c 125
5450d0 126     private static GitBlit gitblit;
6c6e7d 127     
fc948c 128     private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
JM 129
831469 130     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
JM 131
132     private final List<FederationModel> federationRegistrations = Collections
133             .synchronizedList(new ArrayList<FederationModel>());
134
135     private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
4d44cf 136
JM 137     private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
138
139     private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
fee060 140     
cdf77b 141     private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>();
fee060 142     
13a3f5 143     private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>();
JM 144     
fee060 145     private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
fc948c 146
b75734 147     private ServletContext servletContext;
JM 148
166e6a 149     private File repositoriesFolder;
fc948c 150
85c2e6 151     private IUserService userService;
fc948c 152
892570 153     private IStoredSettings settings;
b75734 154
84c1d5 155     private ServerSettings settingsModel;
b75734 156
JM 157     private ServerStatus serverStatus;
831469 158
JM 159     private MailExecutor mailExecutor;
6c6e7d 160     
e31da0 161     private LuceneExecutor luceneExecutor;
JM 162     
6c6e7d 163     private TimeZone timezone;
13a3f5 164     
JM 165     private FileBasedConfig projectConfigs;
87cc1e 166
5450d0 167     public GitBlit() {
JM 168         if (gitblit == null) {
892570 169             // set the static singleton reference
5450d0 170             gitblit = this;
JM 171         }
85f639 172     }
LM 173
174     public GitBlit(final IUserService userService) {
175         this.userService = userService;
176         gitblit = this;
87cc1e 177     }
JM 178
892570 179     /**
JM 180      * Returns the Gitblit singleton.
181      * 
182      * @return gitblit singleton
183      */
2a7306 184     public static GitBlit self() {
5450d0 185         if (gitblit == null) {
892570 186             new GitBlit();
5450d0 187         }
JM 188         return gitblit;
831469 189     }
JM 190
191     /**
192      * Determine if this is the GO variant of Gitblit.
193      * 
194      * @return true if this is the GO variant of Gitblit.
195      */
196     public static boolean isGO() {
197         return self().settings instanceof FileSettings;
6c6e7d 198     }
JM 199     
200     /**
201      * Returns the preferred timezone for the Gitblit instance.
202      * 
203      * @return a timezone
204      */
205     public static TimeZone getTimezone() {
206         if (self().timezone == null) {
207             String tzid = getString("web.timezone", null);
208             if (StringUtils.isEmpty(tzid)) {
209                 self().timezone = TimeZone.getDefault();
210                 return self().timezone;
211             }
212             self().timezone = TimeZone.getTimeZone(tzid);
213         }
214         return self().timezone;
2a7306 215     }
3d0494 216     
ae9e15 217     /**
JM 218      * Returns the user-defined blob encodings.
219      * 
220      * @return an array of encodings, may be empty
221      */
222     public static String [] getEncodings() {
223         return getStrings(Keys.web.blobEncodings).toArray(new String[0]);
224     }
225     
2a7306 226
892570 227     /**
JM 228      * Returns the boolean value for the specified key. If the key does not
229      * exist or the value for the key can not be interpreted as a boolean, the
230      * defaultValue is returned.
231      * 
232      * @see IStoredSettings.getBoolean(String, boolean)
233      * @param key
234      * @param defaultValue
235      * @return key value or defaultValue
236      */
2a7306 237     public static boolean getBoolean(String key, boolean defaultValue) {
892570 238         return self().settings.getBoolean(key, defaultValue);
2a7306 239     }
JM 240
892570 241     /**
JM 242      * Returns the integer value for the specified key. If the key does not
243      * exist or the value for the key can not be interpreted as an integer, the
244      * defaultValue is returned.
245      * 
246      * @see IStoredSettings.getInteger(String key, int defaultValue)
247      * @param key
248      * @param defaultValue
249      * @return key value or defaultValue
250      */
2a7306 251     public static int getInteger(String key, int defaultValue) {
892570 252         return self().settings.getInteger(key, defaultValue);
2a7306 253     }
JM 254
892570 255     /**
7e5ee5 256      * Returns the char value for the specified key. If the key does not exist
JM 257      * or the value for the key can not be interpreted as a character, the
258      * defaultValue is returned.
259      * 
260      * @see IStoredSettings.getChar(String key, char defaultValue)
261      * @param key
262      * @param defaultValue
263      * @return key value or defaultValue
264      */
265     public static char getChar(String key, char defaultValue) {
266         return self().settings.getChar(key, defaultValue);
267     }
dd6f08 268
7e5ee5 269     /**
892570 270      * Returns the string value for the specified key. If the key does not exist
JM 271      * or the value for the key can not be interpreted as a string, the
272      * defaultValue is returned.
273      * 
274      * @see IStoredSettings.getString(String key, String defaultValue)
275      * @param key
276      * @param defaultValue
277      * @return key value or defaultValue
278      */
2a7306 279     public static String getString(String key, String defaultValue) {
892570 280         return self().settings.getString(key, defaultValue);
2a7306 281     }
JM 282
892570 283     /**
JM 284      * Returns a list of space-separated strings from the specified key.
285      * 
286      * @see IStoredSettings.getStrings(String key)
287      * @param name
288      * @return list of strings
289      */
2a7306 290     public static List<String> getStrings(String key) {
892570 291         return self().settings.getStrings(key);
7c1cdc 292     }
JM 293
294     /**
295      * Returns a map of space-separated key-value pairs from the specified key.
296      * 
297      * @see IStoredSettings.getStrings(String key)
298      * @param name
299      * @return map of string, string
300      */
301     public static Map<String, String> getMap(String key) {
302         return self().settings.getMap(key);
2a7306 303     }
892570 304
JM 305     /**
306      * Returns the list of keys whose name starts with the specified prefix. If
307      * the prefix is null or empty, all key names are returned.
308      * 
309      * @see IStoredSettings.getAllKeys(String key)
310      * @param startingWith
311      * @return list of keys
312      */
2a7306 313
JM 314     public static List<String> getAllKeys(String startingWith) {
892570 315         return self().settings.getAllKeys(startingWith);
fc948c 316     }
155bf7 317
892570 318     /**
JM 319      * Is Gitblit running in debug mode?
320      * 
321      * @return true if Gitblit is running in debug mode
322      */
8c9a20 323     public static boolean isDebugMode() {
892570 324         return self().settings.getBoolean(Keys.web.debugMode, false);
d03aff 325     }
JM 326
327     /**
b774de 328      * Returns the file object for the specified configuration key.
JM 329      * 
330      * @return the file
331      */
332     public static File getFileOrFolder(String key, String defaultFileOrFolder) {
333         String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
334         return getFileOrFolder(fileOrFolder);
335     }
336
337     /**
338      * Returns the file object which may have it's base-path determined by
339      * environment variables for running on a cloud hosting service. All Gitblit
340      * file or folder retrievals are (at least initially) funneled through this
341      * method so it is the correct point to globally override/alter filesystem
342      * access based on environment or some other indicator.
343      * 
344      * @return the file
345      */
346     public static File getFileOrFolder(String fileOrFolder) {
347         String openShift = System.getenv("OPENSHIFT_DATA_DIR");
348         if (!StringUtils.isEmpty(openShift)) {
349             // running on RedHat OpenShift
350             return new File(openShift, fileOrFolder);
351         }
352         return new File(fileOrFolder);
353     }
354
355     /**
356      * Returns the path of the repositories folder. This method checks to see if
357      * Gitblit is running on a cloud service and may return an adjusted path.
358      * 
359      * @return the repositories folder path
360      */
361     public static File getRepositoriesFolder() {
362         return getFileOrFolder(Keys.git.repositoriesFolder, "git");
363     }
364
365     /**
366      * Returns the path of the proposals folder. This method checks to see if
367      * Gitblit is running on a cloud service and may return an adjusted path.
368      * 
369      * @return the proposals folder path
370      */
371     public static File getProposalsFolder() {
372         return getFileOrFolder(Keys.federation.proposalsFolder, "proposals");
6cc1d4 373     }
JM 374
375     /**
376      * Returns the path of the Groovy folder. This method checks to see if
377      * Gitblit is running on a cloud service and may return an adjusted path.
378      * 
379      * @return the Groovy scripts folder path
380      */
381     public static File getGroovyScriptsFolder() {
382         return getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
b774de 383     }
JM 384
385     /**
d03aff 386      * Updates the list of server settings.
JM 387      * 
388      * @param settings
389      * @return true if the update succeeded
390      */
97a20e 391     public boolean updateSettings(Map<String, String> updatedSettings) {
486ee1 392         return settings.saveSettings(updatedSettings);
b75734 393     }
JM 394
395     public ServerStatus getStatus() {
396         // update heap memory status
397         serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
398         serverStatus.heapFree = Runtime.getRuntime().freeMemory();
399         return serverStatus;
87cc1e 400     }
JM 401
892570 402     /**
JM 403      * Returns the list of non-Gitblit clone urls. This allows Gitblit to
404      * advertise alternative urls for Git client repository access.
405      * 
406      * @param repositoryName
407      * @return list of non-gitblit clone urls
408      */
8a2e9c 409     public List<String> getOtherCloneUrls(String repositoryName) {
JM 410         List<String> cloneUrls = new ArrayList<String>();
892570 411         for (String url : settings.getStrings(Keys.web.otherUrls)) {
8a2e9c 412             cloneUrls.add(MessageFormat.format(url, repositoryName));
JM 413         }
414         return cloneUrls;
fc948c 415     }
JM 416
892570 417     /**
JM 418      * Set the user service. The user service authenticates all users and is
419      * responsible for managing user permissions.
420      * 
421      * @param userService
422      */
85c2e6 423     public void setUserService(IUserService userService) {
JM 424         logger.info("Setting up user service " + userService.toString());
425         this.userService = userService;
63ee41 426         this.userService.setup(settings);
fc948c 427     }
6cca86 428     
JM 429     /**
430      * 
431      * @return true if the user service supports credential changes
432      */
433     public boolean supportsCredentialChanges() {
434         return userService.supportsCredentialChanges();
435     }
436
437     /**
438      * 
0aa8cf 439      * @return true if the user service supports display name changes
JM 440      */
441     public boolean supportsDisplayNameChanges() {
442         return userService.supportsDisplayNameChanges();
443     }
444
445     /**
446      * 
447      * @return true if the user service supports email address changes
448      */
449     public boolean supportsEmailAddressChanges() {
450         return userService.supportsEmailAddressChanges();
451     }
452
453     /**
454      * 
6cca86 455      * @return true if the user service supports team membership changes
JM 456      */
457     public boolean supportsTeamMembershipChanges() {
458         return userService.supportsTeamMembershipChanges();
459     }
fc948c 460
892570 461     /**
JM 462      * Authenticate a user based on a username and password.
463      * 
464      * @see IUserService.authenticate(String, char[])
465      * @param username
466      * @param password
467      * @return a user object or null
468      */
511554 469     public UserModel authenticate(String username, char[] password) {
831469 470         if (StringUtils.isEmpty(username)) {
JM 471             // can not authenticate empty username
472             return null;
473         }
474         String pw = new String(password);
475         if (StringUtils.isEmpty(pw)) {
476             // can not authenticate empty password
477             return null;
478         }
479
480         // check to see if this is the federation user
481         if (canFederate()) {
482             if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
483                 List<String> tokens = getFederationTokens();
484                 if (tokens.contains(pw)) {
485                     // the federation user is an administrator
486                     UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
487                     federationUser.canAdmin = true;
488                     return federationUser;
489                 }
490             }
491         }
492
493         // delegate authentication to the user service
85c2e6 494         if (userService == null) {
fc948c 495             return null;
JM 496         }
85c2e6 497         return userService.authenticate(username, password);
JM 498     }
499
892570 500     /**
JM 501      * Authenticate a user based on their cookie.
502      * 
503      * @param cookies
504      * @return a user object or null
505      */
85c2e6 506     public UserModel authenticate(Cookie[] cookies) {
JM 507         if (userService == null) {
508             return null;
509         }
510         if (userService.supportsCookies()) {
511             if (cookies != null && cookies.length > 0) {
512                 for (Cookie cookie : cookies) {
513                     if (cookie.getName().equals(Constants.NAME)) {
514                         String value = cookie.getValue();
515                         return userService.authenticate(value.toCharArray());
516                     }
517                 }
518             }
519         }
520         return null;
85f639 521     }
LM 522
523     /**
524      * Authenticate a user based on HTTP request paramters.
525      * This method is inteded to be used as fallback when other
526      * means of authentication are failing (username / password or cookies).
527      * @param httpRequest
528      * @return a user object or null
529      */
530     public UserModel authenticate(HttpServletRequest httpRequest) {
531         return null;
532     }
533
534     /**
535      * Open a file resource using the Servlet container.
536      * @param file to open
537      * @return InputStream of the opened file
538      * @throws ResourceStreamNotFoundException
539      */
540     public InputStream getResourceAsStream(String file) throws ResourceStreamNotFoundException {
541         ContextRelativeResource res = WicketUtils.getResource(file);
542         return res.getResourceStream().getInputStream();
85c2e6 543     }
JM 544
892570 545     /**
JM 546      * Sets a cookie for the specified user.
547      * 
548      * @param response
549      * @param user
550      */
85c2e6 551     public void setCookie(WebResponse response, UserModel user) {
JM 552         if (userService == null) {
553             return;
554         }
555         if (userService.supportsCookies()) {
556             Cookie userCookie;
557             if (user == null) {
558                 // clear cookie for logout
559                 userCookie = new Cookie(Constants.NAME, "");
560             } else {
561                 // set cookie for login
62aeb9 562                 String cookie = userService.getCookie(user);
JM 563                 if (StringUtils.isEmpty(cookie)) {
564                     // create empty cookie
565                     userCookie = new Cookie(Constants.NAME, "");
566                 } else {
567                     // create real cookie
568                     userCookie = new Cookie(Constants.NAME, cookie);
569                     userCookie.setMaxAge(Integer.MAX_VALUE);
570                 }
85c2e6 571             }
JM 572             userCookie.setPath("/");
573             response.addCookie(userCookie);
574         }
fc948c 575     }
ea094a 576     
JM 577     /**
578      * Logout a user.
579      * 
580      * @param user
581      */
582     public void logout(UserModel user) {
583         if (userService == null) {
584             return;
585         }
586         userService.logout(user);
587     }
fc948c 588
892570 589     /**
JM 590      * Returns the list of all users available to the login service.
591      * 
592      * @see IUserService.getAllUsernames()
593      * @return list of all usernames
594      */
f98825 595     public List<String> getAllUsernames() {
85c2e6 596         List<String> names = new ArrayList<String>(userService.getAllUsernames());
00afd7 597         return names;
abeaaf 598     }
e36d4d 599
abeaaf 600     /**
JM 601      * Returns the list of all users available to the login service.
602      * 
603      * @see IUserService.getAllUsernames()
604      * @return list of all usernames
605      */
606     public List<UserModel> getAllUsers() {
607         List<UserModel> users = userService.getAllUsers();
608         return users;
8a2e9c 609     }
JM 610
892570 611     /**
JM 612      * Delete the user object with the specified username
613      * 
614      * @see IUserService.deleteUser(String)
615      * @param username
616      * @return true if successful
617      */
8a2e9c 618     public boolean deleteUser(String username) {
85c2e6 619         return userService.deleteUser(username);
f98825 620     }
dfb889 621
892570 622     /**
JM 623      * Retrieve the user object for the specified username.
624      * 
625      * @see IUserService.getUserModel(String)
626      * @param username
627      * @return a user object or null
628      */
f98825 629     public UserModel getUserModel(String username) {
85c2e6 630         UserModel user = userService.getUserModel(username);
dfb889 631         return user;
f98825 632     }
8a2e9c 633
892570 634     /**
97a715 635      * Returns the list of users and their access permissions for the specified repository.
JM 636      * 
637      * @param repository
638      * @return a list of User-AccessPermission tuples
639      */
822dfe 640     public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
JM 641         List<RegistrantAccessPermission> permissions = new ArrayList<RegistrantAccessPermission>();
97a715 642         for (String user : userService.getUsernamesForRepositoryRole(repository.name)) {
87f6c3 643             UserModel model = userService.getUserModel(user);
JM 644             AccessPermission ap = model.getRepositoryPermission(repository);
645             boolean isExplicit = model.hasExplicitRepositoryPermission(repository.name);
646             permissions.add(new RegistrantAccessPermission(user, ap, isExplicit, RegistrantType.USER));
97a715 647         }
JM 648         return permissions;
649     }
650     
651     /**
652      * Sets the access permissions to the specified repository for the specified users.
653      * 
654      * @param repository
655      * @param permissions
656      * @return true if the user models have been updated
657      */
822dfe 658     public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
97a715 659         List<UserModel> users = new ArrayList<UserModel>();
822dfe 660         for (RegistrantAccessPermission up : permissions) {
87f6c3 661             if (up.isExplicit) {
JM 662                 // only set explicitly defined permissions
663                 UserModel user = userService.getUserModel(up.registrant);
664                 user.setRepositoryPermission(repository.name, up.permission);
665                 users.add(user);
666             }
97a715 667         }
JM 668         return userService.updateUserModels(users);
669     }
670     
671     /**
672      * Returns the list of all users who have an explicit access permission
673      * for the specified repository.
892570 674      * 
JM 675      * @see IUserService.getUsernamesForRepositoryRole(String)
676      * @param repository
97a715 677      * @return list of all usernames that have an access permission for the repository
892570 678      */
f98825 679     public List<String> getRepositoryUsers(RepositoryModel repository) {
892570 680         return userService.getUsernamesForRepositoryRole(repository.name);
f98825 681     }
8a2e9c 682
892570 683     /**
JM 684      * Sets the list of all uses who are allowed to bypass the access
685      * restriction placed on the specified repository.
686      * 
687      * @see IUserService.setUsernamesForRepositoryRole(String, List<String>)
688      * @param repository
689      * @param usernames
690      * @return true if successful
691      */
20714a 692     @Deprecated
f98825 693     public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
822dfe 694         // rejects all changes since 1.2.0 because this would elevate
JM 695         // all discrete access permissions to RW+
696         return false;
dfb889 697     }
JM 698
892570 699     /**
JM 700      * Adds/updates a complete user object keyed by username. This method allows
701      * for renaming a user.
702      * 
703      * @see IUserService.updateUserModel(String, UserModel)
704      * @param username
705      * @param user
706      * @param isCreate
707      * @throws GitBlitException
708      */
709     public void updateUserModel(String username, UserModel user, boolean isCreate)
2a7306 710             throws GitBlitException {
16038c 711         if (!username.equalsIgnoreCase(user.username)) {
JM 712             if (userService.getUserModel(user.username) != null) {
d03aff 713                 throw new GitBlitException(MessageFormat.format(
JM 714                         "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
715                         user.username));
16038c 716             }
17363c 717             
JM 718             // rename repositories and owner fields for all repositories
719             for (RepositoryModel model : getRepositoryModels(user)) {
720                 if (model.isUsersPersonalRepository(username)) {
721                     // personal repository
722                     model.owner = user.username;
723                     String oldRepositoryName = model.name;
724                     model.name = "~" + user.username + model.name.substring(model.projectPath.length());
725                     model.projectPath = "~" + user.username;
726                     updateRepositoryModel(oldRepositoryName, model, false);
727                 } else if (model.isOwner(username)) {
728                     // common/shared repo
729                     model.owner = user.username;
730                     updateRepositoryModel(model.name, model, false);
731                 }
732             }
16038c 733         }
85c2e6 734         if (!userService.updateUserModel(username, user)) {
dfb889 735             throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
JM 736         }
fe24a0 737     }
JM 738
739     /**
740      * Returns the list of available teams that a user or repository may be
741      * assigned to.
742      * 
743      * @return the list of teams
744      */
745     public List<String> getAllTeamnames() {
746         List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
abeaaf 747         return teams;
JM 748     }
e36d4d 749
abeaaf 750     /**
JM 751      * Returns the list of available teams that a user or repository may be
752      * assigned to.
753      * 
754      * @return the list of teams
755      */
756     public List<TeamModel> getAllTeams() {
757         List<TeamModel> teams = userService.getAllTeams();
fe24a0 758         return teams;
JM 759     }
760
761     /**
762      * Returns the TeamModel object for the specified name.
763      * 
764      * @param teamname
765      * @return a TeamModel object or null
766      */
767     public TeamModel getTeamModel(String teamname) {
768         return userService.getTeamModel(teamname);
769     }
97a715 770     
JM 771     /**
772      * Returns the list of teams and their access permissions for the specified repository.
773      * 
774      * @param repository
775      * @return a list of Team-AccessPermission tuples
776      */
822dfe 777     public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
JM 778         List<RegistrantAccessPermission> permissions = new ArrayList<RegistrantAccessPermission>();
97a715 779         for (String team : userService.getTeamnamesForRepositoryRole(repository.name)) {
87f6c3 780             TeamModel model = userService.getTeamModel(team);
JM 781             AccessPermission ap = model.getRepositoryPermission(repository);
782             boolean isExplicit = model.hasExplicitRepositoryPermission(repository.name);
783             permissions.add(new RegistrantAccessPermission(team, ap, isExplicit, RegistrantType.TEAM));
97a715 784         }
JM 785         return permissions;
786     }
787     
788     /**
789      * Sets the access permissions to the specified repository for the specified teams.
790      * 
791      * @param repository
792      * @param permissions
793      * @return true if the team models have been updated
794      */
822dfe 795     public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
97a715 796         List<TeamModel> teams = new ArrayList<TeamModel>();
822dfe 797         for (RegistrantAccessPermission tp : permissions) {
87f6c3 798             if (tp.isExplicit) {
JM 799                 // only set explicitly defined access permissions
800                 TeamModel team = userService.getTeamModel(tp.registrant);
801                 team.setRepositoryPermission(repository.name, tp.permission);
802                 teams.add(team);
803             }
97a715 804         }
JM 805         return userService.updateTeamModels(teams);
806     }
807     
fe24a0 808     /**
822dfe 809      * Returns the list of all teams who have an explicit access permission for
JM 810      * the specified repository.
fe24a0 811      * 
JM 812      * @see IUserService.getTeamnamesForRepositoryRole(String)
813      * @param repository
822dfe 814      * @return list of all teamnames with explicit access permissions to the repository
fe24a0 815      */
JM 816     public List<String> getRepositoryTeams(RepositoryModel repository) {
817         return userService.getTeamnamesForRepositoryRole(repository.name);
818     }
819
820     /**
821      * Sets the list of all uses who are allowed to bypass the access
822      * restriction placed on the specified repository.
823      * 
824      * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
825      * @param repository
826      * @param teamnames
827      * @return true if successful
828      */
20714a 829     @Deprecated
fe24a0 830     public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
822dfe 831         // rejects all changes since 1.2.0 because this would elevate
JM 832         // all discrete access permissions to RW+
833         return false;
fe24a0 834     }
fa54be 835
fe24a0 836     /**
JM 837      * Updates the TeamModel object for the specified name.
838      * 
839      * @param teamname
840      * @param team
841      * @param isCreate
842      */
fa54be 843     public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
JM 844             throws GitBlitException {
fe24a0 845         if (!teamname.equalsIgnoreCase(team.name)) {
JM 846             if (userService.getTeamModel(team.name) != null) {
847                 throw new GitBlitException(MessageFormat.format(
848                         "Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
849                         team.name));
850             }
851         }
852         if (!userService.updateTeamModel(teamname, team)) {
853             throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
854         }
855     }
fa54be 856
fe24a0 857     /**
JM 858      * Delete the team object with the specified teamname
859      * 
860      * @see IUserService.deleteTeam(String)
861      * @param teamname
862      * @return true if successful
863      */
864     public boolean deleteTeam(String teamname) {
865         return userService.deleteTeam(teamname);
dfb889 866     }
fee060 867     
JM 868     /**
869      * Adds the repository to the list of cached repositories if Gitblit is
870      * configured to cache the repository list.
871      * 
9f5a86 872      * @param model
fee060 873      */
9f5a86 874     private void addToCachedRepositoryList(RepositoryModel model) {
fee060 875         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
9f5a86 876             repositoryListCache.put(model.name, model);
1e1b85 877             
JM 878             // update the fork origin repository with this repository clone
879             if (!StringUtils.isEmpty(model.originRepository)) {
880                 if (repositoryListCache.containsKey(model.originRepository)) {
881                     RepositoryModel origin = repositoryListCache.get(model.originRepository);
9f5a86 882                     origin.addFork(model.name);
1e1b85 883                 }
JM 884             }
fee060 885         }
cdf77b 886     }
JM 887     
888     /**
889      * Removes the repository from the list of cached repositories.
890      * 
891      * @param name
1e1b85 892      * @return the model being removed
cdf77b 893      */
1e1b85 894     private RepositoryModel removeFromCachedRepositoryList(String name) {
cdf77b 895         if (StringUtils.isEmpty(name)) {
1e1b85 896             return null;
cdf77b 897         }
1e1b85 898         return repositoryListCache.remove(name);
fee060 899     }
dfb889 900
892570 901     /**
cdf77b 902      * Clears all the cached metadata for the specified repository.
4d44cf 903      * 
JM 904      * @param repositoryName
905      */
cdf77b 906     private void clearRepositoryMetadataCache(String repositoryName) {
4d44cf 907         repositorySizeCache.remove(repositoryName);
JM 908         repositoryMetricsCache.remove(repositoryName);
fee060 909     }
JM 910     
911     /**
912      * Resets the repository list cache.
913      * 
914      */
915     public void resetRepositoryListCache() {
916         logger.info("Repository cache manually reset");
917         repositoryListCache.clear();
918     }
919     
920     /**
921      * Calculate the checksum of settings that affect the repository list cache.
922      * @return a checksum
923      */
924     private String getRepositoryListSettingsChecksum() {
925         StringBuilder ns = new StringBuilder();
926         ns.append(settings.getString(Keys.git.cacheRepositoryList, "")).append('\n');
927         ns.append(settings.getString(Keys.git.onlyAccessBareRepositories, "")).append('\n');
928         ns.append(settings.getString(Keys.git.searchRepositoriesSubfolders, "")).append('\n');
929         ns.append(settings.getString(Keys.git.searchRecursionDepth, "")).append('\n');
930         ns.append(settings.getString(Keys.git.searchExclusions, "")).append('\n');
931         String checksum = StringUtils.getSHA1(ns.toString());
932         return checksum;
933     }
934     
935     /**
936      * Compare the last repository list setting checksum to the current checksum.
937      * If different then clear the cache so that it may be rebuilt.
938      * 
939      * @return true if the cached repository list is valid since the last check
940      */
941     private boolean isValidRepositoryList() {
942         String newChecksum = getRepositoryListSettingsChecksum();
943         boolean valid = newChecksum.equals(repositoryListSettingsChecksum.get());
944         repositoryListSettingsChecksum.set(newChecksum);
945         if (!valid && settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
946             logger.info("Repository list settings have changed. Clearing repository list cache.");
947             repositoryListCache.clear();
948         }
949         return valid;
4d44cf 950     }
JM 951
952     /**
892570 953      * Returns the list of all repositories available to Gitblit. This method
JM 954      * does not consider user access permissions.
955      * 
956      * @return list of all repositories
957      */
166e6a 958     public List<String> getRepositoryList() {
fee060 959         if (repositoryListCache.size() == 0 || !isValidRepositoryList()) {
JM 960             // we are not caching OR we have not yet cached OR the cached list is invalid
961             long startTime = System.currentTimeMillis();
962             List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder, 
963                     settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
964                     settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true),
965                     settings.getInteger(Keys.git.searchRecursionDepth, -1),
966                     settings.getStrings(Keys.git.searchExclusions));
967
968             if (!settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
969                 // we are not caching
970                 StringUtils.sortRepositorynames(repositories);
971                 return repositories;
972             } else {
973                 // we are caching this list
974                 String msg = "{0} repositories identified in {1} msecs";
975
976                 // optionally (re)calculate repository sizes
977                 if (getBoolean(Keys.web.showRepositorySizes, true)) {
978                     msg = "{0} repositories identified with calculated folder sizes in {1} msecs";
979                     for (String repository : repositories) {
980                         RepositoryModel model = getRepositoryModel(repository);
981                         if (!model.skipSizeCalculation) {
982                             calculateSize(model);
983                         }
984                     }
cdf77b 985                 } else {
JM 986                     // update cache
987                     for (String repository : repositories) {
988                         getRepositoryModel(repository);
989                     }
fee060 990                 }
JM 991                 
992                 long duration = System.currentTimeMillis() - startTime;
993                 logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration));
994             }
995         }
996         
997         // return sorted copy of cached list
cdf77b 998         List<String> list = new ArrayList<String>(repositoryListCache.keySet());        
fee060 999         StringUtils.sortRepositorynames(list);
JM 1000         return list;
166e6a 1001     }
155bf7 1002
892570 1003     /**
JM 1004      * Returns the JGit repository for the specified name.
1005      * 
1006      * @param repositoryName
1007      * @return repository or null
1008      */
166e6a 1009     public Repository getRepository(String repositoryName) {
11924d 1010         return getRepository(repositoryName, true);
JM 1011     }
1012
1013     /**
1014      * Returns the JGit repository for the specified name.
1015      * 
1016      * @param repositoryName
1017      * @param logError
1018      * @return repository or null
1019      */
1020     public Repository getRepository(String repositoryName, boolean logError) {
2ef0a3 1021         File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
JM 1022         if (dir == null)
1023             return null;
1024
166e6a 1025         Repository r = null;
JM 1026         try {
2ef0a3 1027             FileKey key = FileKey.exact(dir, FS.DETECTED);
JM 1028             r = RepositoryCache.open(key, true);
1029         } catch (IOException e) {
11924d 1030             if (logError) {
JM 1031                 logger.error("GitBlit.getRepository(String) failed to find "
1032                         + new File(repositoriesFolder, repositoryName).getAbsolutePath());
5d9bd7 1033             }
166e6a 1034         }
JM 1035         return r;
1036     }
dfb889 1037
892570 1038     /**
JM 1039      * Returns the list of repository models that are accessible to the user.
1040      * 
1041      * @param user
1042      * @return list of repository models accessible to user
1043      */
511554 1044     public List<RepositoryModel> getRepositoryModels(UserModel user) {
fee060 1045         long methodStart = System.currentTimeMillis();
166e6a 1046         List<String> list = getRepositoryList();
JM 1047         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
1048         for (String repo : list) {
dfb889 1049             RepositoryModel model = getRepositoryModel(user, repo);
JM 1050             if (model != null) {
1051                 repositories.add(model);
1052             }
166e6a 1053         }
3d293a 1054         if (getBoolean(Keys.web.showRepositorySizes, true)) {
JM 1055             int repoCount = 0;
1056             long startTime = System.currentTimeMillis();
1057             ByteFormat byteFormat = new ByteFormat();
1058             for (RepositoryModel model : repositories) {
1059                 if (!model.skipSizeCalculation) {
1060                     repoCount++;
1061                     model.size = byteFormat.format(calculateSize(model));
1062                 }
1063             }
1064             long duration = System.currentTimeMillis() - startTime;
fee060 1065             if (duration > 250) {
JM 1066                 // only log calcualtion time if > 250 msecs
1067                 logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
3d293a 1068                     repoCount, duration));
fee060 1069             }
3d293a 1070         }
fee060 1071         long duration = System.currentTimeMillis() - methodStart;
JM 1072         logger.info(MessageFormat.format("{0} repository models loaded for {1} in {2} msecs",
4bcaea 1073                 repositories.size(), user == null ? "anonymous" : user.username, duration));
166e6a 1074         return repositories;
JM 1075     }
8a2e9c 1076
892570 1077     /**
JM 1078      * Returns a repository model if the repository exists and the user may
1079      * access the repository.
1080      * 
1081      * @param user
1082      * @param repositoryName
1083      * @return repository model or null
1084      */
511554 1085     public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
dfb889 1086         RepositoryModel model = getRepositoryModel(repositoryName);
85c2e6 1087         if (model == null) {
JM 1088             return null;
1089         }
20714a 1090         if (user == null) {
JM 1091             user = UserModel.ANONYMOUS;
1092         }
1093         if (user.canView(model)) {
dfb889 1094             return model;
JM 1095         }
20714a 1096         return null;
dfb889 1097     }
JM 1098
892570 1099     /**
JM 1100      * Returns the repository model for the specified repository. This method
1101      * does not consider user access permissions.
1102      * 
1103      * @param repositoryName
1104      * @return repository model or null
1105      */
166e6a 1106     public RepositoryModel getRepositoryModel(String repositoryName) {
cdf77b 1107         if (!repositoryListCache.containsKey(repositoryName)) {
JM 1108             RepositoryModel model = loadRepositoryModel(repositoryName);
1109             if (model == null) {
1110                 return null;
1111             }
9f5a86 1112             addToCachedRepositoryList(model);
cdf77b 1113             return model;
JM 1114         }
1115         
1116         // cached model
1117         RepositoryModel model = repositoryListCache.get(repositoryName);
1118         
1119         // check for updates
1120         Repository r = getRepository(repositoryName);
1121         if (r == null) {
1122             // repository is missing
1123             removeFromCachedRepositoryList(repositoryName);
1124             logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName));
1125             return null;
1126         }
1127         
1128         FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r);
1129         if (config.isOutdated()) {
1130             // reload model
1131             logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
1132             model = loadRepositoryModel(repositoryName);
1133             removeFromCachedRepositoryList(repositoryName);
9f5a86 1134             addToCachedRepositoryList(model);
cdf77b 1135         } else {
JM 1136             // update a few repository parameters 
1137             if (!model.hasCommits) {
1138                 // update hasCommits, assume a repository only gains commits :)
1139                 model.hasCommits = JGitUtils.hasCommits(r);
1140             }
1141
1142             model.lastChange = JGitUtils.getLastChange(r);
1143         }
1144         r.close();
1145         
1146         // return a copy of the cached model
1147         return DeepCopier.copy(model);
13a3f5 1148     }
JM 1149     
1150     
1151     /**
1152      * Returns the map of project config.  This map is cached and reloaded if
1153      * the underlying projects.conf file changes.
1154      * 
1155      * @return project config map
1156      */
1157     private Map<String, ProjectModel> getProjectConfigs() {
1e1b85 1158         if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
13a3f5 1159             
JM 1160             try {
1161                 projectConfigs.load();
1162             } catch (Exception e) {
1163             }
1164
1165             // project configs
1166             String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
1167             ProjectModel rootProject = new ProjectModel(rootName, true);
1168
1169             Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>();
1170             // cache the root project under its alias and an empty path
1171             configs.put("", rootProject);
1172             configs.put(rootProject.name.toLowerCase(), rootProject);
1173
1174             for (String name : projectConfigs.getSubsections("project")) {
1175                 ProjectModel project;
1176                 if (name.equalsIgnoreCase(rootName)) {
1177                     project = rootProject;
1178                 } else {
1179                     project = new ProjectModel(name);
1180                 }
1181                 project.title = projectConfigs.getString("project", name, "title");
1182                 project.description = projectConfigs.getString("project", name, "description");
1183                 // TODO add more interesting metadata
1184                 // project manager?
1185                 // commit message regex?
1186                 // RW+
1187                 // RW
1188                 // R
1189                 configs.put(name.toLowerCase(), project);                
1190             }
1191             projectCache.clear();
1192             projectCache.putAll(configs);
1193         }
1194         return projectCache;
1195     }
1196     
1197     /**
1198      * Returns a list of project models for the user.
1199      * 
1200      * @param user
1e1b85 1201      * @param includeUsers
13a3f5 1202      * @return list of projects that are accessible to the user
JM 1203      */
1e1b85 1204     public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
13a3f5 1205         Map<String, ProjectModel> configs = getProjectConfigs();
JM 1206
1207         // per-user project lists, this accounts for security and visibility
1208         Map<String, ProjectModel> map = new TreeMap<String, ProjectModel>();
1209         // root project
1210         map.put("", configs.get(""));
1211         
1212         for (RepositoryModel model : getRepositoryModels(user)) {
1213             String rootPath = StringUtils.getRootPath(model.name).toLowerCase();            
1214             if (!map.containsKey(rootPath)) {
1215                 ProjectModel project;
1216                 if (configs.containsKey(rootPath)) {
1217                     // clone the project model because it's repository list will
1218                     // be tailored for the requesting user
1219                     project = DeepCopier.copy(configs.get(rootPath));
1220                 } else {
1221                     project = new ProjectModel(rootPath);
1222                 }
1223                 map.put(rootPath, project);
1224             }
1225             map.get(rootPath).addRepository(model);
1226         }
1227         
1228         // sort projects, root project first
1e1b85 1229         List<ProjectModel> projects;
JM 1230         if (includeUsers) {
1231             // all projects
1232             projects = new ArrayList<ProjectModel>(map.values());
1233             Collections.sort(projects);
1234             projects.remove(map.get(""));
1235             projects.add(0, map.get(""));
1236         } else {
1237             // all non-user projects
1238             projects = new ArrayList<ProjectModel>();
1239             ProjectModel root = map.remove("");
1240             for (ProjectModel model : map.values()) {
1241                 if (!model.isUserProject()) {
1242                     projects.add(model);
1243                 }
1244             }
1245             Collections.sort(projects);
1246             projects.add(0, root);
1247         }
13a3f5 1248         return projects;
JM 1249     }
1250     
1251     /**
1252      * Returns the project model for the specified user.
1253      * 
1254      * @param name
1255      * @param user
1256      * @return a project model, or null if it does not exist
1257      */
1258     public ProjectModel getProjectModel(String name, UserModel user) {
1e1b85 1259         for (ProjectModel project : getProjectModels(user, true)) {
13a3f5 1260             if (project.name.equalsIgnoreCase(name)) {
JM 1261                 return project;
1262             }
1263         }
1264         return null;
1265     }
1266     
1267     /**
1268      * Returns a project model for the Gitblit/system user.
1269      * 
1270      * @param name a project name
1271      * @return a project model or null if the project does not exist
1272      */
1273     public ProjectModel getProjectModel(String name) {
1274         Map<String, ProjectModel> configs = getProjectConfigs();
1275         ProjectModel project = configs.get(name.toLowerCase());
1276         if (project == null) {
1e1b85 1277             project = new ProjectModel(name);
JM 1278             if (name.length() > 0 && name.charAt(0) == '~') {
1279                 UserModel user = getUserModel(name.substring(1));
1280                 if (user != null) {
1281                     project.title = user.getDisplayName();
1282                     project.description = "personal repositories";
1283                 }
13a3f5 1284             }
1e1b85 1285         } else {
JM 1286             // clone the object
1287             project = DeepCopier.copy(project);
1288         }
1289         if (StringUtils.isEmpty(name)) {
1290             // get root repositories
1291             for (String repository : getRepositoryList()) {
1292                 if (repository.indexOf('/') == -1) {
1293                     project.addRepository(repository);
1294                 }
1295             }
1296         } else {
1297             // get repositories in subfolder
1298             String folder = name.toLowerCase() + "/";
1299             for (String repository : getRepositoryList()) {
1300                 if (repository.toLowerCase().startsWith(folder)) {
1301                     project.addRepository(repository);
1302                 }
1303             }
1304         }
1305         if (project.repositories.size() == 0) {
1306             // no repositories == no project
1307             return null;
13a3f5 1308         }
JM 1309         return project;
cdf77b 1310     }
JM 1311     
1312     /**
1313      * Workaround JGit.  I need to access the raw config object directly in order
1314      * to see if the config is dirty so that I can reload a repository model.
1315      * If I use the stock JGit method to get the config it already reloads the
1316      * config.  If the config changes are made within Gitblit this is fine as
1317      * the returned config will still be flagged as dirty.  BUT... if the config
1318      * is manipulated outside Gitblit then it fails to recognize this as dirty.
1319      *  
1320      * @param r
1321      * @return a config
1322      */
1323     private StoredConfig getRepositoryConfig(Repository r) {
1324         try {
1325             Field f = r.getClass().getDeclaredField("repoConfig");
1326             f.setAccessible(true);
1327             StoredConfig config = (StoredConfig) f.get(r);
1328             return config;
1329         } catch (Exception e) {
1330             logger.error("Failed to retrieve \"repoConfig\" via reflection", e);
1331         }
1332         return r.getConfig();
1333     }
1334     
1335     /**
1336      * Create a repository model from the configuration and repository data.
1337      * 
1338      * @param repositoryName
1339      * @return a repositoryModel or null if the repository does not exist
1340      */
1341     private RepositoryModel loadRepositoryModel(String repositoryName) {
166e6a 1342         Repository r = getRepository(repositoryName);
1f9dae 1343         if (r == null) {
JM 1344             return null;
1345         }
166e6a 1346         RepositoryModel model = new RepositoryModel();
9f5a86 1347         model.isBare = r.isBare();
JM 1348         File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "git");
1349         if (model.isBare) {
1350             model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
1351         } else {
1352             model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory().getParentFile());
1353         }
bc9d4a 1354         model.hasCommits = JGitUtils.hasCommits(r);
4fea45 1355         model.lastChange = JGitUtils.getLastChange(r);
20714a 1356         model.projectPath = StringUtils.getFirstPathElement(repositoryName);
fee060 1357         
JM 1358         StoredConfig config = r.getConfig();
1e1b85 1359         boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
JM 1360         
166e6a 1361         if (config != null) {
00afd7 1362             model.description = getConfig(config, "description", "");
JM 1363             model.owner = getConfig(config, "owner", "");
1364             model.useTickets = getConfig(config, "useTickets", false);
1365             model.useDocs = getConfig(config, "useDocs", false);
1e1b85 1366             model.allowForks = getConfig(config, "allowForks", true);
2a7306 1367             model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
94dcbd 1368                     "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));
6adf56 1369             model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
JM 1370                     "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
15640f 1371             model.verifyCommitter = getConfig(config, "verifyCommitter", false);
1e1b85 1372             model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
00afd7 1373             model.isFrozen = getConfig(config, "isFrozen", false);
a1ea87 1374             model.showReadme = getConfig(config, "showReadme", false);
3d293a 1375             model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
fe3262 1376             model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
831469 1377             model.federationStrategy = FederationStrategy.fromName(getConfig(config,
JM 1378                     "federationStrategy", null));
8f73a7 1379             model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
380afa 1380                     Constants.CONFIG_GITBLIT, null, "federationSets")));
831469 1381             model.isFederated = getConfig(config, "isFederated", false);
JM 1382             model.origin = config.getString("remote", "origin", "url");
e0af48 1383             if (model.origin != null) {
JM 1384                 model.origin = model.origin.replace('\\', '/');
1385             }
fa54be 1386             model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
380afa 1387                     Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
fa54be 1388             model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
380afa 1389                     Constants.CONFIG_GITBLIT, null, "postReceiveScript")));
eb96ea 1390             model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
380afa 1391                     Constants.CONFIG_GITBLIT, null, "mailingList")));
40ca5c 1392             model.indexedBranches = new ArrayList<String>(Arrays.asList(config.getStringList(
380afa 1393                     Constants.CONFIG_GITBLIT, null, "indexBranch")));
4a5a55 1394             
JC 1395             // Custom defined properties
7c1cdc 1396             model.customFields = new LinkedHashMap<String, String>();
380afa 1397             for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) {
JM 1398                 model.customFields.put(aProperty, config.getString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, aProperty));
4a5a55 1399             }
166e6a 1400         }
90b8d7 1401         model.HEAD = JGitUtils.getHEADRef(r);
JM 1402         model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
166e6a 1403         r.close();
1e1b85 1404         
JM 1405         if (model.origin != null && model.origin.startsWith("file://")) {
1406             // repository was cloned locally... perhaps as a fork
1407             try {
1408                 File folder = new File(new URI(model.origin));
1409                 String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
1410                 if (!StringUtils.isEmpty(originRepo)) {
1411                     // ensure origin still exists
1412                     File repoFolder = new File(getRepositoriesFolder(), originRepo);
1413                     if (repoFolder.exists()) {
1414                         model.originRepository = originRepo;
1415                     }
1416                 }
1417             } catch (URISyntaxException e) {
1418                 logger.error("Failed to determine fork for " + model, e);
1419             }
1420         }
166e6a 1421         return model;
00afd7 1422     }
eb870f 1423     
JM 1424     /**
1425      * Determines if this server has the requested repository.
1426      * 
1427      * @param name
1428      * @return true if the repository exists
1429      */
1430     public boolean hasRepository(String repositoryName) {
cdf77b 1431         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
JM 1432             // if we are caching use the cache to determine availability
1433             // otherwise we end up adding a phantom repository to the cache
1434             return repositoryListCache.containsKey(repositoryName);
1435         }        
eb870f 1436         Repository r = getRepository(repositoryName, false);
JM 1437         if (r == null) {
1438             return false;
1439         }
1440         r.close();
1441         return true;
1442     }
eb1405 1443     
JM 1444     /**
1445      * Determines if the specified user has a fork of the specified origin
1446      * repository.
1447      * 
1448      * @param username
1449      * @param origin
1450      * @return true the if the user has a fork
1451      */
1452     public boolean hasFork(String username, String origin) {
1453         return getFork(username, origin) != null;
1454     }
1455     
1456     /**
1457      * Gets the name of a user's fork of the specified origin
1458      * repository.
1459      * 
1460      * @param username
1461      * @param origin
1462      * @return the name of the user's fork, null otherwise
1463      */
1464     public String getFork(String username, String origin) {
1465         String userProject = "~" + username.toLowerCase();
1466         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
1467             String userPath = userProject + "/";
1468
1469             // collect all origin nodes in fork network
1470             Set<String> roots = new HashSet<String>();
1471             roots.add(origin);
1472             RepositoryModel originModel = repositoryListCache.get(origin);
1473             while (originModel != null) {
1474                 if (!ArrayUtils.isEmpty(originModel.forks)) {
1475                     for (String fork : originModel.forks) {
1476                         if (!fork.startsWith(userPath)) {
1477                             roots.add(fork);
1478                         }
1479                     }
1480                 }
1481                 
1482                 if (originModel.originRepository != null) {
1483                     roots.add(originModel.originRepository);
1484                     originModel = repositoryListCache.get(originModel.originRepository);
1485                 } else {
1486                     // break
1487                     originModel = null;
1488                 }
1489             }
1490             
1491             for (String repository : repositoryListCache.keySet()) {
1492                 if (repository.toLowerCase().startsWith(userPath)) {
1493                     RepositoryModel model = repositoryListCache.get(repository);
1494                     if (!StringUtils.isEmpty(model.originRepository)) {
1495                         if (roots.contains(model.originRepository)) {
1496                             // user has a fork in this graph
1497                             return model.name;
1498                         }
1499                     }
1500                 }
1501             }
1502         } else {
1503             // not caching
1504             ProjectModel project = getProjectModel(userProject);
1505             for (String repository : project.repositories) {
1506                 if (repository.toLowerCase().startsWith(userProject)) {
1507                     RepositoryModel model = repositoryListCache.get(repository);
1508                     if (model.originRepository.equalsIgnoreCase(origin)) {
1509                         // user has a fork
1510                         return model.name;
1511                     }
1512                 }
1513             }
1514         }
1515         // user does not have a fork
1516         return null;
1517     }
22b181 1518     
JM 1519     /**
1520      * Returns the fork network for a repository by traversing up the fork graph
1521      * to discover the root and then down through all children of the root node.
1522      * 
1523      * @param repository
1524      * @return a ForkModel
1525      */
1526     public ForkModel getForkNetwork(String repository) {
1527         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
1528             // find the root
1529             RepositoryModel model = repositoryListCache.get(repository);
1530             while (model.originRepository != null) {
1531                 model = repositoryListCache.get(model.originRepository);
1532             }
1533             ForkModel root = getForkModel(model.name);
1534             return root;
1535         }
1536         return null;
1537     }
1538     
1539     private ForkModel getForkModel(String repository) {
1540         RepositoryModel model = repositoryListCache.get(repository);
e94078 1541         ForkModel fork = new ForkModel(model);
22b181 1542         if (!ArrayUtils.isEmpty(model.forks)) {
JM 1543             for (String aFork : model.forks) {
1544                 ForkModel fm = getForkModel(aFork);
1545                 fork.forks.add(fm);
1546             }
1547         }
1548         return fork;
1549     }
8a2e9c 1550
892570 1551     /**
4d44cf 1552      * Returns the size in bytes of the repository. Gitblit caches the
JM 1553      * repository sizes to reduce the performance penalty of recursive
1554      * calculation. The cache is updated if the repository has been changed
1555      * since the last calculation.
5c2841 1556      * 
JM 1557      * @param model
1558      * @return size in bytes
1559      */
1560     public long calculateSize(RepositoryModel model) {
4d44cf 1561         if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
JM 1562             return repositorySizeCache.getObject(model.name);
1563         }
5c2841 1564         File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
4d44cf 1565         long size = com.gitblit.utils.FileUtils.folderSize(gitDir);
JM 1566         repositorySizeCache.updateObject(model.name, model.lastChange, size);
1567         return size;
5c2841 1568     }
JM 1569
1570     /**
dd6f08 1571      * Ensure that a cached repository is completely closed and its resources
JM 1572      * are properly released.
1573      * 
1574      * @param repositoryName
1575      */
1576     private void closeRepository(String repositoryName) {
1577         Repository repository = getRepository(repositoryName);
20714a 1578         if (repository == null) {
JM 1579             return;
1580         }
2ef0a3 1581         RepositoryCache.close(repository);
JM 1582
dd6f08 1583         // assume 2 uses in case reflection fails
JM 1584         int uses = 2;
1585         try {
1586             // The FileResolver caches repositories which is very useful
1587             // for performance until you want to delete a repository.
1588             // I have to use reflection to call close() the correct
1589             // number of times to ensure that the object and ref databases
1590             // are properly closed before I can delete the repository from
1591             // the filesystem.
1592             Field useCnt = Repository.class.getDeclaredField("useCnt");
1593             useCnt.setAccessible(true);
1594             uses = ((AtomicInteger) useCnt.get(repository)).get();
1595         } catch (Exception e) {
1596             logger.warn(MessageFormat
1597                     .format("Failed to reflectively determine use count for repository {0}",
1598                             repositoryName), e);
1599         }
1600         if (uses > 0) {
1601             logger.info(MessageFormat
1602                     .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
1603                             repositoryName, uses, uses));
1604             for (int i = 0; i < uses; i++) {
1605                 repository.close();
1606             }
1607         }
e6637c 1608         
JM 1609         // close any open index writer/searcher in the Lucene executor
1610         luceneExecutor.close(repositoryName);
dd6f08 1611     }
JM 1612
1613     /**
4d44cf 1614      * Returns the metrics for the default branch of the specified repository.
JM 1615      * This method builds a metrics cache. The cache is updated if the
1616      * repository is updated. A new copy of the metrics list is returned on each
1617      * call so that modifications to the list are non-destructive.
1618      * 
1619      * @param model
1620      * @param repository
1621      * @return a new array list of metrics
1622      */
1623     public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
1624         if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
1625             return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
1626         }
40538c 1627         List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null, getTimezone());
4d44cf 1628         repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
JM 1629         return new ArrayList<Metric>(metrics);
1630     }
1631
1632     /**
1633      * Returns the gitblit string value for the specified key. If key is not
892570 1634      * set, returns defaultValue.
JM 1635      * 
1636      * @param config
1637      * @param field
1638      * @param defaultValue
1639      * @return field value or defaultValue
1640      */
00afd7 1641     private String getConfig(StoredConfig config, String field, String defaultValue) {
380afa 1642         String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
00afd7 1643         if (StringUtils.isEmpty(value)) {
JM 1644             return defaultValue;
1645         }
1646         return value;
1647     }
8a2e9c 1648
892570 1649     /**
380afa 1650      * Returns the gitblit boolean value for the specified key. If key is not
892570 1651      * set, returns defaultValue.
JM 1652      * 
1653      * @param config
1654      * @param field
1655      * @param defaultValue
1656      * @return field value or defaultValue
1657      */
00afd7 1658     private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
380afa 1659         return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue);
166e6a 1660     }
JM 1661
892570 1662     /**
JM 1663      * Creates/updates the repository model keyed by reopsitoryName. Saves all
1664      * repository settings in .git/config. This method allows for renaming
1665      * repositories and will update user access permissions accordingly.
1666      * 
1667      * All repositories created by this method are bare and automatically have
1668      * .git appended to their names, which is the standard convention for bare
1669      * repositories.
1670      * 
1671      * @param repositoryName
1672      * @param repository
1673      * @param isCreate
1674      * @throws GitBlitException
1675      */
1676     public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
2a7306 1677             boolean isCreate) throws GitBlitException {
f5d0ad 1678         Repository r = null;
2c60de 1679         String projectPath = StringUtils.getFirstPathElement(repository.name);
JM 1680         if (!StringUtils.isEmpty(projectPath)) {
1681             if (projectPath.equalsIgnoreCase(getString(Keys.web.repositoryRootGroupName, "main"))) {
1682                 // strip leading group name
1683                 repository.name = repository.name.substring(projectPath.length() + 1);
1684             }
1685         }
f5d0ad 1686         if (isCreate) {
a3bde6 1687             // ensure created repository name ends with .git
168566 1688             if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
a3bde6 1689                 repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
JM 1690             }
166e6a 1691             if (new File(repositoriesFolder, repository.name).exists()) {
2a7306 1692                 throw new GitBlitException(MessageFormat.format(
JM 1693                         "Can not create repository ''{0}'' because it already exists.",
1694                         repository.name));
166e6a 1695             }
dfb889 1696             // create repository
f5d0ad 1697             logger.info("create repository " + repository.name);
168566 1698             r = JGitUtils.createRepository(repositoriesFolder, repository.name);
f5d0ad 1699         } else {
8a2e9c 1700             // rename repository
JM 1701             if (!repositoryName.equalsIgnoreCase(repository.name)) {
16038c 1702                 if (!repository.name.toLowerCase().endsWith(
JM 1703                         org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
1704                     repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
1705                 }
1706                 if (new File(repositoriesFolder, repository.name).exists()) {
d03aff 1707                     throw new GitBlitException(MessageFormat.format(
JM 1708                             "Failed to rename ''{0}'' because ''{1}'' already exists.",
1709                             repositoryName, repository.name));
16038c 1710                 }
dd6f08 1711                 closeRepository(repositoryName);
8a2e9c 1712                 File folder = new File(repositoriesFolder, repositoryName);
JM 1713                 File destFolder = new File(repositoriesFolder, repository.name);
1714                 if (destFolder.exists()) {
2a7306 1715                     throw new GitBlitException(
JM 1716                             MessageFormat
1717                                     .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
1718                                             repositoryName, repository.name));
8a2e9c 1719                 }
c1a4cc 1720                 File parentFile = destFolder.getParentFile();
JM 1721                 if (!parentFile.exists() && !parentFile.mkdirs()) {
1722                     throw new GitBlitException(MessageFormat.format(
1723                             "Failed to create folder ''{0}''", parentFile.getAbsolutePath()));
1724                 }
8a2e9c 1725                 if (!folder.renameTo(destFolder)) {
2a7306 1726                     throw new GitBlitException(MessageFormat.format(
JM 1727                             "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
1728                             repository.name));
8a2e9c 1729                 }
JM 1730                 // rename the roles
85c2e6 1731                 if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
2a7306 1732                     throw new GitBlitException(MessageFormat.format(
JM 1733                             "Failed to rename repository permissions ''{0}'' to ''{1}''.",
1734                             repositoryName, repository.name));
8a2e9c 1735                 }
1e1b85 1736                 
JM 1737                 // rename fork origins in their configs
1738                 if (!ArrayUtils.isEmpty(repository.forks)) {
1739                     for (String fork : repository.forks) {
1740                         Repository rf = getRepository(fork);
1741                         try {
1742                             StoredConfig config = rf.getConfig();
1743                             String origin = config.getString("remote", "origin", "url");
1744                             origin = origin.replace(repositoryName, repository.name);
1745                             config.setString("remote", "origin", "url", origin);
1746                             config.save();
1747                         } catch (Exception e) {
1748                             logger.error("Failed to update repository fork config for " + fork, e);
1749                         }
1750                         rf.close();
1751                     }
1752                 }
dd79f4 1753                 
JM 1754                 // remove this repository from any origin model's fork list
1755                 if (!StringUtils.isEmpty(repository.originRepository)) {
1756                     RepositoryModel origin = repositoryListCache.get(repository.originRepository);
1757                     if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
1758                         origin.forks.remove(repositoryName);
1759                     }
1760                 }
4d44cf 1761
JM 1762                 // clear the cache
cdf77b 1763                 clearRepositoryMetadataCache(repositoryName);
1e1b85 1764                 repository.resetDisplayName();
8a2e9c 1765             }
JM 1766
f5d0ad 1767             // load repository
JM 1768             logger.info("edit repository " + repository.name);
2ef0a3 1769             r = getRepository(repository.name);
f97bf0 1770         }
JM 1771
f5d0ad 1772         // update settings
2a7306 1773         if (r != null) {
831469 1774             updateConfiguration(r, repository);
2cdb73 1775             // only update symbolic head if it changes
90b8d7 1776             String currentRef = JGitUtils.getHEADRef(r);
JM 1777             if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) {
d394d9 1778                 logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}", 
90b8d7 1779                         repository.name, currentRef, repository.HEAD));
JM 1780                 if (JGitUtils.setHEADtoRef(r, repository.HEAD)) {
1781                     // clear the cache
cdf77b 1782                     clearRepositoryMetadataCache(repository.name);
90b8d7 1783                 }
912938 1784             }
d394d9 1785
JM 1786             // close the repository object
2a7306 1787             r.close();
831469 1788         }
cdf77b 1789         
JM 1790         // update repository cache
1791         removeFromCachedRepositoryList(repositoryName);
1792         // model will actually be replaced on next load because config is stale
9f5a86 1793         addToCachedRepositoryList(repository);
831469 1794     }
fee060 1795     
831469 1796     /**
JM 1797      * Updates the Gitblit configuration for the specified repository.
1798      * 
1799      * @param r
1800      *            the Git repository
1801      * @param repository
1802      *            the Gitblit repository model
1803      */
1804     public void updateConfiguration(Repository r, RepositoryModel repository) {
fee060 1805         StoredConfig config = r.getConfig();
380afa 1806         config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
JM 1807         config.setString(Constants.CONFIG_GITBLIT, null, "owner", repository.owner);
1808         config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
1809         config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
1e1b85 1810         config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
380afa 1811         config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
6adf56 1812         config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
15640f 1813         config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);
380afa 1814         config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
JM 1815         config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);
1816         config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme);
1817         config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSizeCalculation", repository.skipSizeCalculation);
1818         config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSummaryMetrics", repository.skipSummaryMetrics);
1819         config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy",
831469 1820                 repository.federationStrategy.name());
380afa 1821         config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated);
a21fc5 1822
5efafa 1823         updateList(config, "federationSets", repository.federationSets);
JM 1824         updateList(config, "preReceiveScript", repository.preReceiveScripts);
1825         updateList(config, "postReceiveScript", repository.postReceiveScripts);
1826         updateList(config, "mailingList", repository.mailingLists);
1827         updateList(config, "indexBranch", repository.indexedBranches);
4a5a55 1828         
JC 1829         // User Defined Properties
380afa 1830         if (repository.customFields != null) {
JM 1831             if (repository.customFields.size() == 0) {
1832                 // clear section
1833                 config.unsetSection(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS);
1834             } else {
1835                 for (Entry<String, String> property : repository.customFields.entrySet()) {
1836                     // set field
1837                     String key = property.getKey();
1838                     String value = property.getValue();
1839                     config.setString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, key, value);
1840                 }
1841             }
4a5a55 1842         }
a21fc5 1843
831469 1844         try {
JM 1845             config.save();
1846         } catch (IOException e) {
1847             logger.error("Failed to save repository config!", e);
f97bf0 1848         }
f5d0ad 1849     }
5efafa 1850     
JM 1851     private void updateList(StoredConfig config, String field, List<String> list) {
1852         // a null list is skipped, not cleared
1853         // this is for RPC administration where an older manager might be used
1854         if (list == null) {
1855             return;
1856         }
1857         if (ArrayUtils.isEmpty(list)) {
380afa 1858             config.unset(Constants.CONFIG_GITBLIT, null, field);
5efafa 1859         } else {
380afa 1860             config.setStringList(Constants.CONFIG_GITBLIT, null, field, list);
5efafa 1861         }
JM 1862     }
f5d0ad 1863
892570 1864     /**
JM 1865      * Deletes the repository from the file system and removes the repository
1866      * permission from all repository users.
1867      * 
1868      * @param model
1869      * @return true if successful
1870      */
8a2e9c 1871     public boolean deleteRepositoryModel(RepositoryModel model) {
JM 1872         return deleteRepository(model.name);
1873     }
1874
892570 1875     /**
JM 1876      * Deletes the repository from the file system and removes the repository
1877      * permission from all repository users.
1878      * 
1879      * @param repositoryName
1880      * @return true if successful
1881      */
8a2e9c 1882     public boolean deleteRepository(String repositoryName) {
JM 1883         try {
dd6f08 1884             closeRepository(repositoryName);
fee060 1885             // clear the repository cache
cdf77b 1886             clearRepositoryMetadataCache(repositoryName);
1e1b85 1887             
JM 1888             RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
20714a 1889             if (model != null && !ArrayUtils.isEmpty(model.forks)) {
1e1b85 1890                 resetRepositoryListCache();
JM 1891             }
fee060 1892
8a2e9c 1893             File folder = new File(repositoriesFolder, repositoryName);
JM 1894             if (folder.exists() && folder.isDirectory()) {
892570 1895                 FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
85c2e6 1896                 if (userService.deleteRepositoryRole(repositoryName)) {
cdf77b 1897                     logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
8a2e9c 1898                     return true;
JM 1899                 }
1900             }
1901         } catch (Throwable t) {
1902             logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
1903         }
1904         return false;
1905     }
1906
892570 1907     /**
JM 1908      * Returns an html version of the commit message with any global or
1909      * repository-specific regular expression substitution applied.
1910      * 
1911      * @param repositoryName
1912      * @param text
1913      * @return html version of the commit message
1914      */
8c9a20 1915     public String processCommitMessage(String repositoryName, String text) {
JM 1916         String html = StringUtils.breakLinesForHtml(text);
1917         Map<String, String> map = new HashMap<String, String>();
1918         // global regex keys
892570 1919         if (settings.getBoolean(Keys.regex.global, false)) {
JM 1920             for (String key : settings.getAllKeys(Keys.regex.global)) {
8c9a20 1921                 if (!key.equals(Keys.regex.global)) {
JM 1922                     String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1923                     map.put(subKey, settings.getString(key, ""));
8c9a20 1924                 }
JM 1925             }
1926         }
1927
1928         // repository-specific regex keys
892570 1929         List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "."
8c9a20 1930                 + repositoryName.toLowerCase());
JM 1931         for (String key : keys) {
1932             String subKey = key.substring(key.lastIndexOf('.') + 1);
892570 1933             map.put(subKey, settings.getString(key, ""));
8c9a20 1934         }
JM 1935
1936         for (Entry<String, String> entry : map.entrySet()) {
1937             String definition = entry.getValue().trim();
1938             String[] chunks = definition.split("!!!");
1939             if (chunks.length == 2) {
1940                 html = html.replaceAll(chunks[0], chunks[1]);
1941             } else {
1942                 logger.warn(entry.getKey()
1943                         + " improperly formatted.  Use !!! to separate match from replacement: "
1944                         + definition);
1945             }
1946         }
1947         return html;
1948     }
1949
892570 1950     /**
831469 1951      * Returns Gitblit's scheduled executor service for scheduling tasks.
JM 1952      * 
1953      * @return scheduledExecutor
1954      */
1955     public ScheduledExecutorService executor() {
1956         return scheduledExecutor;
1957     }
1958
1959     public static boolean canFederate() {
2c32fd 1960         String passphrase = getString(Keys.federation.passphrase, "");
JM 1961         return !StringUtils.isEmpty(passphrase);
831469 1962     }
JM 1963
1964     /**
1965      * Configures this Gitblit instance to pull any registered federated gitblit
1966      * instances.
1967      */
1968     private void configureFederation() {
2c32fd 1969         boolean validPassphrase = true;
JM 1970         String passphrase = settings.getString(Keys.federation.passphrase, "");
1971         if (StringUtils.isEmpty(passphrase)) {
1972             logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
1973             validPassphrase = false;
831469 1974         }
2c32fd 1975         if (validPassphrase) {
8f73a7 1976             // standard tokens
831469 1977             for (FederationToken tokenType : FederationToken.values()) {
JM 1978                 logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
1979                         getFederationToken(tokenType)));
8f73a7 1980             }
JM 1981
1982             // federation set tokens
1983             for (String set : settings.getStrings(Keys.federation.sets)) {
1984                 logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
1985                         getFederationToken(set)));
831469 1986             }
JM 1987         }
1988
1989         // Schedule the federation executor
1990         List<FederationModel> registrations = getFederationRegistrations();
1991         if (registrations.size() > 0) {
f6740d 1992             FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
JM 1993             scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
831469 1994         }
JM 1995     }
1996
1997     /**
1998      * Returns the list of federated gitblit instances that this instance will
1999      * try to pull.
2000      * 
2001      * @return list of registered gitblit instances
2002      */
2003     public List<FederationModel> getFederationRegistrations() {
2004         if (federationRegistrations.isEmpty()) {
f6740d 2005             federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
831469 2006         }
JM 2007         return federationRegistrations;
2008     }
2009
2010     /**
2011      * Retrieve the specified federation registration.
2012      * 
2013      * @param name
2014      *            the name of the registration
2015      * @return a federation registration
2016      */
2017     public FederationModel getFederationRegistration(String url, String name) {
2018         // check registrations
2019         for (FederationModel r : getFederationRegistrations()) {
2020             if (r.name.equals(name) && r.url.equals(url)) {
2021                 return r;
2022             }
2023         }
2024
2025         // check the results
2026         for (FederationModel r : getFederationResultRegistrations()) {
2027             if (r.name.equals(name) && r.url.equals(url)) {
2028                 return r;
2029             }
2030         }
2031         return null;
2032     }
2033
2034     /**
31abc2 2035      * Returns the list of federation sets.
JM 2036      * 
2037      * @return list of federation sets
2038      */
2039     public List<FederationSet> getFederationSets(String gitblitUrl) {
2040         List<FederationSet> list = new ArrayList<FederationSet>();
2041         // generate standard tokens
2042         for (FederationToken type : FederationToken.values()) {
2043             FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
2044             fset.repositories = getRepositories(gitblitUrl, fset.token);
2045             list.add(fset);
2046         }
2047         // generate tokens for federation sets
2048         for (String set : settings.getStrings(Keys.federation.sets)) {
2049             FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
2050                     getFederationToken(set));
2051             fset.repositories = getRepositories(gitblitUrl, fset.token);
2052             list.add(fset);
2053         }
2054         return list;
2055     }
2056
2057     /**
831469 2058      * Returns the list of possible federation tokens for this Gitblit instance.
JM 2059      * 
2060      * @return list of federation tokens
2061      */
2062     public List<String> getFederationTokens() {
2063         List<String> tokens = new ArrayList<String>();
8f73a7 2064         // generate standard tokens
831469 2065         for (FederationToken type : FederationToken.values()) {
JM 2066             tokens.add(getFederationToken(type));
8f73a7 2067         }
JM 2068         // generate tokens for federation sets
2069         for (String set : settings.getStrings(Keys.federation.sets)) {
2070             tokens.add(getFederationToken(set));
831469 2071         }
JM 2072         return tokens;
2073     }
2074
2075     /**
2076      * Returns the specified federation token for this Gitblit instance.
2077      * 
2078      * @param type
2079      * @return a federation token
2080      */
2081     public String getFederationToken(FederationToken type) {
8f73a7 2082         return getFederationToken(type.name());
JM 2083     }
2084
2085     /**
2086      * Returns the specified federation token for this Gitblit instance.
2087      * 
2088      * @param value
2089      * @return a federation token
2090      */
2091     public String getFederationToken(String value) {
2c32fd 2092         String passphrase = settings.getString(Keys.federation.passphrase, "");
8f73a7 2093         return StringUtils.getSHA1(passphrase + "-" + value);
831469 2094     }
JM 2095
2096     /**
2097      * Compares the provided token with this Gitblit instance's tokens and
2098      * determines if the requested permission may be granted to the token.
2099      * 
2100      * @param req
2101      * @param token
2102      * @return true if the request can be executed
2103      */
2104     public boolean validateFederationRequest(FederationRequest req, String token) {
2105         String all = getFederationToken(FederationToken.ALL);
2106         String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
2107         String jur = getFederationToken(FederationToken.REPOSITORIES);
2108         switch (req) {
2109         case PULL_REPOSITORIES:
2110             return token.equals(all) || token.equals(unr) || token.equals(jur);
2111         case PULL_USERS:
fe24a0 2112         case PULL_TEAMS:
831469 2113             return token.equals(all) || token.equals(unr);
JM 2114         case PULL_SETTINGS:
df162c 2115         case PULL_SCRIPTS:
831469 2116             return token.equals(all);
JM 2117         }
2118         return false;
2119     }
2120
2121     /**
2122      * Acknowledge and cache the status of a remote Gitblit instance.
2123      * 
2124      * @param identification
2125      *            the identification of the pulling Gitblit instance
2126      * @param registration
2127      *            the registration from the pulling Gitblit instance
2128      * @return true if acknowledged
2129      */
2130     public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
2131         // reset the url to the identification of the pulling Gitblit instance
2132         registration.url = identification;
2133         String id = identification;
2134         if (!StringUtils.isEmpty(registration.folder)) {
2135             id += "-" + registration.folder;
2136         }
2137         federationPullResults.put(id, registration);
2138         return true;
2139     }
2140
2141     /**
2142      * Returns the list of registration results.
2143      * 
2144      * @return the list of registration results
2145      */
2146     public List<FederationModel> getFederationResultRegistrations() {
2147         return new ArrayList<FederationModel>(federationPullResults.values());
2148     }
2149
2150     /**
2151      * Submit a federation proposal. The proposal is cached locally and the
2152      * Gitblit administrator(s) are notified via email.
2153      * 
2154      * @param proposal
2155      *            the proposal
2156      * @param gitblitUrl
4aafd4 2157      *            the url of your gitblit instance to send an email to
JM 2158      *            administrators
831469 2159      * @return true if the proposal was submitted
JM 2160      */
2161     public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
2162         // convert proposal to json
93f0b1 2163         String json = JsonUtils.toJsonString(proposal);
831469 2164
JM 2165         try {
2166             // make the proposals folder
b774de 2167             File proposalsFolder = getProposalsFolder();
831469 2168             proposalsFolder.mkdirs();
JM 2169
2170             // cache json to a file
2171             File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
2172             com.gitblit.utils.FileUtils.writeContent(file, json);
2173         } catch (Exception e) {
2174             logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
2175         }
2176
2177         // send an email, if possible
2178         try {
2179             Message message = mailExecutor.createMessageForAdministrators();
2180             if (message != null) {
2181                 message.setSubject("Federation proposal from " + proposal.url);
2182                 message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
2183                         + proposal.token);
2184                 mailExecutor.queue(message);
2185             }
2186         } catch (Throwable t) {
2187             logger.error("Failed to notify administrators of proposal", t);
2188         }
2189         return true;
2190     }
2191
2192     /**
2193      * Returns the list of pending federation proposals
2194      * 
2195      * @return list of federation proposals
2196      */
2197     public List<FederationProposal> getPendingFederationProposals() {
2198         List<FederationProposal> list = new ArrayList<FederationProposal>();
b774de 2199         File folder = getProposalsFolder();
831469 2200         if (folder.exists()) {
JM 2201             File[] files = folder.listFiles(new FileFilter() {
2202                 @Override
2203                 public boolean accept(File file) {
2204                     return file.isFile()
2205                             && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
2206                 }
2207             });
2208             for (File file : files) {
2209                 String json = com.gitblit.utils.FileUtils.readContent(file, null);
31abc2 2210                 FederationProposal proposal = JsonUtils.fromJsonString(json,
JM 2211                         FederationProposal.class);
831469 2212                 list.add(proposal);
JM 2213             }
2214         }
2215         return list;
2216     }
2217
2218     /**
dd9ae7 2219      * Get repositories for the specified token.
JM 2220      * 
2221      * @param gitblitUrl
2222      *            the base url of this gitblit instance
2223      * @param token
2224      *            the federation token
2225      * @return a map of <cloneurl, RepositoryModel>
2226      */
2227     public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
2228         Map<String, String> federationSets = new HashMap<String, String>();
2229         for (String set : getStrings(Keys.federation.sets)) {
2230             federationSets.put(getFederationToken(set), set);
2231         }
2232
2233         // Determine the Gitblit clone url
2234         StringBuilder sb = new StringBuilder();
2235         sb.append(gitblitUrl);
2236         sb.append(Constants.GIT_PATH);
2237         sb.append("{0}");
2238         String cloneUrl = sb.toString();
2239
2240         // Retrieve all available repositories
2241         UserModel user = new UserModel(Constants.FEDERATION_USER);
2242         user.canAdmin = true;
2243         List<RepositoryModel> list = getRepositoryModels(user);
2244
2245         // create the [cloneurl, repositoryModel] map
2246         Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
2247         for (RepositoryModel model : list) {
2248             // by default, setup the url for THIS repository
2249             String url = MessageFormat.format(cloneUrl, model.name);
2250             switch (model.federationStrategy) {
2251             case EXCLUDE:
2252                 // skip this repository
2253                 continue;
2254             case FEDERATE_ORIGIN:
2255                 // federate the origin, if it is defined
2256                 if (!StringUtils.isEmpty(model.origin)) {
2257                     url = model.origin;
2258                 }
2259                 break;
2260             }
2261
2262             if (federationSets.containsKey(token)) {
2263                 // include repositories only for federation set
2264                 String set = federationSets.get(token);
2265                 if (model.federationSets.contains(set)) {
2266                     repositories.put(url, model);
2267                 }
2268             } else {
2269                 // standard federation token for ALL
2270                 repositories.put(url, model);
2271             }
2272         }
2273         return repositories;
2274     }
2275
2276     /**
2277      * Creates a proposal from the token.
2278      * 
2279      * @param gitblitUrl
2280      *            the url of this Gitblit instance
2281      * @param token
2282      * @return a potential proposal
2283      */
2284     public FederationProposal createFederationProposal(String gitblitUrl, String token) {
2285         FederationToken tokenType = FederationToken.REPOSITORIES;
2286         for (FederationToken type : FederationToken.values()) {
2287             if (token.equals(getFederationToken(type))) {
2288                 tokenType = type;
2289                 break;
2290             }
2291         }
2292         Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
2293         FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
2294                 repositories);
2295         return proposal;
2296     }
2297
2298     /**
831469 2299      * Returns the proposal identified by the supplied token.
JM 2300      * 
2301      * @param token
2302      * @return the specified proposal or null
2303      */
2304     public FederationProposal getPendingFederationProposal(String token) {
2305         List<FederationProposal> list = getPendingFederationProposals();
2306         for (FederationProposal proposal : list) {
2307             if (proposal.token.equals(token)) {
2308                 return proposal;
2309             }
2310         }
2311         return null;
2312     }
2313
2314     /**
2315      * Deletes a pending federation proposal.
2316      * 
2317      * @param a
2318      *            proposal
2319      * @return true if the proposal was deleted
2320      */
2321     public boolean deletePendingFederationProposal(FederationProposal proposal) {
b774de 2322         File folder = getProposalsFolder();
831469 2323         File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
JM 2324         return file.delete();
2325     }
2326
2327     /**
d7905a 2328      * Returns the list of all Groovy push hook scripts. Script files must have
6cc1d4 2329      * .groovy extension
JM 2330      * 
2331      * @return list of available hook scripts
2332      */
d7905a 2333     public List<String> getAllScripts() {
6cc1d4 2334         File groovyFolder = getGroovyScriptsFolder();
JM 2335         File[] files = groovyFolder.listFiles(new FileFilter() {
2336             @Override
2337             public boolean accept(File pathname) {
2338                 return pathname.isFile() && pathname.getName().endsWith(".groovy");
2339             }
2340         });
2341         List<String> scripts = new ArrayList<String>();
2342         if (files != null) {
2343             for (File file : files) {
2344                 String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
d7905a 2345                 scripts.add(script);
6cc1d4 2346             }
JM 2347         }
2348         return scripts;
2349     }
d7905a 2350
JM 2351     /**
2352      * Returns the list of pre-receive scripts the repository inherited from the
2353      * global settings and team affiliations.
2354      * 
2355      * @param repository
2356      *            if null only the globally specified scripts are returned
2357      * @return a list of scripts
2358      */
2359     public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
2360         Set<String> scripts = new LinkedHashSet<String>();
2361         // Globals
0d013a 2362         for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
JM 2363             if (script.endsWith(".groovy")) {
d7905a 2364                 scripts.add(script.substring(0, script.lastIndexOf('.')));
0d013a 2365             } else {
d7905a 2366                 scripts.add(script);
0d013a 2367             }
JM 2368         }
d7905a 2369
JM 2370         // Team Scripts
2371         if (repository != null) {
2372             for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
2373                 TeamModel team = userService.getTeamModel(teamname);
2374                 scripts.addAll(team.preReceiveScripts);
2375             }
2376         }
2377         return new ArrayList<String>(scripts);
0d013a 2378     }
d7905a 2379
JM 2380     /**
2381      * Returns the list of all available Groovy pre-receive push hook scripts
2382      * that are not already inherited by the repository. Script files must have
2383      * .groovy extension
2384      * 
2385      * @param repository
2386      *            optional parameter
2387      * @return list of available hook scripts
2388      */
2389     public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
2390         Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
2391
2392         // create list of available scripts by excluding inherited scripts
2393         List<String> scripts = new ArrayList<String>();
2394         for (String script : getAllScripts()) {
2395             if (!inherited.contains(script)) {
2396                 scripts.add(script);
2397             }
2398         }
2399         return scripts;
2400     }
2401
2402     /**
2403      * Returns the list of post-receive scripts the repository inherited from
2404      * the global settings and team affiliations.
2405      * 
2406      * @param repository
2407      *            if null only the globally specified scripts are returned
2408      * @return a list of scripts
2409      */
2410     public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
2411         Set<String> scripts = new LinkedHashSet<String>();
2412         // Global Scripts
0d013a 2413         for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
JM 2414             if (script.endsWith(".groovy")) {
d7905a 2415                 scripts.add(script.substring(0, script.lastIndexOf('.')));
0d013a 2416             } else {
d7905a 2417                 scripts.add(script);
0d013a 2418             }
JM 2419         }
d7905a 2420         // Team Scripts
JM 2421         if (repository != null) {
2422             for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
2423                 TeamModel team = userService.getTeamModel(teamname);
2424                 scripts.addAll(team.postReceiveScripts);
2425             }
2426         }
2427         return new ArrayList<String>(scripts);
2428     }
2429
2430     /**
2431      * Returns the list of unused Groovy post-receive push hook scripts that are
2432      * not already inherited by the repository. Script files must have .groovy
2433      * extension
2434      * 
2435      * @param repository
2436      *            optional parameter
2437      * @return list of available hook scripts
2438      */
2439     public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
2440         Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
2441
2442         // create list of available scripts by excluding inherited scripts
2443         List<String> scripts = new ArrayList<String>();
2444         for (String script : getAllScripts()) {
2445             if (!inherited.contains(script)) {
2446                 scripts.add(script);
2447             }
2448         }
2449         return scripts;
0d013a 2450     }
d896e6 2451     
JM 2452     /**
2453      * Search the specified repositories using the Lucene query.
2454      * 
2455      * @param query
d04009 2456      * @param page
JM 2457      * @param pageSize
d896e6 2458      * @param repositories
JM 2459      * @return
2460      */
d04009 2461     public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) {        
JM 2462         List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);
d896e6 2463         return srs;
JM 2464     }
0d013a 2465
6cc1d4 2466     /**
831469 2467      * Notify the administrators by email.
JM 2468      * 
2469      * @param subject
2470      * @param message
2471      */
eb96ea 2472     public void sendMailToAdministrators(String subject, String message) {
831469 2473         try {
JM 2474             Message mail = mailExecutor.createMessageForAdministrators();
2475             if (mail != null) {
2476                 mail.setSubject(subject);
2477                 mail.setText(message);
2478                 mailExecutor.queue(mail);
2479             }
2480         } catch (MessagingException e) {
2481             logger.error("Messaging error", e);
2482         }
2483     }
2484
2485     /**
fa54be 2486      * Notify users by email of something.
JM 2487      * 
2488      * @param subject
2489      * @param message
2490      * @param toAddresses
2491      */
0b9119 2492     public void sendMail(String subject, String message, Collection<String> toAddresses) {
eb96ea 2493         this.sendMail(subject, message, toAddresses.toArray(new String[0]));
fa54be 2494     }
JM 2495
2496     /**
2497      * Notify users by email of something.
2498      * 
2499      * @param subject
2500      * @param message
2501      * @param toAddresses
2502      */
eb96ea 2503     public void sendMail(String subject, String message, String... toAddresses) {
fa54be 2504         try {
JM 2505             Message mail = mailExecutor.createMessage(toAddresses);
2506             if (mail != null) {
2507                 mail.setSubject(subject);
2508                 mail.setText(message);
2509                 mailExecutor.queue(mail);
2510             }
2511         } catch (MessagingException e) {
2512             logger.error("Messaging error", e);
2513         }
b938ae 2514     }
JM 2515
2516     /**
b75734 2517      * Returns the descriptions/comments of the Gitblit config settings.
JM 2518      * 
84c1d5 2519      * @return SettingsModel
b75734 2520      */
84c1d5 2521     public ServerSettings getSettingsModel() {
b75734 2522         // ensure that the current values are updated in the setting models
773bb6 2523         for (String key : settings.getAllKeys(null)) {
JM 2524             SettingModel setting = settingsModel.get(key);
e09d4b 2525             if (setting == null) {
JM 2526                 // unreferenced setting, create a setting model
2527                 setting = new SettingModel();
2528                 setting.name = key;
2529                 settingsModel.add(setting);
773bb6 2530             }
e09d4b 2531             setting.currentValue = settings.getString(key, "");            
773bb6 2532         }
d7905a 2533         settingsModel.pushScripts = getAllScripts();
84c1d5 2534         return settingsModel;
b75734 2535     }
JM 2536
2537     /**
2538      * Parse the properties file and aggregate all the comments by the setting
2539      * key. A setting model tracks the current value, the default value, the
2540      * description of the setting and and directives about the setting.
85f639 2541      * @param referencePropertiesInputStream
b75734 2542      * 
JM 2543      * @return Map<String, SettingModel>
2544      */
85f639 2545     private ServerSettings loadSettingModels(InputStream referencePropertiesInputStream) {
84c1d5 2546         ServerSettings settingsModel = new ServerSettings();
0aa8cf 2547         settingsModel.supportsCredentialChanges = userService.supportsCredentialChanges();
JM 2548         settingsModel.supportsDisplayNameChanges = userService.supportsDisplayNameChanges();
2549         settingsModel.supportsEmailAddressChanges = userService.supportsEmailAddressChanges();
2550         settingsModel.supportsTeamMembershipChanges = userService.supportsTeamMembershipChanges();
b75734 2551         try {
JM 2552             // Read bundled Gitblit properties to extract setting descriptions.
2553             // This copy is pristine and only used for populating the setting
2554             // models map.
85f639 2555             InputStream is = referencePropertiesInputStream;
b75734 2556             BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
JM 2557             StringBuilder description = new StringBuilder();
2558             SettingModel setting = new SettingModel();
2559             String line = null;
2560             while ((line = propertiesReader.readLine()) != null) {
2561                 if (line.length() == 0) {
2562                     description.setLength(0);
2563                     setting = new SettingModel();
2564                 } else {
2565                     if (line.charAt(0) == '#') {
2566                         if (line.length() > 1) {
2567                             String text = line.substring(1).trim();
2568                             if (SettingModel.CASE_SENSITIVE.equals(text)) {
2569                                 setting.caseSensitive = true;
2570                             } else if (SettingModel.RESTART_REQUIRED.equals(text)) {
2571                                 setting.restartRequired = true;
2572                             } else if (SettingModel.SPACE_DELIMITED.equals(text)) {
2573                                 setting.spaceDelimited = true;
2574                             } else if (text.startsWith(SettingModel.SINCE)) {
2575                                 try {
2576                                     setting.since = text.split(" ")[1];
2577                                 } catch (Exception e) {
2578                                     setting.since = text;
2579                                 }
2580                             } else {
2581                                 description.append(text);
2582                                 description.append('\n');
2583                             }
2584                         }
2585                     } else {
2586                         String[] kvp = line.split("=", 2);
2587                         String key = kvp[0].trim();
2588                         setting.name = key;
2589                         setting.defaultValue = kvp[1].trim();
2590                         setting.currentValue = setting.defaultValue;
2591                         setting.description = description.toString().trim();
84c1d5 2592                         settingsModel.add(setting);
b75734 2593                         description.setLength(0);
JM 2594                         setting = new SettingModel();
2595                     }
2596                 }
2597             }
2598             propertiesReader.close();
2599         } catch (NullPointerException e) {
2600             logger.error("Failed to find resource copy of gitblit.properties");
2601         } catch (IOException e) {
2602             logger.error("Failed to load resource copy of gitblit.properties");
2603         }
84c1d5 2604         return settingsModel;
b75734 2605     }
JM 2606
2607     /**
892570 2608      * Configure the Gitblit singleton with the specified settings source. This
JM 2609      * source may be file settings (Gitblit GO) or may be web.xml settings
2610      * (Gitblit WAR).
2611      * 
2612      * @param settings
2613      */
f6740d 2614     public void configureContext(IStoredSettings settings, boolean startFederation) {
1f9dae 2615         logger.info("Reading configuration from " + settings.toString());
892570 2616         this.settings = settings;
b774de 2617         repositoriesFolder = getRepositoriesFolder();
5450d0 2618         logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
fee060 2619
JM 2620         // calculate repository list settings checksum for future config changes
2621         repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum());
2622
2623         // build initial repository list
2624         if (settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
2625             logger.info("Identifying available repositories...");
2626             getRepositoryList();
2627         }
6c6e7d 2628         
JM 2629         logTimezone("JVM", TimeZone.getDefault());
2630         logTimezone(Constants.NAME, getTimezone());
2631
486ee1 2632         serverStatus = new ServerStatus(isGO());
85f639 2633
LM 2634         if (this.userService == null) {
2635             String realm = settings.getString(Keys.realm.userService, "users.properties");
2636             IUserService loginService = null;
2637             try {
2638                 // check to see if this "file" is a login service class
2639                 Class<?> realmClass = Class.forName(realm);
2640                 loginService = (IUserService) realmClass.newInstance();
2641             } catch (Throwable t) {
2642                 loginService = new GitblitUserService();
2643             }
2644             setUserService(loginService);
5450d0 2645         }
13a3f5 2646         
JM 2647         // load and cache the project metadata
2648         projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect());
2649         getProjectConfigs();
831469 2650         mailExecutor = new MailExecutor(settings);
JM 2651         if (mailExecutor.isReady()) {
e31da0 2652             logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
831469 2653             scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
JM 2654         } else {
2655             logger.warn("Mail server is not properly configured.  Mail services disabled.");
f6740d 2656         }
d896e6 2657         luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
273cb9 2658         logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes.");
JM 2659         scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2, TimeUnit.MINUTES);
f6740d 2660         if (startFederation) {
JM 2661             configureFederation();
478678 2662         }
JM 2663         
2664         // Configure JGit
2665         WindowCacheConfig cfg = new WindowCacheConfig();
2666         
2667         cfg.setPackedGitWindowSize(settings.getFilesize(Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
2668         cfg.setPackedGitLimit(settings.getFilesize(Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
2669         cfg.setDeltaBaseCacheLimit(settings.getFilesize(Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
2670         cfg.setPackedGitOpenFiles(settings.getFilesize(Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
2671         cfg.setStreamFileThreshold(settings.getFilesize(Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
2672         cfg.setPackedGitMMAP(settings.getBoolean(Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
2673         
2674         try {
2675             WindowCache.reconfigure(cfg);
2676             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
2677             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
2678             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
2679             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
2680             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
2681             logger.debug(MessageFormat.format("{0} = {1}", Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
2682         } catch (IllegalArgumentException e) {
2683             logger.error("Failed to configure JGit parameters!", e);
2684         }
165254 2685
JP 2686         ContainerUtils.CVE_2007_0450.test();
6c6e7d 2687     }
JM 2688     
2689     private void logTimezone(String type, TimeZone zone) {
2690         SimpleDateFormat df = new SimpleDateFormat("z Z");
2691         df.setTimeZone(zone);
2692         String offset = df.format(new Date());
2693         logger.info(type + " timezone is " + zone.getID() + " (" + offset + ")");
87cc1e 2694     }
JM 2695
892570 2696     /**
JM 2697      * Configure Gitblit from the web.xml, if no configuration has already been
2698      * specified.
2699      * 
2700      * @see ServletContextListener.contextInitialize(ServletContextEvent)
2701      */
87cc1e 2702     @Override
JM 2703     public void contextInitialized(ServletContextEvent contextEvent) {
85f639 2704         contextInitialized(contextEvent, contextEvent.getServletContext().getResourceAsStream("/WEB-INF/reference.properties"));
LM 2705     }
2706
2707     public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) {
b75734 2708         servletContext = contextEvent.getServletContext();
892570 2709         if (settings == null) {
JM 2710             // Gitblit WAR is running in a servlet container
b774de 2711             ServletContext context = contextEvent.getServletContext();
JM 2712             WebXmlSettings webxmlSettings = new WebXmlSettings(context);
2713
2714             // 0.7.0 web.properties in the deployed war folder
7a1889 2715             String webProps = context.getRealPath("/WEB-INF/web.properties");
JM 2716             if (!StringUtils.isEmpty(webProps)) {
2717                 File overrideFile = new File(webProps);
2718                 if (overrideFile.exists()) {
2719                     webxmlSettings.applyOverrides(overrideFile);
2720                 }
b774de 2721             }
7a1889 2722             
b774de 2723
JM 2724             // 0.8.0 gitblit.properties file located outside the deployed war
2725             // folder lie, for example, on RedHat OpenShift.
7a1889 2726             File overrideFile = getFileOrFolder("gitblit.properties");
b774de 2727             if (!overrideFile.getPath().equals("gitblit.properties")) {
JM 2728                 webxmlSettings.applyOverrides(overrideFile);
2729             }
f6740d 2730             configureContext(webxmlSettings, true);
e36d4d 2731
JM 2732             // Copy the included scripts to the configured groovy folder
2733             File localScripts = getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
2734             if (!localScripts.exists()) {
2735                 File includedScripts = new File(context.getRealPath("/WEB-INF/groovy"));
2736                 if (!includedScripts.equals(localScripts)) {
2737                     try {
2738                         com.gitblit.utils.FileUtils.copy(localScripts, includedScripts.listFiles());
2739                     } catch (IOException e) {
2740                         logger.error(MessageFormat.format(
2741                                 "Failed to copy included Groovy scripts from {0} to {1}",
2742                                 includedScripts, localScripts));
2743                     }
2744                 }
2745             }
87cc1e 2746         }
b6db0d 2747         
85f639 2748         settingsModel = loadSettingModels(referencePropertiesInputStream);
486ee1 2749         serverStatus.servletContainer = servletContext.getServerInfo();
87cc1e 2750     }
JM 2751
892570 2752     /**
JM 2753      * Gitblit is being shutdown either because the servlet container is
2754      * shutting down or because the servlet container is re-deploying Gitblit.
2755      */
87cc1e 2756     @Override
JM 2757     public void contextDestroyed(ServletContextEvent contextEvent) {
f339f5 2758         logger.info("Gitblit context destroyed by servlet container.");
831469 2759         scheduledExecutor.shutdownNow();
b938ae 2760         luceneExecutor.close();
87cc1e 2761     }
1e1b85 2762     
JM 2763     /**
2764      * Creates a personal fork of the specified repository. The clone is view
2765      * restricted by default and the owner of the source repository is given
2766      * access to the clone. 
2767      * 
2768      * @param repository
2769      * @param user
1f52c8 2770      * @return the repository model of the fork, if successful
JM 2771      * @throws GitBlitException
1e1b85 2772      */
1f52c8 2773     public RepositoryModel fork(RepositoryModel repository, UserModel user) throws GitBlitException {
1e1b85 2774         String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
JM 2775         String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name);
1f52c8 2776
JM 2777         // clone the repository
1e1b85 2778         try {
JM 2779             JGitUtils.cloneRepository(repositoriesFolder, cloneName, fromUrl, true, null);
2780         } catch (Exception e) {
1f52c8 2781             throw new GitBlitException(e);
1e1b85 2782         }
1f52c8 2783
JM 2784         // create a Gitblit repository model for the clone
2785         RepositoryModel cloneModel = repository.cloneAs(cloneName);
20714a 2786         // owner has REWIND/RW+ permissions
1f52c8 2787         cloneModel.owner = user.username;
JM 2788         updateRepositoryModel(cloneName, cloneModel, false);
2789
20714a 2790         // add the owner of the source repository to the clone's access list
JM 2791         if (!StringUtils.isEmpty(repository.owner)) {
2792             UserModel originOwner = getUserModel(repository.owner);
2793             if (originOwner != null) {
2794                 originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
2795                 updateUserModel(originOwner.username, originOwner, false);
1f52c8 2796             }
JM 2797         }
2798
20714a 2799         // grant origin's user list clone permission to fork
JM 2800         List<String> users = getRepositoryUsers(repository);
2801         List<UserModel> cloneUsers = new ArrayList<UserModel>();
2802         for (String name : users) {
2803             if (!name.equalsIgnoreCase(user.username)) {
2804                 UserModel cloneUser = getUserModel(name);
2805                 if (cloneUser.canClone(repository)) {
2806                     // origin user can clone origin, grant clone access to fork
2807                     cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
2808                 }
2809                 cloneUsers.add(cloneUser);
2810             }
2811         }
2812         userService.updateUserModels(cloneUsers);
2813
2814         // grant origin's team list clone permission to fork
2815         List<String> teams = getRepositoryTeams(repository);
2816         List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
2817         for (String name : teams) {
2818             TeamModel cloneTeam = getTeamModel(name);
2819             if (cloneTeam.canClone(repository)) {
2820                 // origin team can clone origin, grant clone access to fork
2821                 cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
2822             }
2823             cloneTeams.add(cloneTeam);
2824         }
2825         userService.updateTeamModels(cloneTeams);            
2826
1f52c8 2827         // add this clone to the cached model
9f5a86 2828         addToCachedRepositoryList(cloneModel);
1f52c8 2829         return cloneModel;
1e1b85 2830     }
85f639 2831
LM 2832     /**
2833      * Allow to understand if GitBlit supports and is configured to allow
2834      * cookie-based authentication.
2835      * 
2836      * @return status of Cookie authentication enablement.
2837      */
2838     public boolean allowCookieAuthentication() {
2839         return GitBlit.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
2840     }
fc948c 2841 }