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