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.

PatchsetReceivePack.java 37KB

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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129
  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 java.io.IOException;
  19. import java.text.MessageFormat;
  20. import java.util.ArrayList;
  21. import java.util.Arrays;
  22. import java.util.Collection;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.concurrent.TimeUnit;
  28. import java.util.regex.Matcher;
  29. import java.util.regex.Pattern;
  30. import org.eclipse.jgit.lib.AnyObjectId;
  31. import org.eclipse.jgit.lib.BatchRefUpdate;
  32. import org.eclipse.jgit.lib.NullProgressMonitor;
  33. import org.eclipse.jgit.lib.ObjectId;
  34. import org.eclipse.jgit.lib.PersonIdent;
  35. import org.eclipse.jgit.lib.ProgressMonitor;
  36. import org.eclipse.jgit.lib.Ref;
  37. import org.eclipse.jgit.lib.RefUpdate;
  38. import org.eclipse.jgit.lib.Repository;
  39. import org.eclipse.jgit.revwalk.RevCommit;
  40. import org.eclipse.jgit.revwalk.RevSort;
  41. import org.eclipse.jgit.revwalk.RevWalk;
  42. import org.eclipse.jgit.transport.ReceiveCommand;
  43. import org.eclipse.jgit.transport.ReceiveCommand.Result;
  44. import org.eclipse.jgit.transport.ReceiveCommand.Type;
  45. import org.eclipse.jgit.transport.ReceivePack;
  46. import org.slf4j.Logger;
  47. import org.slf4j.LoggerFactory;
  48. import com.gitblit.Constants;
  49. import com.gitblit.Keys;
  50. import com.gitblit.manager.IGitblit;
  51. import com.gitblit.models.RepositoryModel;
  52. import com.gitblit.models.TicketModel;
  53. import com.gitblit.models.TicketModel.Change;
  54. import com.gitblit.models.TicketModel.Field;
  55. import com.gitblit.models.TicketModel.Patchset;
  56. import com.gitblit.models.TicketModel.PatchsetType;
  57. import com.gitblit.models.TicketModel.Status;
  58. import com.gitblit.models.UserModel;
  59. import com.gitblit.tickets.ITicketService;
  60. import com.gitblit.tickets.TicketMilestone;
  61. import com.gitblit.tickets.TicketNotifier;
  62. import com.gitblit.utils.ArrayUtils;
  63. import com.gitblit.utils.DiffUtils;
  64. import com.gitblit.utils.DiffUtils.DiffStat;
  65. import com.gitblit.utils.JGitUtils;
  66. import com.gitblit.utils.JGitUtils.MergeResult;
  67. import com.gitblit.utils.JGitUtils.MergeStatus;
  68. import com.gitblit.utils.RefLogUtils;
  69. import com.gitblit.utils.StringUtils;
  70. /**
  71. * PatchsetReceivePack processes receive commands and allows for creating, updating,
  72. * and closing Gitblit tickets. It also executes Groovy pre- and post- receive
  73. * hooks.
  74. *
  75. * The patchset mechanism defined in this class is based on the ReceiveCommits class
  76. * from the Gerrit code review server.
  77. *
  78. * The general execution flow is:
  79. * <ol>
  80. * <li>onPreReceive()</li>
  81. * <li>executeCommands()</li>
  82. * <li>onPostReceive()</li>
  83. * </ol>
  84. *
  85. * @author Android Open Source Project
  86. * @author James Moger
  87. *
  88. */
  89. public class PatchsetReceivePack extends GitblitReceivePack {
  90. protected static final List<String> MAGIC_REFS = Arrays.asList(Constants.R_FOR, Constants.R_TICKET);
  91. protected static final Pattern NEW_PATCHSET =
  92. Pattern.compile("^refs/tickets/(?:[0-9a-zA-Z][0-9a-zA-Z]/)?([1-9][0-9]*)(?:/new)?$");
  93. private static final Logger LOGGER = LoggerFactory.getLogger(PatchsetReceivePack.class);
  94. protected final ITicketService ticketService;
  95. protected final TicketNotifier ticketNotifier;
  96. private boolean requireCleanMerge;
  97. public PatchsetReceivePack(IGitblit gitblit, Repository db, RepositoryModel repository, UserModel user) {
  98. super(gitblit, db, repository, user);
  99. this.ticketService = gitblit.getTicketService();
  100. this.ticketNotifier = ticketService.createNotifier();
  101. }
  102. /** Returns the patchset ref root from the ref */
  103. private String getPatchsetRef(String refName) {
  104. for (String patchRef : MAGIC_REFS) {
  105. if (refName.startsWith(patchRef)) {
  106. return patchRef;
  107. }
  108. }
  109. return null;
  110. }
  111. /** Checks if the supplied ref name is a patchset ref */
  112. private boolean isPatchsetRef(String refName) {
  113. return !StringUtils.isEmpty(getPatchsetRef(refName));
  114. }
  115. /** Checks if the supplied ref name is a change ref */
  116. private boolean isTicketRef(String refName) {
  117. return refName.startsWith(Constants.R_TICKETS_PATCHSETS);
  118. }
  119. /** Extracts the integration branch from the ref name */
  120. private String getIntegrationBranch(String refName) {
  121. String patchsetRef = getPatchsetRef(refName);
  122. String branch = refName.substring(patchsetRef.length());
  123. if (branch.indexOf('%') > -1) {
  124. branch = branch.substring(0, branch.indexOf('%'));
  125. }
  126. String defaultBranch = "master";
  127. try {
  128. defaultBranch = getRepository().getBranch();
  129. } catch (Exception e) {
  130. LOGGER.error("failed to determine default branch for " + repository.name, e);
  131. }
  132. long ticketId = 0L;
  133. try {
  134. ticketId = Long.parseLong(branch);
  135. } catch (Exception e) {
  136. // not a number
  137. }
  138. if (ticketId > 0 || branch.equalsIgnoreCase("default") || branch.equalsIgnoreCase("new")) {
  139. return defaultBranch;
  140. }
  141. return branch;
  142. }
  143. /** Extracts the ticket id from the ref name */
  144. private long getTicketId(String refName) {
  145. if (refName.startsWith(Constants.R_FOR)) {
  146. String ref = refName.substring(Constants.R_FOR.length());
  147. if (ref.indexOf('%') > -1) {
  148. ref = ref.substring(0, ref.indexOf('%'));
  149. }
  150. try {
  151. return Long.parseLong(ref);
  152. } catch (Exception e) {
  153. // not a number
  154. }
  155. } else if (refName.startsWith(Constants.R_TICKET) ||
  156. refName.startsWith(Constants.R_TICKETS_PATCHSETS)) {
  157. return PatchsetCommand.getTicketNumber(refName);
  158. }
  159. return 0L;
  160. }
  161. /** Returns true if the ref namespace exists */
  162. private boolean hasRefNamespace(String ref) {
  163. Map<String, Ref> blockingFors;
  164. try {
  165. blockingFors = getRepository().getRefDatabase().getRefs(ref);
  166. } catch (IOException err) {
  167. sendError("Cannot scan refs in {0}", repository.name);
  168. LOGGER.error("Error!", err);
  169. return true;
  170. }
  171. if (!blockingFors.isEmpty()) {
  172. sendError("{0} needs the following refs removed to receive patchsets: {1}",
  173. repository.name, blockingFors.keySet());
  174. return true;
  175. }
  176. return false;
  177. }
  178. /** Removes change ref receive commands */
  179. private List<ReceiveCommand> excludeTicketCommands(Collection<ReceiveCommand> commands) {
  180. List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
  181. for (ReceiveCommand cmd : commands) {
  182. if (!isTicketRef(cmd.getRefName())) {
  183. // this is not a ticket ref update
  184. filtered.add(cmd);
  185. }
  186. }
  187. return filtered;
  188. }
  189. /** Removes patchset receive commands for pre- and post- hook integrations */
  190. private List<ReceiveCommand> excludePatchsetCommands(Collection<ReceiveCommand> commands) {
  191. List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
  192. for (ReceiveCommand cmd : commands) {
  193. if (!isPatchsetRef(cmd.getRefName())) {
  194. // this is a non-patchset ref update
  195. filtered.add(cmd);
  196. }
  197. }
  198. return filtered;
  199. }
  200. /** Process receive commands EXCEPT for Patchset commands. */
  201. @Override
  202. public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
  203. Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
  204. super.onPreReceive(rp, filtered);
  205. }
  206. /** Process receive commands EXCEPT for Patchset commands. */
  207. @Override
  208. public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
  209. Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
  210. super.onPostReceive(rp, filtered);
  211. // send all queued ticket notifications after processing all patchsets
  212. ticketNotifier.sendAll();
  213. }
  214. @Override
  215. protected void validateCommands() {
  216. // workaround for JGit's awful scoping choices
  217. //
  218. // set the patchset refs to OK to bypass checks in the super implementation
  219. for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
  220. if (isPatchsetRef(cmd.getRefName())) {
  221. if (cmd.getType() == ReceiveCommand.Type.CREATE) {
  222. cmd.setResult(Result.OK);
  223. }
  224. }
  225. }
  226. super.validateCommands();
  227. }
  228. /** Execute commands to update references. */
  229. @Override
  230. protected void executeCommands() {
  231. // workaround for JGit's awful scoping choices
  232. //
  233. // reset the patchset refs to NOT_ATTEMPTED (see validateCommands)
  234. for (ReceiveCommand cmd : filterCommands(Result.OK)) {
  235. if (isPatchsetRef(cmd.getRefName())) {
  236. cmd.setResult(Result.NOT_ATTEMPTED);
  237. }
  238. }
  239. List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
  240. if (toApply.isEmpty()) {
  241. return;
  242. }
  243. ProgressMonitor updating = NullProgressMonitor.INSTANCE;
  244. boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
  245. if (sideBand) {
  246. SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
  247. pm.setDelayStart(250, TimeUnit.MILLISECONDS);
  248. updating = pm;
  249. }
  250. BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
  251. batch.setAllowNonFastForwards(isAllowNonFastForwards());
  252. batch.setRefLogIdent(getRefLogIdent());
  253. batch.setRefLogMessage("push", true);
  254. ReceiveCommand patchsetRefCmd = null;
  255. PatchsetCommand patchsetCmd = null;
  256. for (ReceiveCommand cmd : toApply) {
  257. if (Result.NOT_ATTEMPTED != cmd.getResult()) {
  258. // Already rejected by the core receive process.
  259. continue;
  260. }
  261. if (isPatchsetRef(cmd.getRefName())) {
  262. if (ticketService == null) {
  263. sendRejection(cmd, "Sorry, the ticket service is unavailable and can not accept patchsets at this time.");
  264. continue;
  265. }
  266. if (!ticketService.isReady()) {
  267. sendRejection(cmd, "Sorry, the ticket service can not accept patchsets at this time.");
  268. continue;
  269. }
  270. if (UserModel.ANONYMOUS.equals(user)) {
  271. // server allows anonymous pushes, but anonymous patchset
  272. // contributions are prohibited by design
  273. sendRejection(cmd, "Sorry, anonymous patchset contributions are prohibited.");
  274. continue;
  275. }
  276. final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
  277. if (m.matches()) {
  278. // prohibit pushing directly to a patchset ref
  279. long id = getTicketId(cmd.getRefName());
  280. sendError("You may not directly push directly to a patchset ref!");
  281. sendError("Instead, please push to one the following:");
  282. sendError(" - {0}{1,number,0}", Constants.R_FOR, id);
  283. sendError(" - {0}{1,number,0}", Constants.R_TICKET, id);
  284. sendRejection(cmd, "protected ref");
  285. continue;
  286. }
  287. if (hasRefNamespace(Constants.R_FOR)) {
  288. // the refs/for/ namespace exists and it must not
  289. LOGGER.error("{} already has refs in the {} namespace",
  290. repository.name, Constants.R_FOR);
  291. sendRejection(cmd, "Sorry, a repository administrator will have to remove the {} namespace", Constants.R_FOR);
  292. continue;
  293. }
  294. if (patchsetRefCmd != null) {
  295. sendRejection(cmd, "You may only push one patchset at a time.");
  296. continue;
  297. }
  298. // responsible verification
  299. String responsible = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.RESPONSIBLE);
  300. if (!StringUtils.isEmpty(responsible)) {
  301. UserModel assignee = gitblit.getUserModel(responsible);
  302. if (assignee == null) {
  303. // no account by this name
  304. sendRejection(cmd, "{0} can not be assigned any tickets because there is no user account by that name", responsible);
  305. continue;
  306. } else if (!assignee.canPush(repository)) {
  307. // account does not have RW permissions
  308. sendRejection(cmd, "{0} ({1}) can not be assigned any tickets because the user does not have RW permissions for {2}",
  309. assignee.getDisplayName(), assignee.username, repository.name);
  310. continue;
  311. }
  312. }
  313. // milestone verification
  314. String milestone = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.MILESTONE);
  315. if (!StringUtils.isEmpty(milestone)) {
  316. TicketMilestone milestoneModel = ticketService.getMilestone(repository, milestone);
  317. if (milestoneModel == null) {
  318. // milestone does not exist
  319. sendRejection(cmd, "Sorry, \"{0}\" is not a valid milestone!", milestone);
  320. continue;
  321. }
  322. }
  323. // watcher verification
  324. List<String> watchers = PatchsetCommand.getOptions(cmd, PatchsetCommand.WATCH);
  325. if (!ArrayUtils.isEmpty(watchers)) {
  326. for (String watcher : watchers) {
  327. UserModel user = gitblit.getUserModel(watcher);
  328. if (user == null) {
  329. // watcher does not exist
  330. sendRejection(cmd, "Sorry, \"{0}\" is not a valid username for the watch list!", watcher);
  331. continue;
  332. }
  333. }
  334. }
  335. patchsetRefCmd = cmd;
  336. patchsetCmd = preparePatchset(cmd);
  337. if (patchsetCmd != null) {
  338. batch.addCommand(patchsetCmd);
  339. }
  340. continue;
  341. }
  342. batch.addCommand(cmd);
  343. }
  344. if (!batch.getCommands().isEmpty()) {
  345. try {
  346. batch.execute(getRevWalk(), updating);
  347. } catch (IOException err) {
  348. for (ReceiveCommand cmd : toApply) {
  349. if (cmd.getResult() == Result.NOT_ATTEMPTED) {
  350. sendRejection(cmd, "lock error: {0}", err.getMessage());
  351. }
  352. }
  353. }
  354. }
  355. //
  356. // set the results into the patchset ref receive command
  357. //
  358. if (patchsetRefCmd != null && patchsetCmd != null) {
  359. if (!patchsetCmd.getResult().equals(Result.OK)) {
  360. // patchset command failed!
  361. LOGGER.error(patchsetCmd.getType() + " " + patchsetCmd.getRefName()
  362. + " " + patchsetCmd.getResult());
  363. patchsetRefCmd.setResult(patchsetCmd.getResult(), patchsetCmd.getMessage());
  364. } else {
  365. // all patchset commands were applied
  366. patchsetRefCmd.setResult(Result.OK);
  367. // update the ticket branch ref
  368. RefUpdate ru = updateRef(patchsetCmd.getTicketBranch(), patchsetCmd.getNewId());
  369. updateReflog(ru);
  370. TicketModel ticket = processPatchset(patchsetCmd);
  371. if (ticket != null) {
  372. ticketNotifier.queueMailing(ticket);
  373. }
  374. }
  375. }
  376. //
  377. // if there are standard ref update receive commands that were
  378. // successfully processed, process referenced tickets, if any
  379. //
  380. List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
  381. List<ReceiveCommand> refUpdates = excludePatchsetCommands(allUpdates);
  382. List<ReceiveCommand> stdUpdates = excludeTicketCommands(refUpdates);
  383. if (!stdUpdates.isEmpty()) {
  384. int ticketsProcessed = 0;
  385. for (ReceiveCommand cmd : stdUpdates) {
  386. switch (cmd.getType()) {
  387. case CREATE:
  388. case UPDATE:
  389. case UPDATE_NONFASTFORWARD:
  390. Collection<TicketModel> tickets = processMergedTickets(cmd);
  391. ticketsProcessed += tickets.size();
  392. for (TicketModel ticket : tickets) {
  393. ticketNotifier.queueMailing(ticket);
  394. }
  395. break;
  396. default:
  397. break;
  398. }
  399. }
  400. if (ticketsProcessed == 1) {
  401. sendInfo("1 ticket updated");
  402. } else if (ticketsProcessed > 1) {
  403. sendInfo("{0} tickets updated", ticketsProcessed);
  404. }
  405. }
  406. // reset the ticket caches for the repository
  407. ticketService.resetCaches(repository);
  408. }
  409. /**
  410. * Prepares a patchset command.
  411. *
  412. * @param cmd
  413. * @return the patchset command
  414. */
  415. private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
  416. String branch = getIntegrationBranch(cmd.getRefName());
  417. long number = getTicketId(cmd.getRefName());
  418. TicketModel ticket = null;
  419. if (number > 0 && ticketService.hasTicket(repository, number)) {
  420. ticket = ticketService.getTicket(repository, number);
  421. }
  422. if (ticket == null) {
  423. if (number > 0) {
  424. // requested ticket does not exist
  425. sendError("Sorry, {0} does not have ticket {1,number,0}!", repository.name, number);
  426. sendRejection(cmd, "Invalid ticket number");
  427. return null;
  428. }
  429. } else {
  430. if (ticket.isMerged()) {
  431. // ticket already merged & resolved
  432. Change mergeChange = null;
  433. for (Change change : ticket.changes) {
  434. if (change.isMerge()) {
  435. mergeChange = change;
  436. break;
  437. }
  438. }
  439. sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
  440. mergeChange.author, mergeChange.patchset, number, ticket.mergeTo);
  441. sendRejection(cmd, "Ticket {0,number,0} already resolved", number);
  442. return null;
  443. } else if (!StringUtils.isEmpty(ticket.mergeTo)) {
  444. // ticket specifies integration branch
  445. branch = ticket.mergeTo;
  446. }
  447. }
  448. final int shortCommitIdLen = settings.getInteger(Keys.web.shortCommitIdLength, 6);
  449. final String shortTipId = cmd.getNewId().getName().substring(0, shortCommitIdLen);
  450. final RevCommit tipCommit = JGitUtils.getCommit(getRepository(), cmd.getNewId().getName());
  451. final String forBranch = branch;
  452. RevCommit mergeBase = null;
  453. Ref forBranchRef = getAdvertisedRefs().get(Constants.R_HEADS + forBranch);
  454. if (forBranchRef == null || forBranchRef.getObjectId() == null) {
  455. // unknown integration branch
  456. sendError("Sorry, there is no integration branch named ''{0}''.", forBranch);
  457. sendRejection(cmd, "Invalid integration branch specified");
  458. return null;
  459. } else {
  460. // determine the merge base for the patchset on the integration branch
  461. String base = JGitUtils.getMergeBase(getRepository(), forBranchRef.getObjectId(), tipCommit.getId());
  462. if (StringUtils.isEmpty(base)) {
  463. sendError("");
  464. sendError("There is no common ancestry between {0} and {1}.", forBranch, shortTipId);
  465. sendError("Please reconsider your proposed integration branch, {0}.", forBranch);
  466. sendError("");
  467. sendRejection(cmd, "no merge base for patchset and {0}", forBranch);
  468. return null;
  469. }
  470. mergeBase = JGitUtils.getCommit(getRepository(), base);
  471. }
  472. // ensure that the patchset can be cleanly merged right now
  473. MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch);
  474. switch (status) {
  475. case ALREADY_MERGED:
  476. sendError("");
  477. sendError("You have already merged this patchset.", forBranch);
  478. sendError("");
  479. sendRejection(cmd, "everything up-to-date");
  480. return null;
  481. case MERGEABLE:
  482. break;
  483. default:
  484. if (ticket == null || requireCleanMerge) {
  485. sendError("");
  486. sendError("Your patchset can not be cleanly merged into {0}.", forBranch);
  487. sendError("Please rebase your patchset and push again.");
  488. sendError("NOTE:", number);
  489. sendError("You should push your rebase to refs/for/{0,number,0}", number);
  490. sendError("");
  491. sendError(" git push origin HEAD:refs/for/{0,number,0}", number);
  492. sendError("");
  493. sendRejection(cmd, "patchset not mergeable");
  494. return null;
  495. }
  496. }
  497. // check to see if this commit is already linked to a ticket
  498. long id = identifyTicket(tipCommit, false);
  499. if (id > 0) {
  500. sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, id);
  501. sendRejection(cmd, "everything up-to-date");
  502. return null;
  503. }
  504. PatchsetCommand psCmd;
  505. if (ticket == null) {
  506. /*
  507. * NEW TICKET
  508. */
  509. Patchset patchset = newPatchset(null, mergeBase.getName(), tipCommit.getName());
  510. int minLength = 10;
  511. int maxLength = 100;
  512. String minTitle = MessageFormat.format(" minimum length of a title is {0} characters.", minLength);
  513. String maxTitle = MessageFormat.format(" maximum length of a title is {0} characters.", maxLength);
  514. if (patchset.commits > 1) {
  515. sendError("");
  516. sendError("To create a proposal ticket, please squash your commits and");
  517. sendError("provide a meaningful commit message with a short title &");
  518. sendError("an optional description/body.");
  519. sendError("");
  520. sendError(minTitle);
  521. sendError(maxTitle);
  522. sendError("");
  523. sendRejection(cmd, "please squash to one commit");
  524. return null;
  525. }
  526. // require a reasonable title/subject
  527. String title = tipCommit.getFullMessage().trim().split("\n")[0];
  528. if (title.length() < minLength) {
  529. // reject, title too short
  530. sendError("");
  531. sendError("Please supply a longer title in your commit message!");
  532. sendError("");
  533. sendError(minTitle);
  534. sendError(maxTitle);
  535. sendError("");
  536. sendRejection(cmd, "ticket title is too short [{0}/{1}]", title.length(), maxLength);
  537. return null;
  538. }
  539. if (title.length() > maxLength) {
  540. // reject, title too long
  541. sendError("");
  542. sendError("Please supply a more concise title in your commit message!");
  543. sendError("");
  544. sendError(minTitle);
  545. sendError(maxTitle);
  546. sendError("");
  547. sendRejection(cmd, "ticket title is too long [{0}/{1}]", title.length(), maxLength);
  548. return null;
  549. }
  550. // assign new id
  551. long ticketId = ticketService.assignNewId(repository);
  552. // create the patchset command
  553. psCmd = new PatchsetCommand(user.username, patchset);
  554. psCmd.newTicket(tipCommit, forBranch, ticketId, cmd.getRefName());
  555. } else {
  556. /*
  557. * EXISTING TICKET
  558. */
  559. Patchset patchset = newPatchset(ticket, mergeBase.getName(), tipCommit.getName());
  560. psCmd = new PatchsetCommand(user.username, patchset);
  561. psCmd.updateTicket(tipCommit, forBranch, ticket, cmd.getRefName());
  562. }
  563. // confirm user can push the patchset
  564. boolean pushPermitted = ticket == null
  565. || !ticket.hasPatchsets()
  566. || ticket.isAuthor(user.username)
  567. || ticket.isPatchsetAuthor(user.username)
  568. || ticket.isResponsible(user.username)
  569. || user.canPush(repository);
  570. switch (psCmd.getPatchsetType()) {
  571. case Proposal:
  572. // proposals (first patchset) are always acceptable
  573. break;
  574. case FastForward:
  575. // patchset updates must be permitted
  576. if (!pushPermitted) {
  577. // reject
  578. sendError("");
  579. sendError("To push a patchset to this ticket one of the following must be true:");
  580. sendError(" 1. you created the ticket");
  581. sendError(" 2. you created the first patchset");
  582. sendError(" 3. you are specified as responsible for the ticket");
  583. sendError(" 4. you are listed as a reviewer for the ticket");
  584. sendError(" 5. you have push (RW) permission to {0}", repository.name);
  585. sendError("");
  586. sendRejection(cmd, "not permitted to push to ticket {0,number,0}", ticket.number);
  587. return null;
  588. }
  589. break;
  590. default:
  591. // non-fast-forward push
  592. if (!pushPermitted) {
  593. // reject
  594. sendRejection(cmd, "non-fast-forward ({0})", psCmd.getPatchsetType());
  595. return null;
  596. }
  597. break;
  598. }
  599. return psCmd;
  600. }
  601. /**
  602. * Creates or updates an ticket with the specified patchset.
  603. *
  604. * @param cmd
  605. * @return a ticket if the creation or update was successful
  606. */
  607. private TicketModel processPatchset(PatchsetCommand cmd) {
  608. Change change = cmd.getChange();
  609. if (cmd.isNewTicket()) {
  610. // create the ticket object
  611. TicketModel ticket = ticketService.createTicket(repository, cmd.getTicketId(), change);
  612. if (ticket != null) {
  613. sendInfo("");
  614. sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
  615. sendInfo("created proposal ticket from patchset");
  616. sendInfo(ticketService.getTicketUrl(ticket));
  617. sendInfo("");
  618. // log the new patch ref
  619. RefLogUtils.updateRefLog(user, getRepository(),
  620. Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
  621. return ticket;
  622. } else {
  623. sendError("FAILED to create ticket");
  624. }
  625. } else {
  626. // update an existing ticket
  627. TicketModel ticket = ticketService.updateTicket(repository, cmd.getTicketId(), change);
  628. if (ticket != null) {
  629. sendInfo("");
  630. sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
  631. if (change.patchset.rev == 1) {
  632. // new patchset
  633. sendInfo("uploaded patchset {0} ({1})", change.patchset.number, change.patchset.type.toString());
  634. } else {
  635. // updated patchset
  636. sendInfo("added {0} {1} to patchset {2}",
  637. change.patchset.added,
  638. change.patchset.added == 1 ? "commit" : "commits",
  639. change.patchset.number);
  640. }
  641. sendInfo(ticketService.getTicketUrl(ticket));
  642. sendInfo("");
  643. // log the new patchset ref
  644. RefLogUtils.updateRefLog(user, getRepository(),
  645. Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
  646. // return the updated ticket
  647. return ticket;
  648. } else {
  649. sendError("FAILED to upload {0} for ticket {1,number,0}", change.patchset, cmd.getTicketId());
  650. }
  651. }
  652. return null;
  653. }
  654. /**
  655. * Automatically closes open tickets that have been merged to their integration
  656. * branch by a client.
  657. *
  658. * @param cmd
  659. */
  660. private Collection<TicketModel> processMergedTickets(ReceiveCommand cmd) {
  661. Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
  662. final RevWalk rw = getRevWalk();
  663. try {
  664. rw.reset();
  665. rw.markStart(rw.parseCommit(cmd.getNewId()));
  666. if (!ObjectId.zeroId().equals(cmd.getOldId())) {
  667. rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
  668. }
  669. RevCommit c;
  670. while ((c = rw.next()) != null) {
  671. rw.parseBody(c);
  672. long ticketNumber = identifyTicket(c, true);
  673. if (ticketNumber == 0L || mergedTickets.containsKey(ticketNumber)) {
  674. continue;
  675. }
  676. TicketModel ticket = ticketService.getTicket(repository, ticketNumber);
  677. String integrationBranch;
  678. if (StringUtils.isEmpty(ticket.mergeTo)) {
  679. // unspecified integration branch
  680. integrationBranch = null;
  681. } else {
  682. // specified integration branch
  683. integrationBranch = Constants.R_HEADS + ticket.mergeTo;
  684. }
  685. // ticket must be open and, if specified, the ref must match the integration branch
  686. if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
  687. continue;
  688. }
  689. String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
  690. boolean knownPatchset = false;
  691. Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
  692. if (refs != null) {
  693. for (Ref ref : refs) {
  694. if (ref.getName().startsWith(baseRef)) {
  695. knownPatchset = true;
  696. break;
  697. }
  698. }
  699. }
  700. String mergeSha = c.getName();
  701. String mergeTo = Repository.shortenRefName(cmd.getRefName());
  702. Change change;
  703. Patchset patchset;
  704. if (knownPatchset) {
  705. // identify merged patchset by the patchset tip
  706. patchset = null;
  707. for (Patchset ps : ticket.getPatchsets()) {
  708. if (ps.tip.equals(mergeSha)) {
  709. patchset = ps;
  710. break;
  711. }
  712. }
  713. if (patchset == null) {
  714. // should not happen - unless ticket has been hacked
  715. sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
  716. mergeSha, ticket.number);
  717. continue;
  718. }
  719. // create a new change
  720. change = new Change(user.username);
  721. } else {
  722. // new patchset pushed by user
  723. String base = cmd.getOldId().getName();
  724. patchset = newPatchset(ticket, base, mergeSha);
  725. PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
  726. psCmd.updateTicket(c, mergeTo, ticket, null);
  727. // create a ticket patchset ref
  728. updateRef(psCmd.getPatchsetBranch(), c.getId());
  729. RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId());
  730. updateReflog(ru);
  731. // create a change from the patchset command
  732. change = psCmd.getChange();
  733. }
  734. // set the common change data about the merge
  735. change.setField(Field.status, Status.Merged);
  736. change.setField(Field.mergeSha, mergeSha);
  737. change.setField(Field.mergeTo, mergeTo);
  738. if (StringUtils.isEmpty(ticket.responsible)) {
  739. // unassigned tickets are assigned to the closer
  740. change.setField(Field.responsible, user.username);
  741. }
  742. ticket = ticketService.updateTicket(repository, ticket.number, change);
  743. if (ticket != null) {
  744. sendInfo("");
  745. sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
  746. sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
  747. sendInfo(ticketService.getTicketUrl(ticket));
  748. sendInfo("");
  749. mergedTickets.put(ticket.number, ticket);
  750. } else {
  751. String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
  752. sendError("FAILED to close ticket {0,number,0} by push of {1}", ticketNumber, shortid);
  753. }
  754. }
  755. } catch (IOException e) {
  756. LOGGER.error("Can't scan for changes to close", e);
  757. } finally {
  758. rw.reset();
  759. }
  760. return mergedTickets.values();
  761. }
  762. /**
  763. * Try to identify a ticket id from the commit.
  764. *
  765. * @param commit
  766. * @param parseMessage
  767. * @return a ticket id or 0
  768. */
  769. private long identifyTicket(RevCommit commit, boolean parseMessage) {
  770. // try lookup by change ref
  771. Map<AnyObjectId, Set<Ref>> map = getRepository().getAllRefsByPeeledObjectId();
  772. Set<Ref> refs = map.get(commit.getId());
  773. if (!ArrayUtils.isEmpty(refs)) {
  774. for (Ref ref : refs) {
  775. long number = PatchsetCommand.getTicketNumber(ref.getName());
  776. if (number > 0) {
  777. return number;
  778. }
  779. }
  780. }
  781. if (parseMessage) {
  782. // parse commit message looking for fixes/closes #n
  783. Pattern p = Pattern.compile("(?:fixes|closes)[\\s-]+#?(\\d+)", Pattern.CASE_INSENSITIVE);
  784. Matcher m = p.matcher(commit.getFullMessage());
  785. while (m.find()) {
  786. String val = m.group();
  787. return Long.parseLong(val);
  788. }
  789. }
  790. return 0L;
  791. }
  792. private int countCommits(String baseId, String tipId) {
  793. int count = 0;
  794. RevWalk walk = getRevWalk();
  795. walk.reset();
  796. walk.sort(RevSort.TOPO);
  797. walk.sort(RevSort.REVERSE, true);
  798. try {
  799. RevCommit tip = walk.parseCommit(getRepository().resolve(tipId));
  800. RevCommit base = walk.parseCommit(getRepository().resolve(baseId));
  801. walk.markStart(tip);
  802. walk.markUninteresting(base);
  803. for (;;) {
  804. RevCommit c = walk.next();
  805. if (c == null) {
  806. break;
  807. }
  808. count++;
  809. }
  810. } catch (IOException e) {
  811. // Should never happen, the core receive process would have
  812. // identified the missing object earlier before we got control.
  813. LOGGER.error("failed to get commit count", e);
  814. return 0;
  815. } finally {
  816. walk.release();
  817. }
  818. return count;
  819. }
  820. /**
  821. * Creates a new patchset with metadata.
  822. *
  823. * @param ticket
  824. * @param mergeBase
  825. * @param tip
  826. */
  827. private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
  828. int totalCommits = countCommits(mergeBase, tip);
  829. Patchset newPatchset = new Patchset();
  830. newPatchset.tip = tip;
  831. newPatchset.base = mergeBase;
  832. newPatchset.commits = totalCommits;
  833. Patchset currPatchset = ticket == null ? null : ticket.getCurrentPatchset();
  834. if (currPatchset == null) {
  835. /*
  836. * PROPOSAL PATCHSET
  837. * patchset 1, rev 1
  838. */
  839. newPatchset.number = 1;
  840. newPatchset.rev = 1;
  841. newPatchset.type = PatchsetType.Proposal;
  842. // diffstat from merge base
  843. DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
  844. newPatchset.insertions = diffStat.getInsertions();
  845. newPatchset.deletions = diffStat.getDeletions();
  846. } else {
  847. /*
  848. * PATCHSET UPDATE
  849. */
  850. int added = totalCommits - currPatchset.commits;
  851. boolean ff = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, tip);
  852. boolean squash = added < 0;
  853. boolean rebase = !currPatchset.base.equals(mergeBase);
  854. // determine type, number and rev of the patchset
  855. if (ff) {
  856. /*
  857. * FAST-FORWARD
  858. * patchset number preserved, rev incremented
  859. */
  860. boolean merged = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, ticket.mergeTo);
  861. if (merged) {
  862. // current patchset was already merged
  863. // new patchset, mark as rebase
  864. newPatchset.type = PatchsetType.Rebase;
  865. newPatchset.number = currPatchset.number + 1;
  866. newPatchset.rev = 1;
  867. // diffstat from parent
  868. DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
  869. newPatchset.insertions = diffStat.getInsertions();
  870. newPatchset.deletions = diffStat.getDeletions();
  871. } else {
  872. // FF update to patchset
  873. newPatchset.type = PatchsetType.FastForward;
  874. newPatchset.number = currPatchset.number;
  875. newPatchset.rev = currPatchset.rev + 1;
  876. newPatchset.parent = currPatchset.tip;
  877. // diffstat from parent
  878. DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), currPatchset.tip, tip);
  879. newPatchset.insertions = diffStat.getInsertions();
  880. newPatchset.deletions = diffStat.getDeletions();
  881. }
  882. } else {
  883. /*
  884. * NON-FAST-FORWARD
  885. * new patchset, rev 1
  886. */
  887. if (rebase && squash) {
  888. newPatchset.type = PatchsetType.Rebase_Squash;
  889. newPatchset.number = currPatchset.number + 1;
  890. newPatchset.rev = 1;
  891. } else if (squash) {
  892. newPatchset.type = PatchsetType.Squash;
  893. newPatchset.number = currPatchset.number + 1;
  894. newPatchset.rev = 1;
  895. } else if (rebase) {
  896. newPatchset.type = PatchsetType.Rebase;
  897. newPatchset.number = currPatchset.number + 1;
  898. newPatchset.rev = 1;
  899. } else {
  900. newPatchset.type = PatchsetType.Amend;
  901. newPatchset.number = currPatchset.number + 1;
  902. newPatchset.rev = 1;
  903. }
  904. // diffstat from merge base
  905. DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
  906. newPatchset.insertions = diffStat.getInsertions();
  907. newPatchset.deletions = diffStat.getDeletions();
  908. }
  909. if (added > 0) {
  910. // ignore squash (negative add)
  911. newPatchset.added = added;
  912. }
  913. }
  914. return newPatchset;
  915. }
  916. private RefUpdate updateRef(String ref, ObjectId newId) {
  917. ObjectId ticketRefId = ObjectId.zeroId();
  918. try {
  919. ticketRefId = getRepository().resolve(ref);
  920. } catch (Exception e) {
  921. // ignore
  922. }
  923. try {
  924. RefUpdate ru = getRepository().updateRef(ref, false);
  925. ru.setRefLogIdent(getRefLogIdent());
  926. ru.setForceUpdate(true);
  927. ru.setExpectedOldObjectId(ticketRefId);
  928. ru.setNewObjectId(newId);
  929. RefUpdate.Result result = ru.update(getRevWalk());
  930. if (result == RefUpdate.Result.LOCK_FAILURE) {
  931. sendError("Failed to obtain lock when updating {0}:{1}", repository.name, ref);
  932. sendError("Perhaps an administrator should remove {0}/{1}.lock?", getRepository().getDirectory(), ref);
  933. return null;
  934. }
  935. return ru;
  936. } catch (IOException e) {
  937. LOGGER.error("failed to update ref " + ref, e);
  938. sendError("There was an error updating ref {0}:{1}", repository.name, ref);
  939. }
  940. return null;
  941. }
  942. private void updateReflog(RefUpdate ru) {
  943. if (ru == null) {
  944. return;
  945. }
  946. ReceiveCommand.Type type = null;
  947. switch (ru.getResult()) {
  948. case NEW:
  949. type = Type.CREATE;
  950. break;
  951. case FAST_FORWARD:
  952. type = Type.UPDATE;
  953. break;
  954. case FORCED:
  955. type = Type.UPDATE_NONFASTFORWARD;
  956. break;
  957. default:
  958. LOGGER.error(MessageFormat.format("unexpected ref update type {0} for {1}",
  959. ru.getResult(), ru.getName()));
  960. return;
  961. }
  962. ReceiveCommand cmd = new ReceiveCommand(ru.getOldObjectId(), ru.getNewObjectId(), ru.getName(), type);
  963. RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
  964. }
  965. /**
  966. * Merge the specified patchset to the integration branch.
  967. *
  968. * @param ticket
  969. * @param patchset
  970. * @return true, if successful
  971. */
  972. public MergeStatus merge(TicketModel ticket) {
  973. PersonIdent committer = new PersonIdent(user.getDisplayName(), StringUtils.isEmpty(user.emailAddress) ? (user.username + "@gitblit") : user.emailAddress);
  974. Patchset patchset = ticket.getCurrentPatchset();
  975. String message = MessageFormat.format("Merged #{0,number,0} \"{1}\"", ticket.number, ticket.title);
  976. Ref oldRef = null;
  977. try {
  978. oldRef = getRepository().getRef(ticket.mergeTo);
  979. } catch (IOException e) {
  980. LOGGER.error("failed to get ref for " + ticket.mergeTo, e);
  981. }
  982. MergeResult mergeResult = JGitUtils.merge(
  983. getRepository(),
  984. patchset.tip,
  985. ticket.mergeTo,
  986. committer,
  987. message);
  988. if (StringUtils.isEmpty(mergeResult.sha)) {
  989. LOGGER.error("FAILED to merge {} to {} ({})", new Object [] { patchset, ticket.mergeTo, mergeResult.status.name() });
  990. return mergeResult.status;
  991. }
  992. Change change = new Change(user.username);
  993. change.setField(Field.status, Status.Merged);
  994. change.setField(Field.mergeSha, mergeResult.sha);
  995. change.setField(Field.mergeTo, ticket.mergeTo);
  996. if (StringUtils.isEmpty(ticket.responsible)) {
  997. // unassigned tickets are assigned to the closer
  998. change.setField(Field.responsible, user.username);
  999. }
  1000. long ticketId = ticket.number;
  1001. ticket = ticketService.updateTicket(repository, ticket.number, change);
  1002. if (ticket != null) {
  1003. ticketNotifier.queueMailing(ticket);
  1004. // update the reflog with the merge
  1005. if (oldRef != null) {
  1006. ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
  1007. ObjectId.fromString(mergeResult.sha), oldRef.getName());
  1008. RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
  1009. }
  1010. return mergeResult.status;
  1011. } else {
  1012. LOGGER.error("FAILED to resolve ticket {} by merge from web ui", ticketId);
  1013. }
  1014. return mergeResult.status;
  1015. }
  1016. public void sendAll() {
  1017. ticketNotifier.sendAll();
  1018. }
  1019. }