James Moger
2014-07-01 c72b44f85fa37650c1540d330117f92687fbd37f
commit | author | age
95cdba 1 /*
JM 2  * Copyright 2013 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  */
16 package com.gitblit.manager;
17
18 import java.io.File;
19 import java.io.FileFilter;
20 import java.io.IOException;
21 import java.lang.reflect.Field;
22 import java.net.URI;
23 import java.net.URISyntaxException;
e685ba 24 import java.nio.charset.Charset;
95cdba 25 import java.text.MessageFormat;
JM 26 import java.text.SimpleDateFormat;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Calendar;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.Date;
33 import java.util.HashSet;
34 import java.util.LinkedHashMap;
35 import java.util.LinkedHashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Set;
40 import java.util.TreeSet;
41 import java.util.concurrent.ConcurrentHashMap;
42 import java.util.concurrent.Executors;
43 import java.util.concurrent.ScheduledExecutorService;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.atomic.AtomicInteger;
46 import java.util.concurrent.atomic.AtomicReference;
47
48 import org.eclipse.jgit.lib.Repository;
49 import org.eclipse.jgit.lib.RepositoryCache;
50 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
51 import org.eclipse.jgit.lib.StoredConfig;
52 import org.eclipse.jgit.storage.file.FileBasedConfig;
53 import org.eclipse.jgit.storage.file.WindowCacheConfig;
54 import org.eclipse.jgit.util.FS;
55 import org.eclipse.jgit.util.FileUtils;
e685ba 56 import org.eclipse.jgit.util.RawParseUtils;
95cdba 57 import org.slf4j.Logger;
JM 58 import org.slf4j.LoggerFactory;
59
60 import com.gitblit.Constants;
61 import com.gitblit.Constants.AccessPermission;
62 import com.gitblit.Constants.AccessRestrictionType;
63 import com.gitblit.Constants.AuthorizationControl;
64 import com.gitblit.Constants.CommitMessageRenderer;
65 import com.gitblit.Constants.FederationStrategy;
66 import com.gitblit.Constants.PermissionType;
67 import com.gitblit.Constants.RegistrantType;
68 import com.gitblit.GitBlitException;
69 import com.gitblit.IStoredSettings;
70 import com.gitblit.Keys;
ca4d98 71 import com.gitblit.extensions.RepositoryLifeCycleListener;
95cdba 72 import com.gitblit.models.ForkModel;
JM 73 import com.gitblit.models.Metric;
74 import com.gitblit.models.RefModel;
75 import com.gitblit.models.RegistrantAccessPermission;
76 import com.gitblit.models.RepositoryModel;
77 import com.gitblit.models.SearchResult;
78 import com.gitblit.models.TeamModel;
79 import com.gitblit.models.UserModel;
7bf6e1 80 import com.gitblit.service.GarbageCollectorService;
JM 81 import com.gitblit.service.LuceneService;
82 import com.gitblit.service.MirrorService;
95cdba 83 import com.gitblit.utils.ArrayUtils;
JM 84 import com.gitblit.utils.ByteFormat;
85 import com.gitblit.utils.CommitCache;
86 import com.gitblit.utils.DeepCopier;
87 import com.gitblit.utils.JGitUtils;
88 import com.gitblit.utils.JGitUtils.LastChange;
89 import com.gitblit.utils.MetricUtils;
90 import com.gitblit.utils.ModelUtils;
91 import com.gitblit.utils.ObjectCache;
92 import com.gitblit.utils.StringUtils;
93 import com.gitblit.utils.TimeUtils;
94
95 /**
96  * Repository manager creates, updates, deletes and caches git repositories.  It
97  * also starts services to mirror, index, and cleanup repositories.
98  *
99  * @author James Moger
100  *
101  */
102 public class RepositoryManager implements IRepositoryManager {
103
104     private final Logger logger = LoggerFactory.getLogger(getClass());
105
106     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
107
108     private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
109
110     private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
111
112     private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>();
113
114     private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
115
116     private final IStoredSettings settings;
117
118     private final IRuntimeManager runtimeManager;
119
ca4d98 120     private final IPluginManager pluginManager;
JM 121
95cdba 122     private final IUserManager userManager;
JM 123
124     private final File repositoriesFolder;
125
7bf6e1 126     private LuceneService luceneExecutor;
95cdba 127
7bf6e1 128     private GarbageCollectorService gcExecutor;
95cdba 129
7bf6e1 130     private MirrorService mirrorExecutor;
95cdba 131
JM 132     public RepositoryManager(
133             IRuntimeManager runtimeManager,
ca4d98 134             IPluginManager pluginManager,
95cdba 135             IUserManager userManager) {
JM 136
137         this.settings = runtimeManager.getSettings();
138         this.runtimeManager = runtimeManager;
ca4d98 139         this.pluginManager = pluginManager;
95cdba 140         this.userManager = userManager;
JM 141         this.repositoriesFolder = runtimeManager.getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
142     }
143
144     @Override
269c50 145     public RepositoryManager start() {
JM 146         logger.info("Repositories folder : {}", repositoriesFolder.getAbsolutePath());
95cdba 147
JM 148         // initialize utilities
149         String prefix = settings.getString(Keys.git.userRepositoryPrefix, "~");
150         ModelUtils.setUserRepoPrefix(prefix);
151
152         // calculate repository list settings checksum for future config changes
153         repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum());
154
155         // build initial repository list
156         if (settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
269c50 157             logger.info("Identifying repositories...");
95cdba 158             getRepositoryList();
JM 159         }
160
161         configureLuceneIndexing();
162         configureGarbageCollector();
163         configureMirrorExecutor();
164         configureJGit();
165         configureCommitCache();
166
088b6f 167         confirmWriteAccess();
JM 168
95cdba 169         return this;
JM 170     }
171
172     @Override
269c50 173     public RepositoryManager stop() {
95cdba 174         scheduledExecutor.shutdownNow();
JM 175         luceneExecutor.close();
176         gcExecutor.close();
177         mirrorExecutor.close();
178
388a23 179         closeAll();
95cdba 180         return this;
JM 181     }
182
183     /**
184      * Returns the most recent change date of any repository served by Gitblit.
185      *
186      * @return a date
187      */
188     @Override
189     public Date getLastActivityDate() {
190         Date date = null;
191         for (String name : getRepositoryList()) {
192             Repository r = getRepository(name);
193             Date lastChange = JGitUtils.getLastChange(r).when;
194             r.close();
195             if (lastChange != null && (date == null || lastChange.after(date))) {
196                 date = lastChange;
197             }
198         }
199         return date;
200     }
201
202     /**
203      * Returns the path of the repositories folder. This method checks to see if
204      * Gitblit is running on a cloud service and may return an adjusted path.
205      *
206      * @return the repositories folder path
207      */
208     @Override
209     public File getRepositoriesFolder() {
210         return repositoriesFolder;
211     }
212
213     /**
214      * Returns the path of the Groovy folder. This method checks to see if
215      * Gitblit is running on a cloud service and may return an adjusted path.
216      *
217      * @return the Groovy scripts folder path
218      */
219     @Override
220     public File getHooksFolder() {
221         return runtimeManager.getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy");
222     }
223
224     /**
225      * Returns the path of the Groovy Grape folder. This method checks to see if
226      * Gitblit is running on a cloud service and may return an adjusted path.
227      *
228      * @return the Groovy Grape folder path
229      */
230     @Override
231     public File getGrapesFolder() {
232         return runtimeManager.getFileOrFolder(Keys.groovy.grapeFolder, "${baseFolder}/groovy/grape");
233     }
234
235     /**
236      *
237      * @return true if we are running the gc executor
238      */
239     @Override
240     public boolean isCollectingGarbage() {
241         return gcExecutor != null && gcExecutor.isRunning();
242     }
243
244     /**
245      * Returns true if Gitblit is actively collecting garbage in this repository.
246      *
247      * @param repositoryName
248      * @return true if actively collecting garbage
249      */
250     @Override
251     public boolean isCollectingGarbage(String repositoryName) {
252         return gcExecutor != null && gcExecutor.isCollectingGarbage(repositoryName);
253     }
254
255     /**
256      * Returns the effective list of permissions for this user, taking into account
257      * team memberships, ownerships.
258      *
259      * @param user
260      * @return the effective list of permissions for the user
261      */
262     @Override
263     public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
264         if (StringUtils.isEmpty(user.username)) {
265             // new user
266             return new ArrayList<RegistrantAccessPermission>();
267         }
268         Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();
269         set.addAll(user.getRepositoryPermissions());
270         // Flag missing repositories
271         for (RegistrantAccessPermission permission : set) {
272             if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) {
273                 RepositoryModel rm = getRepositoryModel(permission.registrant);
274                 if (rm == null) {
275                     permission.permissionType = PermissionType.MISSING;
276                     permission.mutable = false;
277                     continue;
278                 }
279             }
280         }
281
282         // TODO reconsider ownership as a user property
283         // manually specify personal repository ownerships
284         for (RepositoryModel rm : repositoryListCache.values()) {
285             if (rm.isUsersPersonalRepository(user.username) || rm.isOwner(user.username)) {
286                 RegistrantAccessPermission rp = new RegistrantAccessPermission(rm.name, AccessPermission.REWIND,
287                         PermissionType.OWNER, RegistrantType.REPOSITORY, null, false);
288                 // user may be owner of a repository to which they've inherited
289                 // a team permission, replace any existing perm with owner perm
290                 set.remove(rp);
291                 set.add(rp);
292             }
293         }
294
295         List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set);
296         Collections.sort(list);
297         return list;
298     }
299
300     /**
301      * Returns the list of users and their access permissions for the specified
302      * repository including permission source information such as the team or
303      * regular expression which sets the permission.
304      *
305      * @param repository
306      * @return a list of RegistrantAccessPermissions
307      */
308     @Override
309     public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
310         List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
311         if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
312             // no permissions needed, REWIND for everyone!
313             return list;
314         }
315         if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl)) {
316             // no permissions needed, REWIND for authenticated!
317             return list;
318         }
319         // NAMED users and teams
320         for (UserModel user : userManager.getAllUsers()) {
321             RegistrantAccessPermission ap = user.getRepositoryPermission(repository);
322             if (ap.permission.exceeds(AccessPermission.NONE)) {
323                 list.add(ap);
324             }
325         }
326         return list;
327     }
328
329     /**
330      * Sets the access permissions to the specified repository for the specified users.
331      *
332      * @param repository
333      * @param permissions
334      * @return true if the user models have been updated
335      */
336     @Override
337     public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
338         List<UserModel> users = new ArrayList<UserModel>();
339         for (RegistrantAccessPermission up : permissions) {
340             if (up.mutable) {
341                 // only set editable defined permissions
342                 UserModel user = userManager.getUserModel(up.registrant);
343                 user.setRepositoryPermission(repository.name, up.permission);
344                 users.add(user);
345             }
346         }
347         return userManager.updateUserModels(users);
348     }
349
350     /**
351      * Returns the list of all users who have an explicit access permission
352      * for the specified repository.
353      *
354      * @see IUserService.getUsernamesForRepositoryRole(String)
355      * @param repository
356      * @return list of all usernames that have an access permission for the repository
357      */
358     @Override
359     public List<String> getRepositoryUsers(RepositoryModel repository) {
360         return userManager.getUsernamesForRepositoryRole(repository.name);
361     }
362
363     /**
364      * Returns the list of teams and their access permissions for the specified
365      * repository including the source of the permission such as the admin flag
366      * or a regular expression.
367      *
368      * @param repository
369      * @return a list of RegistrantAccessPermissions
370      */
371     @Override
372     public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
373         List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
374         for (TeamModel team : userManager.getAllTeams()) {
375             RegistrantAccessPermission ap = team.getRepositoryPermission(repository);
376             if (ap.permission.exceeds(AccessPermission.NONE)) {
377                 list.add(ap);
378             }
379         }
380         Collections.sort(list);
381         return list;
382     }
383
384     /**
385      * Sets the access permissions to the specified repository for the specified teams.
386      *
387      * @param repository
388      * @param permissions
389      * @return true if the team models have been updated
390      */
391     @Override
392     public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
393         List<TeamModel> teams = new ArrayList<TeamModel>();
394         for (RegistrantAccessPermission tp : permissions) {
395             if (tp.mutable) {
396                 // only set explicitly defined access permissions
397                 TeamModel team = userManager.getTeamModel(tp.registrant);
398                 team.setRepositoryPermission(repository.name, tp.permission);
399                 teams.add(team);
400             }
401         }
402         return userManager.updateTeamModels(teams);
403     }
404
405     /**
406      * Returns the list of all teams who have an explicit access permission for
407      * the specified repository.
408      *
409      * @see IUserService.getTeamnamesForRepositoryRole(String)
410      * @param repository
411      * @return list of all teamnames with explicit access permissions to the repository
412      */
413     @Override
414     public List<String> getRepositoryTeams(RepositoryModel repository) {
415         return userManager.getTeamNamesForRepositoryRole(repository.name);
416     }
417
418     /**
419      * Adds the repository to the list of cached repositories if Gitblit is
420      * configured to cache the repository list.
421      *
422      * @param model
423      */
424     @Override
425     public void addToCachedRepositoryList(RepositoryModel model) {
426         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
44c005 427             String key = getRepositoryKey(model.name);
JM 428             repositoryListCache.put(key, model);
95cdba 429
JM 430             // update the fork origin repository with this repository clone
431             if (!StringUtils.isEmpty(model.originRepository)) {
44c005 432                 String originKey = getRepositoryKey(model.originRepository);
4a2752 433                 if (repositoryListCache.containsKey(originKey)) {
JM 434                     RepositoryModel origin = repositoryListCache.get(originKey);
95cdba 435                     origin.addFork(model.name);
JM 436                 }
437             }
438         }
439     }
440
441     /**
442      * Removes the repository from the list of cached repositories.
443      *
444      * @param name
445      * @return the model being removed
446      */
447     private RepositoryModel removeFromCachedRepositoryList(String name) {
448         if (StringUtils.isEmpty(name)) {
449             return null;
450         }
44c005 451         String key = getRepositoryKey(name);
JM 452         return repositoryListCache.remove(key);
95cdba 453     }
JM 454
455     /**
456      * Clears all the cached metadata for the specified repository.
457      *
458      * @param repositoryName
459      */
460     private void clearRepositoryMetadataCache(String repositoryName) {
461         repositorySizeCache.remove(repositoryName);
462         repositoryMetricsCache.remove(repositoryName);
1b4449 463         CommitCache.instance().clear(repositoryName);
95cdba 464     }
JM 465
466     /**
ce07c4 467      * Reset all caches for this repository.
JM 468      *
469      * @param repositoryName
470      * @since 1.5.1
471      */
472     @Override
473     public void resetRepositoryCache(String repositoryName) {
474         removeFromCachedRepositoryList(repositoryName);
475         clearRepositoryMetadataCache(repositoryName);
8bb12d 476         // force a reload of the repository data (ticket-82, issue-433)
JM 477         getRepositoryModel(repositoryName);
ce07c4 478     }
JM 479
480     /**
95cdba 481      * Resets the repository list cache.
JM 482      *
483      */
484     @Override
485     public void resetRepositoryListCache() {
486         logger.info("Repository cache manually reset");
487         repositoryListCache.clear();
381137 488         repositorySizeCache.clear();
JM 489         repositoryMetricsCache.clear();
1b4449 490         CommitCache.instance().clear();
95cdba 491     }
JM 492
493     /**
494      * Calculate the checksum of settings that affect the repository list cache.
495      * @return a checksum
496      */
497     private String getRepositoryListSettingsChecksum() {
498         StringBuilder ns = new StringBuilder();
499         ns.append(settings.getString(Keys.git.cacheRepositoryList, "")).append('\n');
500         ns.append(settings.getString(Keys.git.onlyAccessBareRepositories, "")).append('\n');
501         ns.append(settings.getString(Keys.git.searchRepositoriesSubfolders, "")).append('\n');
502         ns.append(settings.getString(Keys.git.searchRecursionDepth, "")).append('\n');
503         ns.append(settings.getString(Keys.git.searchExclusions, "")).append('\n');
504         String checksum = StringUtils.getSHA1(ns.toString());
505         return checksum;
506     }
507
508     /**
509      * Compare the last repository list setting checksum to the current checksum.
510      * If different then clear the cache so that it may be rebuilt.
511      *
512      * @return true if the cached repository list is valid since the last check
513      */
514     private boolean isValidRepositoryList() {
515         String newChecksum = getRepositoryListSettingsChecksum();
516         boolean valid = newChecksum.equals(repositoryListSettingsChecksum.get());
517         repositoryListSettingsChecksum.set(newChecksum);
518         if (!valid && settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
519             logger.info("Repository list settings have changed. Clearing repository list cache.");
520             repositoryListCache.clear();
521         }
522         return valid;
523     }
524
525     /**
526      * Returns the list of all repositories available to Gitblit. This method
527      * does not consider user access permissions.
528      *
529      * @return list of all repositories
530      */
531     @Override
532     public List<String> getRepositoryList() {
533         if (repositoryListCache.size() == 0 || !isValidRepositoryList()) {
534             // we are not caching OR we have not yet cached OR the cached list is invalid
535             long startTime = System.currentTimeMillis();
536             List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder,
537                     settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
538                     settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true),
539                     settings.getInteger(Keys.git.searchRecursionDepth, -1),
540                     settings.getStrings(Keys.git.searchExclusions));
541
542             if (!settings.getBoolean(Keys.git.cacheRepositoryList,  true)) {
543                 // we are not caching
544                 StringUtils.sortRepositorynames(repositories);
545                 return repositories;
546             } else {
547                 // we are caching this list
548                 String msg = "{0} repositories identified in {1} msecs";
549                 if (settings.getBoolean(Keys.web.showRepositorySizes, true)) {
550                     // optionally (re)calculate repository sizes
551                     msg = "{0} repositories identified with calculated folder sizes in {1} msecs";
552                 }
553
554                 for (String repository : repositories) {
555                     getRepositoryModel(repository);
556                 }
557
558                 // rebuild fork networks
559                 for (RepositoryModel model : repositoryListCache.values()) {
560                     if (!StringUtils.isEmpty(model.originRepository)) {
44c005 561                         String originKey = getRepositoryKey(model.originRepository);
4a2752 562                         if (repositoryListCache.containsKey(originKey)) {
JM 563                             RepositoryModel origin = repositoryListCache.get(originKey);
95cdba 564                             origin.addFork(model.name);
JM 565                         }
566                     }
567                 }
568
569                 long duration = System.currentTimeMillis() - startTime;
570                 logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration));
571             }
572         }
573
574         // return sorted copy of cached list
575         List<String> list = new ArrayList<String>();
576         for (RepositoryModel model : repositoryListCache.values()) {
577             list.add(model.name);
578         }
579         StringUtils.sortRepositorynames(list);
580         return list;
581     }
582
583     /**
584      * Returns the JGit repository for the specified name.
585      *
586      * @param repositoryName
587      * @return repository or null
588      */
589     @Override
590     public Repository getRepository(String repositoryName) {
591         return getRepository(repositoryName, true);
592     }
593
594     /**
595      * Returns the JGit repository for the specified name.
596      *
44c005 597      * @param name
95cdba 598      * @param logError
JM 599      * @return repository or null
600      */
601     @Override
44c005 602     public Repository getRepository(String name, boolean logError) {
JM 603         String repositoryName = fixRepositoryName(name);
95cdba 604
JM 605         if (isCollectingGarbage(repositoryName)) {
606             logger.warn(MessageFormat.format("Rejecting request for {0}, busy collecting garbage!", repositoryName));
607             return null;
608         }
609
610         File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
611         if (dir == null)
612             return null;
613
614         Repository r = null;
615         try {
616             FileKey key = FileKey.exact(dir, FS.DETECTED);
617             r = RepositoryCache.open(key, true);
618         } catch (IOException e) {
619             if (logError) {
620                 logger.error("GitBlit.getRepository(String) failed to find "
621                         + new File(repositoriesFolder, repositoryName).getAbsolutePath());
622             }
623         }
624         return r;
625     }
626
627     /**
e58e09 628      * Returns the list of all repository models.
JM 629      *
630      * @return list of all repository models
631      */
632     @Override
633     public List<RepositoryModel> getRepositoryModels() {
634         long methodStart = System.currentTimeMillis();
635         List<String> list = getRepositoryList();
636         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
637         for (String repo : list) {
638             RepositoryModel model = getRepositoryModel(repo);
639             if (model != null) {
640                 repositories.add(model);
641             }
642         }
643         long duration = System.currentTimeMillis() - methodStart;
644         logger.info(MessageFormat.format("{0} repository models loaded in {1} msecs", duration));
645         return repositories;
646     }
647
648     /**
95cdba 649      * Returns the list of repository models that are accessible to the user.
JM 650      *
651      * @param user
652      * @return list of repository models accessible to user
653      */
654     @Override
655     public List<RepositoryModel> getRepositoryModels(UserModel user) {
656         long methodStart = System.currentTimeMillis();
657         List<String> list = getRepositoryList();
658         List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
659         for (String repo : list) {
660             RepositoryModel model = getRepositoryModel(user, repo);
661             if (model != null) {
662                 if (!model.hasCommits) {
663                     // only add empty repositories that user can push to
664                     if (UserModel.ANONYMOUS.canPush(model)
665                             || user != null && user.canPush(model)) {
666                         repositories.add(model);
667                     }
668                 } else {
669                     repositories.add(model);
670                 }
671             }
672         }
673         long duration = System.currentTimeMillis() - methodStart;
674         logger.info(MessageFormat.format("{0} repository models loaded for {1} in {2} msecs",
675                 repositories.size(), user == null ? "anonymous" : user.username, duration));
676         return repositories;
677     }
678
679     /**
680      * Returns a repository model if the repository exists and the user may
681      * access the repository.
682      *
683      * @param user
684      * @param repositoryName
685      * @return repository model or null
686      */
687     @Override
688     public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
689         RepositoryModel model = getRepositoryModel(repositoryName);
690         if (model == null) {
691             return null;
692         }
693         if (user == null) {
694             user = UserModel.ANONYMOUS;
695         }
696         if (user.canView(model)) {
697             return model;
698         }
699         return null;
700     }
701
702     /**
703      * Returns the repository model for the specified repository. This method
704      * does not consider user access permissions.
705      *
44c005 706      * @param name
95cdba 707      * @return repository model or null
JM 708      */
709     @Override
44c005 710     public RepositoryModel getRepositoryModel(String name) {
JM 711         String repositoryName = fixRepositoryName(name);
95cdba 712
44c005 713         String repositoryKey = getRepositoryKey(repositoryName);
889bb7 714         if (!repositoryListCache.containsKey(repositoryKey)) {
95cdba 715             RepositoryModel model = loadRepositoryModel(repositoryName);
JM 716             if (model == null) {
717                 return null;
718             }
719             addToCachedRepositoryList(model);
720             return DeepCopier.copy(model);
721         }
722
723         // cached model
889bb7 724         RepositoryModel model = repositoryListCache.get(repositoryKey);
95cdba 725
396e9b 726         if (isCollectingGarbage(model.name)) {
95cdba 727             // Gitblit is busy collecting garbage, use our cached model
JM 728             RepositoryModel rm = DeepCopier.copy(model);
729             rm.isCollectingGarbage = true;
730             return rm;
731         }
732
733         // check for updates
734         Repository r = getRepository(model.name);
735         if (r == null) {
736             // repository is missing
737             removeFromCachedRepositoryList(repositoryName);
738             logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName));
739             return null;
740         }
741
742         FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r);
743         if (config.isOutdated()) {
744             // reload model
745             logger.debug(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
746             model = loadRepositoryModel(model.name);
747             removeFromCachedRepositoryList(model.name);
748             addToCachedRepositoryList(model);
749         } else {
750             // update a few repository parameters
751             if (!model.hasCommits) {
752                 // update hasCommits, assume a repository only gains commits :)
753                 model.hasCommits = JGitUtils.hasCommits(r);
754             }
755
756             updateLastChangeFields(r, model);
757         }
758         r.close();
759
760         // return a copy of the cached model
761         return DeepCopier.copy(model);
762     }
763
764     /**
765      * Returns the star count of the repository.
766      *
767      * @param repository
768      * @return the star count
769      */
770     @Override
771     public long getStarCount(RepositoryModel repository) {
772         long count = 0;
773         for (UserModel user : userManager.getAllUsers()) {
774             if (user.getPreferences().isStarredRepository(repository.name)) {
775                 count++;
776             }
777         }
778         return count;
44c005 779     }
JM 780
781     /**
782      * Replaces illegal character patterns in a repository name.
783      *
784      * @param repositoryName
785      * @return a corrected name
786      */
787     private String fixRepositoryName(String repositoryName) {
788         if (StringUtils.isEmpty(repositoryName)) {
789             return repositoryName;
790         }
791
792         // Decode url-encoded repository name (issue-278)
793         // http://stackoverflow.com/questions/17183110
794         String name  = repositoryName.replace("%7E", "~").replace("%7e", "~");
795         name = name.replace("%2F", "/").replace("%2f", "/");
796
797         if (name.charAt(name.length() - 1) == '/') {
798             name = name.substring(0, name.length() - 1);
799         }
800
801         // strip duplicate-slashes from requests for repositoryName (ticket-117, issue-454)
802         // specify first char as slash so we strip leading slashes
803         char lastChar = '/';
804         StringBuilder sb = new StringBuilder();
805         for (char c : name.toCharArray()) {
806             if (c == '/' && lastChar == c) {
807                 continue;
808             }
809             sb.append(c);
810             lastChar = c;
811         }
812
813         return sb.toString();
814     }
815
816     /**
817      * Returns the cache key for the repository name.
818      *
819      * @param repositoryName
820      * @return the cache key for the repository
821      */
822     private String getRepositoryKey(String repositoryName) {
823         String name = fixRepositoryName(repositoryName);
824         return StringUtils.stripDotGit(name).toLowerCase();
95cdba 825     }
JM 826
827     /**
828      * Workaround JGit.  I need to access the raw config object directly in order
829      * to see if the config is dirty so that I can reload a repository model.
830      * If I use the stock JGit method to get the config it already reloads the
831      * config.  If the config changes are made within Gitblit this is fine as
832      * the returned config will still be flagged as dirty.  BUT... if the config
833      * is manipulated outside Gitblit then it fails to recognize this as dirty.
834      *
835      * @param r
836      * @return a config
837      */
838     private StoredConfig getRepositoryConfig(Repository r) {
839         try {
840             Field f = r.getClass().getDeclaredField("repoConfig");
841             f.setAccessible(true);
842             StoredConfig config = (StoredConfig) f.get(r);
843             return config;
844         } catch (Exception e) {
845             logger.error("Failed to retrieve \"repoConfig\" via reflection", e);
846         }
847         return r.getConfig();
848     }
849
850     /**
851      * Create a repository model from the configuration and repository data.
852      *
853      * @param repositoryName
854      * @return a repositoryModel or null if the repository does not exist
855      */
856     private RepositoryModel loadRepositoryModel(String repositoryName) {
857         Repository r = getRepository(repositoryName);
858         if (r == null) {
859             return null;
860         }
861         RepositoryModel model = new RepositoryModel();
862         model.isBare = r.isBare();
863         File basePath = getRepositoriesFolder();
864         if (model.isBare) {
865             model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
866         } else {
867             model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory().getParentFile());
868         }
869         if (StringUtils.isEmpty(model.name)) {
870             // Repository is NOT located relative to the base folder because it
871             // is symlinked.  Use the provided repository name.
872             model.name = repositoryName;
873         }
874         model.projectPath = StringUtils.getFirstPathElement(repositoryName);
875
876         StoredConfig config = r.getConfig();
2546ea 877         boolean hasOrigin = false;
95cdba 878
JM 879         if (config != null) {
880             // Initialize description from description file
2546ea 881             hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
95cdba 882             if (getConfig(config,"description", null) == null) {
JM 883                 File descFile = new File(r.getDirectory(), "description");
884                 if (descFile.exists()) {
885                     String desc = com.gitblit.utils.FileUtils.readContent(descFile, System.getProperty("line.separator"));
886                     if (!desc.toLowerCase().startsWith("unnamed repository")) {
887                         config.setString(Constants.CONFIG_GITBLIT, null, "description", desc);
888                     }
889                 }
890             }
891             model.description = getConfig(config, "description", "");
892             model.originRepository = getConfig(config, "originRepository", null);
893             model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", "")));
5e3521 894             model.acceptNewPatchsets = getConfig(config, "acceptNewPatchsets", true);
JM 895             model.acceptNewTickets = getConfig(config, "acceptNewTickets", true);
896             model.requireApproval = getConfig(config, "requireApproval", settings.getBoolean(Keys.tickets.requireApproval, false));
f1b882 897             model.mergeTo = getConfig(config, "mergeTo", null);
95cdba 898             model.useIncrementalPushTags = getConfig(config, "useIncrementalPushTags", false);
JM 899             model.incrementalPushTagPrefix = getConfig(config, "incrementalPushTagPrefix", null);
900             model.allowForks = getConfig(config, "allowForks", true);
901             model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
902                     "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, "PUSH")));
903             model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
904                     "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
905             model.verifyCommitter = getConfig(config, "verifyCommitter", false);
906             model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
907             model.isFrozen = getConfig(config, "isFrozen", false);
908             model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
909             model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
910             model.commitMessageRenderer = CommitMessageRenderer.fromName(getConfig(config, "commitMessageRenderer",
911                     settings.getString(Keys.web.commitMessageRenderer, null)));
912             model.federationStrategy = FederationStrategy.fromName(getConfig(config,
913                     "federationStrategy", null));
914             model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
915                     Constants.CONFIG_GITBLIT, null, "federationSets")));
916             model.isFederated = getConfig(config, "isFederated", false);
917             model.gcThreshold = getConfig(config, "gcThreshold", settings.getString(Keys.git.defaultGarbageCollectionThreshold, "500KB"));
918             model.gcPeriod = getConfig(config, "gcPeriod", settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7));
919             try {
920                 model.lastGC = new SimpleDateFormat(Constants.ISO8601).parse(getConfig(config, "lastGC", "1970-01-01'T'00:00:00Z"));
921             } catch (Exception e) {
922                 model.lastGC = new Date(0);
923             }
924             model.maxActivityCommits = getConfig(config, "maxActivityCommits", settings.getInteger(Keys.web.maxActivityCommits, 0));
925             model.origin = config.getString("remote", "origin", "url");
926             if (model.origin != null) {
927                 model.origin = model.origin.replace('\\', '/');
928                 model.isMirror = config.getBoolean("remote", "origin", "mirror", false);
929             }
930             model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
931                     Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
932             model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
933                     Constants.CONFIG_GITBLIT, null, "postReceiveScript")));
934             model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
935                     Constants.CONFIG_GITBLIT, null, "mailingList")));
936             model.indexedBranches = new ArrayList<String>(Arrays.asList(config.getStringList(
937                     Constants.CONFIG_GITBLIT, null, "indexBranch")));
938             model.metricAuthorExclusions = new ArrayList<String>(Arrays.asList(config.getStringList(
939                     Constants.CONFIG_GITBLIT, null, "metricAuthorExclusions")));
940
941             // Custom defined properties
942             model.customFields = new LinkedHashMap<String, String>();
943             for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) {
944                 model.customFields.put(aProperty, config.getString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, aProperty));
945             }
946         }
947         model.HEAD = JGitUtils.getHEADRef(r);
f1b882 948         if (StringUtils.isEmpty(model.mergeTo)) {
JM 949             model.mergeTo = model.HEAD;
950         }
95cdba 951         model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
JM 952         model.sparkleshareId = JGitUtils.getSparkleshareId(r);
953         model.hasCommits = JGitUtils.hasCommits(r);
954         updateLastChangeFields(r, model);
955         r.close();
956
957         if (StringUtils.isEmpty(model.originRepository) && model.origin != null && model.origin.startsWith("file://")) {
958             // repository was cloned locally... perhaps as a fork
959             try {
960                 File folder = new File(new URI(model.origin));
961                 String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
962                 if (!StringUtils.isEmpty(originRepo)) {
963                     // ensure origin still exists
964                     File repoFolder = new File(getRepositoriesFolder(), originRepo);
965                     if (repoFolder.exists()) {
966                         model.originRepository = originRepo.toLowerCase();
967
968                         // persist the fork origin
969                         updateConfiguration(r, model);
970                     }
971                 }
972             } catch (URISyntaxException e) {
973                 logger.error("Failed to determine fork for " + model, e);
974             }
975         }
976         return model;
977     }
978
979     /**
980      * Determines if this server has the requested repository.
981      *
982      * @param n
983      * @return true if the repository exists
984      */
985     @Override
986     public boolean hasRepository(String repositoryName) {
987         return hasRepository(repositoryName, false);
988     }
989
990     /**
991      * Determines if this server has the requested repository.
992      *
993      * @param n
994      * @param caseInsensitive
995      * @return true if the repository exists
996      */
997     @Override
998     public boolean hasRepository(String repositoryName, boolean caseSensitiveCheck) {
999         if (!caseSensitiveCheck && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
1000             // if we are caching use the cache to determine availability
1001             // otherwise we end up adding a phantom repository to the cache
44c005 1002             String key = getRepositoryKey(repositoryName);
JM 1003             return repositoryListCache.containsKey(key);
95cdba 1004         }
JM 1005         Repository r = getRepository(repositoryName, false);
1006         if (r == null) {
1007             return false;
1008         }
1009         r.close();
1010         return true;
1011     }
1012
1013     /**
1014      * Determines if the specified user has a fork of the specified origin
1015      * repository.
1016      *
1017      * @param username
1018      * @param origin
1019      * @return true the if the user has a fork
1020      */
1021     @Override
1022     public boolean hasFork(String username, String origin) {
1023         return getFork(username, origin) != null;
1024     }
1025
1026     /**
1027      * Gets the name of a user's fork of the specified origin
1028      * repository.
1029      *
1030      * @param username
1031      * @param origin
1032      * @return the name of the user's fork, null otherwise
1033      */
1034     @Override
1035     public String getFork(String username, String origin) {
4a2752 1036         if (StringUtils.isEmpty(origin)) {
JM 1037             return null;
1038         }
95cdba 1039         String userProject = ModelUtils.getPersonalPath(username);
JM 1040         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
44c005 1041             String originKey = getRepositoryKey(origin);
95cdba 1042             String userPath = userProject + "/";
JM 1043
1044             // collect all origin nodes in fork network
1045             Set<String> roots = new HashSet<String>();
4a2752 1046             roots.add(originKey);
JM 1047             RepositoryModel originModel = repositoryListCache.get(originKey);
95cdba 1048             while (originModel != null) {
JM 1049                 if (!ArrayUtils.isEmpty(originModel.forks)) {
1050                     for (String fork : originModel.forks) {
1051                         if (!fork.startsWith(userPath)) {
4a2752 1052                             roots.add(fork.toLowerCase());
95cdba 1053                         }
JM 1054                     }
1055                 }
1056
1057                 if (originModel.originRepository != null) {
44c005 1058                     String ooKey = getRepositoryKey(originModel.originRepository);
4a2752 1059                     roots.add(ooKey);
JM 1060                     originModel = repositoryListCache.get(ooKey);
95cdba 1061                 } else {
JM 1062                     // break
1063                     originModel = null;
1064                 }
1065             }
1066
1067             for (String repository : repositoryListCache.keySet()) {
1068                 if (repository.startsWith(userPath)) {
1069                     RepositoryModel model = repositoryListCache.get(repository);
1070                     if (!StringUtils.isEmpty(model.originRepository)) {
44c005 1071                         String ooKey = getRepositoryKey(model.originRepository);
JM 1072                         if (roots.contains(ooKey)) {
95cdba 1073                             // user has a fork in this graph
JM 1074                             return model.name;
1075                         }
1076                     }
1077                 }
1078             }
1079         } else {
1080             // not caching
1081             File subfolder = new File(getRepositoriesFolder(), userProject);
1082             List<String> repositories = JGitUtils.getRepositoryList(subfolder,
1083                     settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
1084                     settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true),
1085                     settings.getInteger(Keys.git.searchRecursionDepth, -1),
1086                     settings.getStrings(Keys.git.searchExclusions));
1087             for (String repository : repositories) {
1088                 RepositoryModel model = getRepositoryModel(userProject + "/" + repository);
b57d3e 1089                 if (model.originRepository != null && model.originRepository.equalsIgnoreCase(origin)) {
95cdba 1090                     // user has a fork
JM 1091                     return model.name;
1092                 }
1093             }
1094         }
1095         // user does not have a fork
1096         return null;
1097     }
1098
1099     /**
1100      * Returns the fork network for a repository by traversing up the fork graph
1101      * to discover the root and then down through all children of the root node.
1102      *
1103      * @param repository
1104      * @return a ForkModel
1105      */
1106     @Override
1107     public ForkModel getForkNetwork(String repository) {
1108         if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
1109             // find the root, cached
44c005 1110             String key = getRepositoryKey(repository);
JM 1111             RepositoryModel model = repositoryListCache.get(key);
95cdba 1112             while (model.originRepository != null) {
44c005 1113                 String originKey = getRepositoryKey(model.originRepository);
JM 1114                 model = repositoryListCache.get(originKey);
95cdba 1115             }
JM 1116             ForkModel root = getForkModelFromCache(model.name);
1117             return root;
1118         } else {
1119             // find the root, non-cached
1120             RepositoryModel model = getRepositoryModel(repository.toLowerCase());
1121             while (model.originRepository != null) {
1122                 model = getRepositoryModel(model.originRepository);
1123             }
1124             ForkModel root = getForkModel(model.name);
1125             return root;
1126         }
1127     }
1128
1129     private ForkModel getForkModelFromCache(String repository) {
44c005 1130         String key = getRepositoryKey(repository);
JM 1131         RepositoryModel model = repositoryListCache.get(key);
95cdba 1132         if (model == null) {
JM 1133             return null;
1134         }
1135         ForkModel fork = new ForkModel(model);
1136         if (!ArrayUtils.isEmpty(model.forks)) {
1137             for (String aFork : model.forks) {
1138                 ForkModel fm = getForkModelFromCache(aFork);
1139                 if (fm != null) {
1140                     fork.forks.add(fm);
1141                 }
1142             }
1143         }
1144         return fork;
1145     }
1146
1147     private ForkModel getForkModel(String repository) {
1148         RepositoryModel model = getRepositoryModel(repository.toLowerCase());
1149         if (model == null) {
1150             return null;
1151         }
1152         ForkModel fork = new ForkModel(model);
1153         if (!ArrayUtils.isEmpty(model.forks)) {
1154             for (String aFork : model.forks) {
1155                 ForkModel fm = getForkModel(aFork);
1156                 if (fm != null) {
1157                     fork.forks.add(fm);
1158                 }
1159             }
1160         }
1161         return fork;
1162     }
1163
1164     /**
1165      * Updates the last changed fields and optionally calculates the size of the
1166      * repository.  Gitblit caches the repository sizes to reduce the performance
1167      * penalty of recursive calculation. The cache is updated if the repository
1168      * has been changed since the last calculation.
1169      *
1170      * @param model
1171      * @return size in bytes of the repository
1172      */
1173     @Override
1174     public long updateLastChangeFields(Repository r, RepositoryModel model) {
1175         LastChange lc = JGitUtils.getLastChange(r);
1176         model.lastChange = lc.when;
1177         model.lastChangeAuthor = lc.who;
1178
1179         if (!settings.getBoolean(Keys.web.showRepositorySizes, true) || model.skipSizeCalculation) {
1180             model.size = null;
1181             return 0L;
1182         }
1183         if (!repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
1184             File gitDir = r.getDirectory();
1185             long sz = com.gitblit.utils.FileUtils.folderSize(gitDir);
1186             repositorySizeCache.updateObject(model.name, model.lastChange, sz);
1187         }
1188         long size = repositorySizeCache.getObject(model.name);
1189         ByteFormat byteFormat = new ByteFormat();
1190         model.size = byteFormat.format(size);
1191         return size;
1192     }
1193
1194     /**
388a23 1195      * Returns true if the repository is idle (not being accessed).
JM 1196      *
1197      * @param repository
1198      * @return true if the repository is idle
1199      */
1200     @Override
1201     public boolean isIdle(Repository repository) {
1202         try {
1203             // Read the use count.
1204             // An idle use count is 2:
1205             // +1 for being in the cache
1206             // +1 for the repository parameter in this method
1207             Field useCnt = Repository.class.getDeclaredField("useCnt");
1208             useCnt.setAccessible(true);
1209             int useCount = ((AtomicInteger) useCnt.get(repository)).get();
1210             return useCount == 2;
1211         } catch (Exception e) {
1212             logger.warn(MessageFormat
1213                     .format("Failed to reflectively determine use count for repository {0}",
1214                             repository.getDirectory().getPath()), e);
1215         }
1216         return false;
1217     }
1218
1219     /**
1220      * Ensures that all cached repository are completely closed and their resources
1221      * are properly released.
1222      */
1223     @Override
1224     public void closeAll() {
1225         for (String repository : getRepositoryList()) {
1226             close(repository);
1227         }
1228     }
1229
1230     /**
95cdba 1231      * Ensure that a cached repository is completely closed and its resources
JM 1232      * are properly released.
1233      *
1234      * @param repositoryName
1235      */
388a23 1236     @Override
JM 1237     public void close(String repositoryName) {
95cdba 1238         Repository repository = getRepository(repositoryName);
JM 1239         if (repository == null) {
1240             return;
1241         }
1242         RepositoryCache.close(repository);
1243
1244         // assume 2 uses in case reflection fails
1245         int uses = 2;
1246         try {
1247             // The FileResolver caches repositories which is very useful
1248             // for performance until you want to delete a repository.
1249             // I have to use reflection to call close() the correct
1250             // number of times to ensure that the object and ref databases
1251             // are properly closed before I can delete the repository from
1252             // the filesystem.
1253             Field useCnt = Repository.class.getDeclaredField("useCnt");
1254             useCnt.setAccessible(true);
1255             uses = ((AtomicInteger) useCnt.get(repository)).get();
1256         } catch (Exception e) {
1257             logger.warn(MessageFormat
1258                     .format("Failed to reflectively determine use count for repository {0}",
1259                             repositoryName), e);
1260         }
1261         if (uses > 0) {
388a23 1262             logger.debug(MessageFormat
95cdba 1263                     .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
JM 1264                             repositoryName, uses, uses));
1265             for (int i = 0; i < uses; i++) {
1266                 repository.close();
1267             }
1268         }
1269
1270         // close any open index writer/searcher in the Lucene executor
1271         luceneExecutor.close(repositoryName);
1272     }
1273
1274     /**
1275      * Returns the metrics for the default branch of the specified repository.
1276      * This method builds a metrics cache. The cache is updated if the
1277      * repository is updated. A new copy of the metrics list is returned on each
1278      * call so that modifications to the list are non-destructive.
1279      *
1280      * @param model
1281      * @param repository
1282      * @return a new array list of metrics
1283      */
1284     @Override
1285     public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
1286         if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
1287             return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
1288         }
1289         List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null, runtimeManager.getTimezone());
1290         repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
1291         return new ArrayList<Metric>(metrics);
1292     }
1293
1294     /**
1295      * Returns the gitblit string value for the specified key. If key is not
1296      * set, returns defaultValue.
1297      *
1298      * @param config
1299      * @param field
1300      * @param defaultValue
1301      * @return field value or defaultValue
1302      */
1303     private String getConfig(StoredConfig config, String field, String defaultValue) {
1304         String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
1305         if (StringUtils.isEmpty(value)) {
1306             return defaultValue;
1307         }
1308         return value;
1309     }
1310
1311     /**
1312      * Returns the gitblit boolean value for the specified key. If key is not
1313      * set, returns defaultValue.
1314      *
1315      * @param config
1316      * @param field
1317      * @param defaultValue
1318      * @return field value or defaultValue
1319      */
1320     private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
1321         return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue);
1322     }
1323
1324     /**
1325      * Returns the gitblit string value for the specified key. If key is not
1326      * set, returns defaultValue.
1327      *
1328      * @param config
1329      * @param field
1330      * @param defaultValue
1331      * @return field value or defaultValue
1332      */
1333     private int getConfig(StoredConfig config, String field, int defaultValue) {
1334         String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
1335         if (StringUtils.isEmpty(value)) {
1336             return defaultValue;
1337         }
1338         try {
1339             return Integer.parseInt(value);
1340         } catch (Exception e) {
1341         }
1342         return defaultValue;
1343     }
1344
1345     /**
1346      * Creates/updates the repository model keyed by reopsitoryName. Saves all
1347      * repository settings in .git/config. This method allows for renaming
1348      * repositories and will update user access permissions accordingly.
1349      *
1350      * All repositories created by this method are bare and automatically have
1351      * .git appended to their names, which is the standard convention for bare
1352      * repositories.
1353      *
1354      * @param repositoryName
1355      * @param repository
1356      * @param isCreate
1357      * @throws GitBlitException
1358      */
1359     @Override
1360     public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
1361             boolean isCreate) throws GitBlitException {
396e9b 1362         if (isCollectingGarbage(repositoryName)) {
95cdba 1363             throw new GitBlitException(MessageFormat.format("sorry, Gitblit is busy collecting garbage in {0}",
JM 1364                     repositoryName));
1365         }
1366         Repository r = null;
1367         String projectPath = StringUtils.getFirstPathElement(repository.name);
1368         if (!StringUtils.isEmpty(projectPath)) {
1369             if (projectPath.equalsIgnoreCase(settings.getString(Keys.web.repositoryRootGroupName, "main"))) {
1370                 // strip leading group name
1371                 repository.name = repository.name.substring(projectPath.length() + 1);
1372             }
1373         }
1374         if (isCreate) {
1375             // ensure created repository name ends with .git
1376             if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
1377                 repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
1378             }
1379             if (hasRepository(repository.name)) {
1380                 throw new GitBlitException(MessageFormat.format(
1381                         "Can not create repository ''{0}'' because it already exists.",
1382                         repository.name));
1383             }
1384             // create repository
1385             logger.info("create repository " + repository.name);
1386             String shared = settings.getString(Keys.git.createRepositoriesShared, "FALSE");
1387             r = JGitUtils.createRepository(repositoriesFolder, repository.name, shared);
1388         } else {
1389             // rename repository
1390             if (!repositoryName.equalsIgnoreCase(repository.name)) {
1391                 if (!repository.name.toLowerCase().endsWith(
1392                         org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
1393                     repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
1394                 }
1395                 if (new File(repositoriesFolder, repository.name).exists()) {
1396                     throw new GitBlitException(MessageFormat.format(
1397                             "Failed to rename ''{0}'' because ''{1}'' already exists.",
1398                             repositoryName, repository.name));
1399                 }
388a23 1400                 close(repositoryName);
95cdba 1401                 File folder = new File(repositoriesFolder, repositoryName);
JM 1402                 File destFolder = new File(repositoriesFolder, repository.name);
1403                 if (destFolder.exists()) {
1404                     throw new GitBlitException(
1405                             MessageFormat
1406                                     .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
1407                                             repositoryName, repository.name));
1408                 }
1409                 File parentFile = destFolder.getParentFile();
1410                 if (!parentFile.exists() && !parentFile.mkdirs()) {
1411                     throw new GitBlitException(MessageFormat.format(
1412                             "Failed to create folder ''{0}''", parentFile.getAbsolutePath()));
1413                 }
1414                 if (!folder.renameTo(destFolder)) {
1415                     throw new GitBlitException(MessageFormat.format(
1416                             "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
1417                             repository.name));
1418                 }
1419                 // rename the roles
1420                 if (!userManager.renameRepositoryRole(repositoryName, repository.name)) {
1421                     throw new GitBlitException(MessageFormat.format(
1422                             "Failed to rename repository permissions ''{0}'' to ''{1}''.",
1423                             repositoryName, repository.name));
1424                 }
1425
1426                 // rename fork origins in their configs
1427                 if (!ArrayUtils.isEmpty(repository.forks)) {
1428                     for (String fork : repository.forks) {
1429                         Repository rf = getRepository(fork);
1430                         try {
1431                             StoredConfig config = rf.getConfig();
1432                             String origin = config.getString("remote", "origin", "url");
1433                             origin = origin.replace(repositoryName, repository.name);
1434                             config.setString("remote", "origin", "url", origin);
1435                             config.setString(Constants.CONFIG_GITBLIT, null, "originRepository", repository.name);
1436                             config.save();
1437                         } catch (Exception e) {
1438                             logger.error("Failed to update repository fork config for " + fork, e);
1439                         }
1440                         rf.close();
1441                     }
1442                 }
1443
1444                 // update this repository's origin's fork list
1445                 if (!StringUtils.isEmpty(repository.originRepository)) {
44c005 1446                     String originKey = getRepositoryKey(repository.originRepository);
JM 1447                     RepositoryModel origin = repositoryListCache.get(originKey);
95cdba 1448                     if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
JM 1449                         origin.forks.remove(repositoryName);
1450                         origin.forks.add(repository.name);
1451                     }
1452                 }
1453
1454                 // clear the cache
1455                 clearRepositoryMetadataCache(repositoryName);
1456                 repository.resetDisplayName();
1457             }
1458
1459             // load repository
1460             logger.info("edit repository " + repository.name);
1461             r = getRepository(repository.name);
1462         }
1463
1464         // update settings
1465         if (r != null) {
1466             updateConfiguration(r, repository);
1467             // Update the description file
1468             File descFile = new File(r.getDirectory(), "description");
1469             if (repository.description != null)
1470             {
1471                 com.gitblit.utils.FileUtils.writeContent(descFile, repository.description);
1472             }
1473             else if (descFile.exists() && !descFile.isDirectory()) {
1474                 descFile.delete();
1475             }
1476             // only update symbolic head if it changes
1477             String currentRef = JGitUtils.getHEADRef(r);
1478             if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) {
1479                 logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}",
1480                         repository.name, currentRef, repository.HEAD));
1481                 if (JGitUtils.setHEADtoRef(r, repository.HEAD)) {
1482                     // clear the cache
1483                     clearRepositoryMetadataCache(repository.name);
1484                 }
1485             }
1486
1487             // Adjust permissions in case we updated the config files
1488             JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "config"),
1489                     settings.getString(Keys.git.createRepositoriesShared, "FALSE"));
1490             JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "HEAD"),
1491                     settings.getString(Keys.git.createRepositoriesShared, "FALSE"));
1492
1493             // close the repository object
1494             r.close();
1495         }
1496
1497         // update repository cache
1498         removeFromCachedRepositoryList(repositoryName);
1499         // model will actually be replaced on next load because config is stale
1500         addToCachedRepositoryList(repository);
ca4d98 1501
JM 1502         if (isCreate && pluginManager != null) {
1503             for (RepositoryLifeCycleListener listener : pluginManager.getExtensions(RepositoryLifeCycleListener.class)) {
1504                 try {
1505                     listener.onCreation(repository);
1506                 } catch (Throwable t) {
1507                     logger.error(String.format("failed to call plugin onCreation %s", repositoryName), t);
1508                 }
1509             }
1510         }
95cdba 1511     }
JM 1512
1513     /**
1514      * Updates the Gitblit configuration for the specified repository.
1515      *
1516      * @param r
1517      *            the Git repository
1518      * @param repository
1519      *            the Gitblit repository model
1520      */
1521     @Override
1522     public void updateConfiguration(Repository r, RepositoryModel repository) {
1523         StoredConfig config = r.getConfig();
1524         config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
1525         config.setString(Constants.CONFIG_GITBLIT, null, "originRepository", repository.originRepository);
1526         config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners));
5e3521 1527         config.setBoolean(Constants.CONFIG_GITBLIT, null, "acceptNewPatchsets", repository.acceptNewPatchsets);
JM 1528         config.setBoolean(Constants.CONFIG_GITBLIT, null, "acceptNewTickets", repository.acceptNewTickets);
1529         if (settings.getBoolean(Keys.tickets.requireApproval, false) == repository.requireApproval) {
1530             // use default
1531             config.unset(Constants.CONFIG_GITBLIT, null, "requireApproval");
1532         } else {
1533             // override default
1534             config.setBoolean(Constants.CONFIG_GITBLIT, null, "requireApproval", repository.requireApproval);
1535         }
f1b882 1536         if (!StringUtils.isEmpty(repository.mergeTo)) {
JM 1537             config.setString(Constants.CONFIG_GITBLIT, null, "mergeTo", repository.mergeTo);
1538         }
95cdba 1539         config.setBoolean(Constants.CONFIG_GITBLIT, null, "useIncrementalPushTags", repository.useIncrementalPushTags);
JM 1540         if (StringUtils.isEmpty(repository.incrementalPushTagPrefix) ||
1541                 repository.incrementalPushTagPrefix.equals(settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r"))) {
1542             config.unset(Constants.CONFIG_GITBLIT, null, "incrementalPushTagPrefix");
1543         } else {
1544             config.setString(Constants.CONFIG_GITBLIT, null, "incrementalPushTagPrefix", repository.incrementalPushTagPrefix);
1545         }
1546         config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
1547         config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
1548         config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
1549         config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);
1550         config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
1551         config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);
1552         config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSizeCalculation", repository.skipSizeCalculation);
1553         config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSummaryMetrics", repository.skipSummaryMetrics);
1554         config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy",
1555                 repository.federationStrategy.name());
1556         config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated);
1557         config.setString(Constants.CONFIG_GITBLIT, null, "gcThreshold", repository.gcThreshold);
1558         if (repository.gcPeriod == settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7)) {
1559             // use default from config
1560             config.unset(Constants.CONFIG_GITBLIT, null, "gcPeriod");
1561         } else {
1562             config.setInt(Constants.CONFIG_GITBLIT, null, "gcPeriod", repository.gcPeriod);
1563         }
1564         if (repository.lastGC != null) {
1565             config.setString(Constants.CONFIG_GITBLIT, null, "lastGC", new SimpleDateFormat(Constants.ISO8601).format(repository.lastGC));
1566         }
1567         if (repository.maxActivityCommits == settings.getInteger(Keys.web.maxActivityCommits, 0)) {
1568             // use default from config
1569             config.unset(Constants.CONFIG_GITBLIT, null, "maxActivityCommits");
1570         } else {
1571             config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits);
1572         }
1573
1574         CommitMessageRenderer defaultRenderer = CommitMessageRenderer.fromName(settings.getString(Keys.web.commitMessageRenderer, null));
1575         if (repository.commitMessageRenderer == null || repository.commitMessageRenderer == defaultRenderer) {
1576             // use default from config
1577             config.unset(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer");
1578         } else {
1579             // repository overrides default
1580             config.setString(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer",
1581                     repository.commitMessageRenderer.name());
1582         }
1583
1584         updateList(config, "federationSets", repository.federationSets);
1585         updateList(config, "preReceiveScript", repository.preReceiveScripts);
1586         updateList(config, "postReceiveScript", repository.postReceiveScripts);
1587         updateList(config, "mailingList", repository.mailingLists);
1588         updateList(config, "indexBranch", repository.indexedBranches);
1589         updateList(config, "metricAuthorExclusions", repository.metricAuthorExclusions);
1590
1591         // User Defined Properties
1592         if (repository.customFields != null) {
1593             if (repository.customFields.size() == 0) {
1594                 // clear section
1595                 config.unsetSection(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS);
1596             } else {
1597                 for (Entry<String, String> property : repository.customFields.entrySet()) {
1598                     // set field
1599                     String key = property.getKey();
1600                     String value = property.getValue();
1601                     config.setString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, key, value);
1602                 }
1603             }
1604         }
1605
1606         try {
1607             config.save();
1608         } catch (IOException e) {
1609             logger.error("Failed to save repository config!", e);
1610         }
1611     }
1612
1613     private void updateList(StoredConfig config, String field, List<String> list) {
1614         // a null list is skipped, not cleared
1615         // this is for RPC administration where an older manager might be used
1616         if (list == null) {
1617             return;
1618         }
1619         if (ArrayUtils.isEmpty(list)) {
1620             config.unset(Constants.CONFIG_GITBLIT, null, field);
1621         } else {
1622             config.setStringList(Constants.CONFIG_GITBLIT, null, field, list);
1623         }
1624     }
1625
1626     /**
b4ed66 1627      * Returns true if the repository can be deleted.
JM 1628      *
1629      * @return true if the repository can be deleted
1630      */
1631     @Override
1632     public boolean canDelete(RepositoryModel repository) {
1633         return settings.getBoolean(Keys.web.allowDeletingNonEmptyRepositories, true)
1634                     || !repository.hasCommits;
1635     }
1636
1637     /**
95cdba 1638      * Deletes the repository from the file system and removes the repository
JM 1639      * permission from all repository users.
1640      *
1641      * @param model
1642      * @return true if successful
1643      */
1644     @Override
1645     public boolean deleteRepositoryModel(RepositoryModel model) {
1646         return deleteRepository(model.name);
1647     }
1648
1649     /**
1650      * Deletes the repository from the file system and removes the repository
1651      * permission from all repository users.
1652      *
1653      * @param repositoryName
1654      * @return true if successful
1655      */
1656     @Override
1657     public boolean deleteRepository(String repositoryName) {
b4ed66 1658         RepositoryModel repository = getRepositoryModel(repositoryName);
JM 1659         if (!canDelete(repository)) {
1660             logger.warn("Attempt to delete {} rejected!", repositoryName);
1661             return false;
1662         }
1663
95cdba 1664         try {
388a23 1665             close(repositoryName);
95cdba 1666             // clear the repository cache
JM 1667             clearRepositoryMetadataCache(repositoryName);
1668
1669             RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
1670             if (model != null && !ArrayUtils.isEmpty(model.forks)) {
1671                 resetRepositoryListCache();
1672             }
1673
1674             File folder = new File(repositoriesFolder, repositoryName);
1675             if (folder.exists() && folder.isDirectory()) {
1676                 FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
1677                 if (userManager.deleteRepositoryRole(repositoryName)) {
1678                     logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
ca4d98 1679
JM 1680                     if (pluginManager != null) {
1681                         for (RepositoryLifeCycleListener listener : pluginManager.getExtensions(RepositoryLifeCycleListener.class)) {
1682                             try {
1683                                 listener.onDeletion(repository);
1684                             } catch (Throwable t) {
1685                                 logger.error(String.format("failed to call plugin onDeletion %s", repositoryName), t);
1686                             }
1687                         }
1688                     }
95cdba 1689                     return true;
JM 1690                 }
1691             }
1692         } catch (Throwable t) {
1693             logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
1694         }
1695         return false;
1696     }
1697
1698     /**
1699      * Returns the list of all Groovy push hook scripts. Script files must have
1700      * .groovy extension
1701      *
1702      * @return list of available hook scripts
1703      */
1704     @Override
1705     public List<String> getAllScripts() {
1706         File groovyFolder = getHooksFolder();
1707         File[] files = groovyFolder.listFiles(new FileFilter() {
1708             @Override
1709             public boolean accept(File pathname) {
1710                 return pathname.isFile() && pathname.getName().endsWith(".groovy");
1711             }
1712         });
1713         List<String> scripts = new ArrayList<String>();
1714         if (files != null) {
1715             for (File file : files) {
1716                 String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
1717                 scripts.add(script);
1718             }
1719         }
1720         return scripts;
1721     }
1722
1723     /**
1724      * Returns the list of pre-receive scripts the repository inherited from the
1725      * global settings and team affiliations.
1726      *
1727      * @param repository
1728      *            if null only the globally specified scripts are returned
1729      * @return a list of scripts
1730      */
1731     @Override
1732     public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
1733         Set<String> scripts = new LinkedHashSet<String>();
1734         // Globals
1735         for (String script : settings.getStrings(Keys.groovy.preReceiveScripts)) {
1736             if (script.endsWith(".groovy")) {
1737                 scripts.add(script.substring(0, script.lastIndexOf('.')));
1738             } else {
1739                 scripts.add(script);
1740             }
1741         }
1742
1743         // Team Scripts
1744         if (repository != null) {
1745             for (String teamname : userManager.getTeamNamesForRepositoryRole(repository.name)) {
1746                 TeamModel team = userManager.getTeamModel(teamname);
1747                 if (!ArrayUtils.isEmpty(team.preReceiveScripts)) {
1748                     scripts.addAll(team.preReceiveScripts);
1749                 }
1750             }
1751         }
1752         return new ArrayList<String>(scripts);
1753     }
1754
1755     /**
1756      * Returns the list of all available Groovy pre-receive push hook scripts
1757      * that are not already inherited by the repository. Script files must have
1758      * .groovy extension
1759      *
1760      * @param repository
1761      *            optional parameter
1762      * @return list of available hook scripts
1763      */
1764     @Override
1765     public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
1766         Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
1767
1768         // create list of available scripts by excluding inherited scripts
1769         List<String> scripts = new ArrayList<String>();
1770         for (String script : getAllScripts()) {
1771             if (!inherited.contains(script)) {
1772                 scripts.add(script);
1773             }
1774         }
1775         return scripts;
1776     }
1777
1778     /**
1779      * Returns the list of post-receive scripts the repository inherited from
1780      * the global settings and team affiliations.
1781      *
1782      * @param repository
1783      *            if null only the globally specified scripts are returned
1784      * @return a list of scripts
1785      */
1786     @Override
1787     public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
1788         Set<String> scripts = new LinkedHashSet<String>();
1789         // Global Scripts
1790         for (String script : settings.getStrings(Keys.groovy.postReceiveScripts)) {
1791             if (script.endsWith(".groovy")) {
1792                 scripts.add(script.substring(0, script.lastIndexOf('.')));
1793             } else {
1794                 scripts.add(script);
1795             }
1796         }
1797         // Team Scripts
1798         if (repository != null) {
1799             for (String teamname : userManager.getTeamNamesForRepositoryRole(repository.name)) {
1800                 TeamModel team = userManager.getTeamModel(teamname);
1801                 if (!ArrayUtils.isEmpty(team.postReceiveScripts)) {
1802                     scripts.addAll(team.postReceiveScripts);
1803                 }
1804             }
1805         }
1806         return new ArrayList<String>(scripts);
1807     }
1808
1809     /**
1810      * Returns the list of unused Groovy post-receive push hook scripts that are
1811      * not already inherited by the repository. Script files must have .groovy
1812      * extension
1813      *
1814      * @param repository
1815      *            optional parameter
1816      * @return list of available hook scripts
1817      */
1818     @Override
1819     public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
1820         Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
1821
1822         // create list of available scripts by excluding inherited scripts
1823         List<String> scripts = new ArrayList<String>();
1824         for (String script : getAllScripts()) {
1825             if (!inherited.contains(script)) {
1826                 scripts.add(script);
1827             }
1828         }
1829         return scripts;
1830     }
1831
1832     /**
1833      * Search the specified repositories using the Lucene query.
1834      *
1835      * @param query
1836      * @param page
1837      * @param pageSize
1838      * @param repositories
1839      * @return
1840      */
1841     @Override
1842     public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) {
1843         List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);
1844         return srs;
1845     }
1846
1847     protected void configureLuceneIndexing() {
7bf6e1 1848         luceneExecutor = new LuceneService(settings, this);
936af6 1849         String frequency = settings.getString(Keys.web.luceneFrequency, "2 mins");
JM 1850         int mins = TimeUtils.convertFrequencyToMinutes(frequency, 2);
1851         scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, mins,  TimeUnit.MINUTES);
1852         logger.info("Lucene will process indexed branches every {} minutes.", mins);
95cdba 1853     }
JM 1854
1855     protected void configureGarbageCollector() {
1856         // schedule gc engine
7bf6e1 1857         gcExecutor = new GarbageCollectorService(settings, this);
95cdba 1858         if (gcExecutor.isReady()) {
269c50 1859             logger.info("Garbage Collector (GC) will scan repositories every 24 hours.");
95cdba 1860             Calendar c = Calendar.getInstance();
JM 1861             c.set(Calendar.HOUR_OF_DAY, settings.getInteger(Keys.git.garbageCollectionHour, 0));
1862             c.set(Calendar.MINUTE, 0);
1863             c.set(Calendar.SECOND, 0);
1864             c.set(Calendar.MILLISECOND, 0);
1865             Date cd = c.getTime();
1866             Date now = new Date();
1867             int delay = 0;
1868             if (cd.before(now)) {
1869                 c.add(Calendar.DATE, 1);
1870                 cd = c.getTime();
1871             }
1872             delay = (int) ((cd.getTime() - now.getTime())/TimeUtils.MIN);
1873             String when = delay + " mins";
1874             if (delay > 60) {
1875                 when = MessageFormat.format("{0,number,0.0} hours", delay / 60f);
1876             }
1877             logger.info(MessageFormat.format("Next scheculed GC scan is in {0}", when));
1878             scheduledExecutor.scheduleAtFixedRate(gcExecutor, delay, 60 * 24, TimeUnit.MINUTES);
269c50 1879         } else {
JM 1880             logger.info("Garbage Collector (GC) is disabled.");
95cdba 1881         }
JM 1882     }
1883
1884     protected void configureMirrorExecutor() {
7bf6e1 1885         mirrorExecutor = new MirrorService(settings, this);
95cdba 1886         if (mirrorExecutor.isReady()) {
936af6 1887             int mins = TimeUtils.convertFrequencyToMinutes(settings.getString(Keys.git.mirrorPeriod, "30 mins"), 5);
95cdba 1888             int delay = 1;
JM 1889             scheduledExecutor.scheduleAtFixedRate(mirrorExecutor, delay, mins,  TimeUnit.MINUTES);
269c50 1890             logger.info("Mirror service will fetch updates every {} minutes.", mins);
95cdba 1891             logger.info("Next scheduled mirror fetch is in {} minutes", delay);
269c50 1892         } else {
JM 1893             logger.info("Mirror service is disabled.");
95cdba 1894         }
JM 1895     }
1896
1897     protected void configureJGit() {
1898         // Configure JGit
1899         WindowCacheConfig cfg = new WindowCacheConfig();
1900
1901         cfg.setPackedGitWindowSize(settings.getFilesize(Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
1902         cfg.setPackedGitLimit(settings.getFilesize(Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
1903         cfg.setDeltaBaseCacheLimit(settings.getFilesize(Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
1904         cfg.setPackedGitOpenFiles(settings.getFilesize(Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
1905         cfg.setPackedGitMMAP(settings.getBoolean(Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
1906
1907         try {
1908             cfg.install();
1909             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
1910             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
1911             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
1912             logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
1913             logger.debug(MessageFormat.format("{0} = {1}", Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
1914         } catch (IllegalArgumentException e) {
1915             logger.error("Failed to configure JGit parameters!", e);
1916         }
e685ba 1917
JM 1918         try {
1919             // issue-486/ticket-151: UTF-9 & UTF-18
1920             Field field = RawParseUtils.class.getDeclaredField("encodingAliases");
1921             field.setAccessible(true);
1922             Map<String, Charset> encodingAliases = (Map<String, Charset>) field.get(null);
1923             encodingAliases.put("utf-9", RawParseUtils.UTF8_CHARSET);
1924             encodingAliases.put("utf-18", RawParseUtils.UTF8_CHARSET);
1925             logger.info("Alias UTF-9 & UTF-18 encodings as UTF-8 in JGit");
1926         } catch (Throwable t) {
1927             logger.error("Failed to inject UTF-9 & UTF-18 encoding aliases into JGit", t);
1928         }
95cdba 1929     }
JM 1930
1931     protected void configureCommitCache() {
1932         int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
1933         if (daysToCache <= 0) {
269c50 1934             logger.info("Commit cache is disabled");
95cdba 1935         } else {
JM 1936             long start = System.nanoTime();
1937             long repoCount = 0;
1938             long commitCount = 0;
269c50 1939             logger.info(MessageFormat.format("Preparing {0} day commit cache. please wait...", daysToCache));
95cdba 1940             CommitCache.instance().setCacheDays(daysToCache);
JM 1941             Date cutoff = CommitCache.instance().getCutoffDate();
1942             for (String repositoryName : getRepositoryList()) {
1943                 RepositoryModel model = getRepositoryModel(repositoryName);
1944                 if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
1945                     repoCount++;
1946                     Repository repository = getRepository(repositoryName);
1947                     for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
1948                         if (!ref.getDate().after(cutoff)) {
1949                             // branch not recently updated
1950                             continue;
1951                         }
1952                         List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
1953                         if (commits.size() > 0) {
1954                             logger.info(MessageFormat.format("  cached {0} commits for {1}:{2}",
1955                                     commits.size(), repositoryName, ref.getName()));
1956                             commitCount += commits.size();
1957                         }
1958                     }
1959                     repository.close();
1960                 }
1961             }
1962             logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
1963                     daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
1964         }
1965     }
088b6f 1966
JM 1967     protected void confirmWriteAccess() {
1968         if (runtimeManager.isServingRepositories()) {
1969             try {
c33f80 1970                 if (!getRepositoriesFolder().exists()) {
JM 1971                     getRepositoriesFolder().mkdirs();
1972                 }
088b6f 1973                 File file = File.createTempFile(".test-", ".txt", getRepositoriesFolder());
JM 1974                 file.delete();
1975             } catch (Exception e) {
1976                 logger.error("");
1977                 logger.error(Constants.BORDER2);
1978                 logger.error("Please check filesystem permissions!");
1979                 logger.error("FAILED TO WRITE TO REPOSITORIES FOLDER!!", e);
1980                 logger.error(Constants.BORDER2);
1981                 logger.error("");
1982             }
1983         }
1984     }
95cdba 1985 }