You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RepositoryManager.java 63KB

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