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