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