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