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.

GitblitReceivePack.java 20KB

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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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.git;
  17. import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
  18. import groovy.lang.Binding;
  19. import groovy.util.GroovyScriptEngine;
  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.text.MessageFormat;
  23. import java.util.Collection;
  24. import java.util.LinkedHashSet;
  25. import java.util.List;
  26. import java.util.Set;
  27. import java.util.concurrent.TimeUnit;
  28. import org.eclipse.jgit.lib.BatchRefUpdate;
  29. import org.eclipse.jgit.lib.NullProgressMonitor;
  30. import org.eclipse.jgit.lib.PersonIdent;
  31. import org.eclipse.jgit.lib.ProgressMonitor;
  32. import org.eclipse.jgit.lib.Repository;
  33. import org.eclipse.jgit.revwalk.RevCommit;
  34. import org.eclipse.jgit.transport.PostReceiveHook;
  35. import org.eclipse.jgit.transport.PreReceiveHook;
  36. import org.eclipse.jgit.transport.ReceiveCommand;
  37. import org.eclipse.jgit.transport.ReceiveCommand.Result;
  38. import org.eclipse.jgit.transport.ReceivePack;
  39. import org.slf4j.Logger;
  40. import org.slf4j.LoggerFactory;
  41. import com.gitblit.Constants;
  42. import com.gitblit.Constants.AccessRestrictionType;
  43. import com.gitblit.IStoredSettings;
  44. import com.gitblit.Keys;
  45. import com.gitblit.client.Translation;
  46. import com.gitblit.extensions.ReceiveHook;
  47. import com.gitblit.manager.IGitblit;
  48. import com.gitblit.models.RepositoryModel;
  49. import com.gitblit.models.UserModel;
  50. import com.gitblit.tickets.BranchTicketService;
  51. import com.gitblit.utils.ArrayUtils;
  52. import com.gitblit.utils.ClientLogger;
  53. import com.gitblit.utils.CommitCache;
  54. import com.gitblit.utils.JGitUtils;
  55. import com.gitblit.utils.RefLogUtils;
  56. import com.gitblit.utils.StringUtils;
  57. /**
  58. * GitblitReceivePack processes receive commands. It also executes Groovy pre-
  59. * and post- receive hooks.
  60. *
  61. * The general execution flow is:
  62. * <ol>
  63. * <li>onPreReceive()</li>
  64. * <li>executeCommands()</li>
  65. * <li>onPostReceive()</li>
  66. * </ol>
  67. *
  68. * @author Android Open Source Project
  69. * @author James Moger
  70. *
  71. */
  72. public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, PostReceiveHook {
  73. private static final Logger LOGGER = LoggerFactory.getLogger(GitblitReceivePack.class);
  74. protected final RepositoryModel repository;
  75. protected final UserModel user;
  76. protected final File groovyDir;
  77. protected String gitblitUrl;
  78. protected GroovyScriptEngine gse;
  79. protected final IStoredSettings settings;
  80. protected final IGitblit gitblit;
  81. public GitblitReceivePack(
  82. IGitblit gitblit,
  83. Repository db,
  84. RepositoryModel repository,
  85. UserModel user) {
  86. super(db);
  87. this.settings = gitblit.getSettings();
  88. this.gitblit = gitblit;
  89. this.repository = repository;
  90. this.user = user;
  91. this.groovyDir = gitblit.getHooksFolder();
  92. try {
  93. // set Grape root
  94. File grapeRoot = gitblit.getGrapesFolder();
  95. grapeRoot.mkdirs();
  96. System.setProperty("grape.root", grapeRoot.getAbsolutePath());
  97. this.gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());
  98. } catch (IOException e) {
  99. }
  100. // set advanced ref permissions
  101. setAllowCreates(user.canCreateRef(repository));
  102. setAllowDeletes(user.canDeleteRef(repository));
  103. setAllowNonFastForwards(user.canRewindRef(repository));
  104. int maxObjectSz = settings.getInteger(Keys.git.maxObjectSizeLimit, -1);
  105. if (maxObjectSz >= 0) {
  106. setMaxObjectSizeLimit(maxObjectSz);
  107. }
  108. int maxPackSz = settings.getInteger(Keys.git.maxPackSizeLimit, -1);
  109. if (maxPackSz >= 0) {
  110. setMaxPackSizeLimit(maxPackSz);
  111. }
  112. setCheckReceivedObjects(settings.getBoolean(Keys.git.checkReceivedObjects, true));
  113. setCheckReferencedObjectsAreReachable(settings.getBoolean(Keys.git.checkReferencedObjectsAreReachable, true));
  114. // setup pre and post receive hook
  115. setPreReceiveHook(this);
  116. setPostReceiveHook(this);
  117. }
  118. /**
  119. * Returns true if the user is permitted to apply the receive commands to
  120. * the repository.
  121. *
  122. * @param commands
  123. * @return true if the user may push these commands
  124. */
  125. protected boolean canPush(Collection<ReceiveCommand> commands) {
  126. // TODO Consider supporting branch permissions here (issue-36)
  127. // Not sure if that should be Gerrit-style, refs/meta/config, or
  128. // gitolite-style, permissions in users.conf
  129. //
  130. // How could commands be empty?
  131. //
  132. // Because a subclass, like PatchsetReceivePack, filters receive
  133. // commands before this method is called. This makes it possible for
  134. // this method to test an empty list. In this case, we assume that the
  135. // subclass receive pack properly enforces push restrictions. for the
  136. // ref.
  137. //
  138. // The empty test is not explicitly required, it's written here to
  139. // clarify special-case behavior.
  140. return commands.isEmpty() ? true : user.canPush(repository);
  141. }
  142. /**
  143. * Instrumentation point where the incoming push event has been parsed,
  144. * validated, objects created BUT refs have not been updated. You might
  145. * use this to enforce a branch-write permissions model.
  146. */
  147. @Override
  148. public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
  149. if (commands.size() == 0) {
  150. // no receive commands to process
  151. // this can happen if receive pack subclasses intercept and filter
  152. // the commands
  153. LOGGER.debug("skipping pre-receive processing, no refs created, updated, or removed");
  154. return;
  155. }
  156. if (repository.isMirror) {
  157. // repository is a mirror
  158. for (ReceiveCommand cmd : commands) {
  159. sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name);
  160. }
  161. return;
  162. }
  163. if (repository.isFrozen) {
  164. // repository is frozen/readonly
  165. for (ReceiveCommand cmd : commands) {
  166. sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is frozen!", repository.name);
  167. }
  168. return;
  169. }
  170. if (!repository.isBare) {
  171. // repository has a working copy
  172. for (ReceiveCommand cmd : commands) {
  173. sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it has a working copy!", repository.name);
  174. }
  175. return;
  176. }
  177. if (!canPush(commands)) {
  178. // user does not have push permissions
  179. for (ReceiveCommand cmd : commands) {
  180. sendRejection(cmd, "User \"{0}\" does not have push permissions for \"{1}\"!", user.username, repository.name);
  181. }
  182. return;
  183. }
  184. if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
  185. // enforce committer verification
  186. if (StringUtils.isEmpty(user.emailAddress)) {
  187. // reject the push because the pushing account does not have an email address
  188. for (ReceiveCommand cmd : commands) {
  189. sendRejection(cmd, "Sorry, the account \"{0}\" does not have an email address set for committer verification!", user.username);
  190. }
  191. return;
  192. }
  193. // Optionally enforce that the committer of first parent chain
  194. // match the account being used to push the commits.
  195. //
  196. // This requires all merge commits are executed with the "--no-ff"
  197. // option to force a merge commit even if fast-forward is possible.
  198. // This ensures that the chain first parents has the commit
  199. // identity of the merging user.
  200. boolean allRejected = false;
  201. for (ReceiveCommand cmd : commands) {
  202. String firstParent = null;
  203. try {
  204. List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
  205. for (RevCommit commit : commits) {
  206. if (firstParent != null) {
  207. if (!commit.getName().equals(firstParent)) {
  208. // ignore: commit is right-descendant of a merge
  209. continue;
  210. }
  211. }
  212. // update expected next commit id
  213. if (commit.getParentCount() == 0) {
  214. firstParent = null;
  215. } else {
  216. firstParent = commit.getParents()[0].getId().getName();
  217. }
  218. PersonIdent committer = commit.getCommitterIdent();
  219. if (!user.is(committer.getName(), committer.getEmailAddress())) {
  220. // verification failed
  221. String reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>",
  222. commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);
  223. LOGGER.warn(reason);
  224. cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
  225. allRejected &= true;
  226. break;
  227. } else {
  228. allRejected = false;
  229. }
  230. }
  231. } catch (Exception e) {
  232. LOGGER.error("Failed to verify commits were made by pushing user", e);
  233. }
  234. }
  235. if (allRejected) {
  236. // all ref updates rejected, abort
  237. return;
  238. }
  239. }
  240. for (ReceiveCommand cmd : commands) {
  241. String ref = cmd.getRefName();
  242. if (ref.startsWith(Constants.R_HEADS)) {
  243. switch (cmd.getType()) {
  244. case UPDATE_NONFASTFORWARD:
  245. case DELETE:
  246. // reset branch commit cache on REWIND and DELETE
  247. CommitCache.instance().clear(repository.name, ref);
  248. break;
  249. default:
  250. break;
  251. }
  252. } else if (ref.equals(BranchTicketService.BRANCH)) {
  253. // ensure pushing user is an administrator OR an owner
  254. // i.e. prevent ticket tampering
  255. boolean permitted = user.canAdmin() || repository.isOwner(user.username);
  256. if (!permitted) {
  257. sendRejection(cmd, "{0} is not permitted to push to {1}", user.username, ref);
  258. }
  259. } else if (ref.startsWith(Constants.R_FOR)) {
  260. // prevent accidental push to refs/for
  261. sendRejection(cmd, "{0} is not configured to receive patchsets", repository.name);
  262. }
  263. }
  264. // call pre-receive plugins
  265. for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
  266. try {
  267. hook.onPreReceive(this, commands);
  268. } catch (Exception e) {
  269. LOGGER.error("Failed to execute extension", e);
  270. }
  271. }
  272. Set<String> scripts = new LinkedHashSet<String>();
  273. scripts.addAll(gitblit.getPreReceiveScriptsInherited(repository));
  274. if (!ArrayUtils.isEmpty(repository.preReceiveScripts)) {
  275. scripts.addAll(repository.preReceiveScripts);
  276. }
  277. runGroovy(commands, scripts);
  278. for (ReceiveCommand cmd : commands) {
  279. if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
  280. LOGGER.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
  281. .getName(), cmd.getResult(), cmd.getMessage()));
  282. }
  283. }
  284. }
  285. /**
  286. * Instrumentation point where the incoming push has been applied to the
  287. * repository. This is the point where we would trigger a Jenkins build
  288. * or send an email.
  289. */
  290. @Override
  291. public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
  292. if (commands.size() == 0) {
  293. LOGGER.debug("skipping post-receive processing, no refs created, updated, or removed");
  294. return;
  295. }
  296. logRefChange(commands);
  297. updateIncrementalPushTags(commands);
  298. updateGitblitRefLog(commands);
  299. // check for updates pushed to the BranchTicketService branch
  300. // if the BranchTicketService is active it will reindex, as appropriate
  301. for (ReceiveCommand cmd : commands) {
  302. if (Result.OK.equals(cmd.getResult())
  303. && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
  304. rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
  305. }
  306. }
  307. // call post-receive plugins
  308. for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
  309. try {
  310. hook.onPostReceive(this, commands);
  311. } catch (Exception e) {
  312. LOGGER.error("Failed to execute extension", e);
  313. }
  314. }
  315. // run Groovy hook scripts
  316. Set<String> scripts = new LinkedHashSet<String>();
  317. scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
  318. if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
  319. scripts.addAll(repository.postReceiveScripts);
  320. }
  321. runGroovy(commands, scripts);
  322. }
  323. /**
  324. * Log the ref changes in the container log.
  325. *
  326. * @param commands
  327. */
  328. protected void logRefChange(Collection<ReceiveCommand> commands) {
  329. boolean isRefCreationOrDeletion = false;
  330. // log ref changes
  331. for (ReceiveCommand cmd : commands) {
  332. if (Result.OK.equals(cmd.getResult())) {
  333. // add some logging for important ref changes
  334. switch (cmd.getType()) {
  335. case DELETE:
  336. LOGGER.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
  337. isRefCreationOrDeletion = true;
  338. break;
  339. case CREATE:
  340. LOGGER.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
  341. isRefCreationOrDeletion = true;
  342. break;
  343. case UPDATE:
  344. LOGGER.info(MessageFormat.format("{0} UPDATED {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
  345. break;
  346. case UPDATE_NONFASTFORWARD:
  347. LOGGER.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
  348. break;
  349. default:
  350. break;
  351. }
  352. }
  353. }
  354. if (isRefCreationOrDeletion) {
  355. gitblit.resetRepositoryCache(repository.name);
  356. }
  357. }
  358. /**
  359. * Optionally update the incremental push tags.
  360. *
  361. * @param commands
  362. */
  363. protected void updateIncrementalPushTags(Collection<ReceiveCommand> commands) {
  364. if (!repository.useIncrementalPushTags) {
  365. return;
  366. }
  367. // tag each pushed branch tip
  368. String emailAddress = user.emailAddress == null ? getRefLogIdent().getEmailAddress() : user.emailAddress;
  369. PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
  370. for (ReceiveCommand cmd : commands) {
  371. if (!cmd.getRefName().startsWith(Constants.R_HEADS)) {
  372. // only tag branch ref changes
  373. continue;
  374. }
  375. if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
  376. && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
  377. String objectId = cmd.getNewId().getName();
  378. String branch = cmd.getRefName().substring(Constants.R_HEADS.length());
  379. // get translation based on the server's locale setting
  380. String template = Translation.get("gb.incrementalPushTagMessage");
  381. String msg = MessageFormat.format(template, branch);
  382. String prefix;
  383. if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
  384. prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
  385. } else {
  386. prefix = repository.incrementalPushTagPrefix;
  387. }
  388. JGitUtils.createIncrementalRevisionTag(
  389. getRepository(),
  390. objectId,
  391. userIdent,
  392. prefix,
  393. "0",
  394. msg);
  395. }
  396. }
  397. }
  398. /**
  399. * Update Gitblit's internal reflog.
  400. *
  401. * @param commands
  402. */
  403. protected void updateGitblitRefLog(Collection<ReceiveCommand> commands) {
  404. try {
  405. RefLogUtils.updateRefLog(user, getRepository(), commands);
  406. LOGGER.debug(MessageFormat.format("{0} reflog updated", repository.name));
  407. } catch (Exception e) {
  408. LOGGER.error(MessageFormat.format("Failed to update {0} reflog", repository.name), e);
  409. }
  410. }
  411. /** Execute commands to update references. */
  412. @Override
  413. protected void executeCommands() {
  414. List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
  415. if (toApply.isEmpty()) {
  416. return;
  417. }
  418. ProgressMonitor updating = NullProgressMonitor.INSTANCE;
  419. boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
  420. if (sideBand) {
  421. SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
  422. pm.setDelayStart(250, TimeUnit.MILLISECONDS);
  423. updating = pm;
  424. }
  425. BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
  426. batch.setAllowNonFastForwards(isAllowNonFastForwards());
  427. batch.setRefLogIdent(getRefLogIdent());
  428. batch.setRefLogMessage("push", true);
  429. for (ReceiveCommand cmd : toApply) {
  430. if (Result.NOT_ATTEMPTED != cmd.getResult()) {
  431. // Already rejected by the core receive process.
  432. continue;
  433. }
  434. batch.addCommand(cmd);
  435. }
  436. if (!batch.getCommands().isEmpty()) {
  437. try {
  438. batch.execute(getRevWalk(), updating);
  439. } catch (IOException err) {
  440. for (ReceiveCommand cmd : toApply) {
  441. if (cmd.getResult() == Result.NOT_ATTEMPTED) {
  442. sendRejection(cmd, "lock error: {0}", err.getMessage());
  443. }
  444. }
  445. }
  446. }
  447. }
  448. protected void setGitblitUrl(String url) {
  449. this.gitblitUrl = url;
  450. }
  451. public void sendRejection(final ReceiveCommand cmd, final String why, Object... objects) {
  452. String text;
  453. if (ArrayUtils.isEmpty(objects)) {
  454. text = why;
  455. } else {
  456. text = MessageFormat.format(why, objects);
  457. }
  458. cmd.setResult(Result.REJECTED_OTHER_REASON, text);
  459. LOGGER.error(text + " (" + user.username + ")");
  460. }
  461. public void sendHeader(String msg, Object... objects) {
  462. sendInfo("--> ", msg, objects);
  463. }
  464. public void sendInfo(String msg, Object... objects) {
  465. sendInfo(" ", msg, objects);
  466. }
  467. private void sendInfo(String prefix, String msg, Object... objects) {
  468. String text;
  469. if (ArrayUtils.isEmpty(objects)) {
  470. text = msg;
  471. super.sendMessage(prefix + msg);
  472. } else {
  473. text = MessageFormat.format(msg, objects);
  474. super.sendMessage(prefix + text);
  475. }
  476. if (!StringUtils.isEmpty(msg)) {
  477. LOGGER.info(text + " (" + user.username + ")");
  478. }
  479. }
  480. public void sendError(String msg, Object... objects) {
  481. String text;
  482. if (ArrayUtils.isEmpty(objects)) {
  483. text = msg;
  484. super.sendError(msg);
  485. } else {
  486. text = MessageFormat.format(msg, objects);
  487. super.sendError(text);
  488. }
  489. if (!StringUtils.isEmpty(msg)) {
  490. LOGGER.error(text + " (" + user.username + ")");
  491. }
  492. }
  493. /**
  494. * Runs the specified Groovy hook scripts.
  495. *
  496. * @param repository
  497. * @param user
  498. * @param commands
  499. * @param scripts
  500. */
  501. private void runGroovy(Collection<ReceiveCommand> commands, Set<String> scripts) {
  502. if (scripts == null || scripts.size() == 0) {
  503. // no Groovy scripts to execute
  504. return;
  505. }
  506. Binding binding = new Binding();
  507. binding.setVariable("gitblit", gitblit);
  508. binding.setVariable("repository", repository);
  509. binding.setVariable("receivePack", this);
  510. binding.setVariable("user", user);
  511. binding.setVariable("commands", commands);
  512. binding.setVariable("url", gitblitUrl);
  513. binding.setVariable("logger", LOGGER);
  514. binding.setVariable("clientLogger", new ClientLogger(this));
  515. for (String script : scripts) {
  516. if (StringUtils.isEmpty(script)) {
  517. continue;
  518. }
  519. // allow script to be specified without .groovy extension
  520. // this is easier to read in the settings
  521. File file = new File(groovyDir, script);
  522. if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
  523. file = new File(groovyDir, script + ".groovy");
  524. if (file.exists()) {
  525. script = file.getName();
  526. }
  527. }
  528. try {
  529. Object result = gse.run(script, binding);
  530. if (result instanceof Boolean) {
  531. if (!((Boolean) result)) {
  532. LOGGER.error(MessageFormat.format(
  533. "Groovy script {0} has failed! Hook scripts aborted.", script));
  534. break;
  535. }
  536. }
  537. } catch (Exception e) {
  538. LOGGER.error(
  539. MessageFormat.format("Failed to execute Groovy script {0}", script), e);
  540. }
  541. }
  542. }
  543. public IGitblit getGitblit() {
  544. return gitblit;
  545. }
  546. public RepositoryModel getRepositoryModel() {
  547. return repository;
  548. }
  549. public UserModel getUserModel() {
  550. return user;
  551. }
  552. }