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