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