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.

BranchTicketService.java 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. /*
  2. * Copyright 2014 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.tickets;
  17. import java.io.IOException;
  18. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collections;
  22. import java.util.HashSet;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.TreeSet;
  27. import java.util.concurrent.ConcurrentHashMap;
  28. import java.util.concurrent.TimeUnit;
  29. import java.util.concurrent.atomic.AtomicLong;
  30. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  31. import org.eclipse.jgit.dircache.DirCache;
  32. import org.eclipse.jgit.dircache.DirCacheBuilder;
  33. import org.eclipse.jgit.dircache.DirCacheEntry;
  34. import org.eclipse.jgit.events.RefsChangedEvent;
  35. import org.eclipse.jgit.events.RefsChangedListener;
  36. import org.eclipse.jgit.lib.FileMode;
  37. import org.eclipse.jgit.lib.ObjectId;
  38. import org.eclipse.jgit.lib.ObjectInserter;
  39. import org.eclipse.jgit.lib.PersonIdent;
  40. import org.eclipse.jgit.lib.Ref;
  41. import org.eclipse.jgit.lib.RefRename;
  42. import org.eclipse.jgit.lib.RefUpdate.Result;
  43. import org.eclipse.jgit.lib.Repository;
  44. import org.eclipse.jgit.revwalk.RevCommit;
  45. import org.eclipse.jgit.revwalk.RevTree;
  46. import org.eclipse.jgit.revwalk.RevWalk;
  47. import org.eclipse.jgit.transport.ReceiveCommand;
  48. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  49. import org.eclipse.jgit.treewalk.TreeWalk;
  50. import com.gitblit.Constants;
  51. import com.gitblit.git.ReceiveCommandEvent;
  52. import com.gitblit.manager.INotificationManager;
  53. import com.gitblit.manager.IPluginManager;
  54. import com.gitblit.manager.IRepositoryManager;
  55. import com.gitblit.manager.IRuntimeManager;
  56. import com.gitblit.manager.IUserManager;
  57. import com.gitblit.models.PathModel;
  58. import com.gitblit.models.PathModel.PathChangeModel;
  59. import com.gitblit.models.RefModel;
  60. import com.gitblit.models.RepositoryModel;
  61. import com.gitblit.models.TicketModel;
  62. import com.gitblit.models.TicketModel.Attachment;
  63. import com.gitblit.models.TicketModel.Change;
  64. import com.gitblit.utils.ArrayUtils;
  65. import com.gitblit.utils.JGitUtils;
  66. import com.gitblit.utils.StringUtils;
  67. import com.google.inject.Inject;
  68. import com.google.inject.Singleton;
  69. /**
  70. * Implementation of a ticket service based on an orphan branch. All tickets
  71. * are serialized as a list of JSON changes and persisted in a hashed directory
  72. * structure, similar to the standard git loose object structure.
  73. *
  74. * @author James Moger
  75. *
  76. */
  77. @Singleton
  78. public class BranchTicketService extends ITicketService implements RefsChangedListener {
  79. public static final String BRANCH = "refs/meta/gitblit/tickets";
  80. private static final String JOURNAL = "journal.json";
  81. private static final String ID_PATH = "id/";
  82. private final Map<String, AtomicLong> lastAssignedId;
  83. @Inject
  84. public BranchTicketService(
  85. IRuntimeManager runtimeManager,
  86. IPluginManager pluginManager,
  87. INotificationManager notificationManager,
  88. IUserManager userManager,
  89. IRepositoryManager repositoryManager) {
  90. super(runtimeManager,
  91. pluginManager,
  92. notificationManager,
  93. userManager,
  94. repositoryManager);
  95. lastAssignedId = new ConcurrentHashMap<String, AtomicLong>();
  96. // register the branch ticket service for repository ref changes
  97. Repository.getGlobalListenerList().addRefsChangedListener(this);
  98. }
  99. @Override
  100. public BranchTicketService start() {
  101. log.info("{} started", getClass().getSimpleName());
  102. return this;
  103. }
  104. @Override
  105. protected void resetCachesImpl() {
  106. lastAssignedId.clear();
  107. }
  108. @Override
  109. protected void resetCachesImpl(RepositoryModel repository) {
  110. if (lastAssignedId.containsKey(repository.name)) {
  111. lastAssignedId.get(repository.name).set(0);
  112. }
  113. }
  114. @Override
  115. protected void close() {
  116. }
  117. /**
  118. * Listen for tickets branch changes and (re)index tickets, as appropriate
  119. */
  120. @Override
  121. public synchronized void onRefsChanged(RefsChangedEvent event) {
  122. if (!(event instanceof ReceiveCommandEvent)) {
  123. return;
  124. }
  125. ReceiveCommandEvent branchUpdate = (ReceiveCommandEvent) event;
  126. RepositoryModel repository = branchUpdate.model;
  127. ReceiveCommand cmd = branchUpdate.cmd;
  128. try {
  129. switch (cmd.getType()) {
  130. case CREATE:
  131. case UPDATE_NONFASTFORWARD:
  132. // reindex everything
  133. reindex(repository);
  134. break;
  135. case UPDATE:
  136. // incrementally index ticket updates
  137. resetCaches(repository);
  138. long start = System.nanoTime();
  139. log.info("incrementally indexing {} ticket branch due to received ref update", repository.name);
  140. Repository db = repositoryManager.getRepository(repository.name);
  141. try {
  142. Set<Long> ids = new HashSet<Long>();
  143. List<PathChangeModel> paths = JGitUtils.getFilesInRange(db,
  144. cmd.getOldId().getName(), cmd.getNewId().getName());
  145. for (PathChangeModel path : paths) {
  146. String name = path.name.substring(path.name.lastIndexOf('/') + 1);
  147. if (!JOURNAL.equals(name)) {
  148. continue;
  149. }
  150. String tid = path.path.split("/")[2];
  151. long ticketId = Long.parseLong(tid);
  152. if (!ids.contains(ticketId)) {
  153. ids.add(ticketId);
  154. TicketModel ticket = getTicket(repository, ticketId);
  155. log.info(MessageFormat.format("indexing ticket #{0,number,0}: {1}",
  156. ticketId, ticket.title));
  157. indexer.index(ticket);
  158. }
  159. }
  160. long end = System.nanoTime();
  161. log.info("incremental indexing of {0} ticket(s) completed in {1} msecs",
  162. ids.size(), TimeUnit.NANOSECONDS.toMillis(end - start));
  163. } finally {
  164. db.close();
  165. }
  166. break;
  167. default:
  168. log.warn("Unexpected receive type {} in BranchTicketService.onRefsChanged" + cmd.getType());
  169. break;
  170. }
  171. } catch (Exception e) {
  172. log.error("failed to reindex " + repository.name, e);
  173. }
  174. }
  175. /**
  176. * Returns a RefModel for the refs/meta/gitblit/tickets branch in the repository.
  177. * If the branch can not be found, null is returned.
  178. *
  179. * @return a refmodel for the gitblit tickets branch or null
  180. */
  181. private RefModel getTicketsBranch(Repository db) {
  182. List<RefModel> refs = JGitUtils.getRefs(db, "refs/");
  183. Ref oldRef = null;
  184. for (RefModel ref : refs) {
  185. if (ref.reference.getName().equals(BRANCH)) {
  186. return ref;
  187. } else if (ref.reference.getName().equals("refs/gitblit/tickets")) {
  188. oldRef = ref.reference;
  189. }
  190. }
  191. if (oldRef != null) {
  192. // rename old ref to refs/meta/gitblit/tickets
  193. RefRename cmd;
  194. try {
  195. cmd = db.renameRef(oldRef.getName(), BRANCH);
  196. cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
  197. cmd.setRefLogMessage("renamed " + oldRef.getName() + " => " + BRANCH);
  198. Result res = cmd.rename();
  199. switch (res) {
  200. case RENAMED:
  201. log.info(db.getDirectory() + " " + cmd.getRefLogMessage());
  202. return getTicketsBranch(db);
  203. default:
  204. log.error("failed to rename " + oldRef.getName() + " => " + BRANCH + " (" + res.name() + ")");
  205. }
  206. } catch (IOException e) {
  207. log.error("failed to rename tickets branch", e);
  208. }
  209. }
  210. return null;
  211. }
  212. /**
  213. * Creates the refs/meta/gitblit/tickets branch.
  214. * @param db
  215. */
  216. private void createTicketsBranch(Repository db) {
  217. JGitUtils.createOrphanBranch(db, BRANCH, null);
  218. }
  219. /**
  220. * Returns the ticket path. This follows the same scheme as Git's object
  221. * store path where the first two characters of the hash id are the root
  222. * folder with the remaining characters as a subfolder within that folder.
  223. *
  224. * @param ticketId
  225. * @return the root path of the ticket content on the refs/meta/gitblit/tickets branch
  226. */
  227. private String toTicketPath(long ticketId) {
  228. StringBuilder sb = new StringBuilder();
  229. sb.append(ID_PATH);
  230. long m = ticketId % 100L;
  231. if (m < 10) {
  232. sb.append('0');
  233. }
  234. sb.append(m);
  235. sb.append('/');
  236. sb.append(ticketId);
  237. return sb.toString();
  238. }
  239. /**
  240. * Returns the path to the attachment for the specified ticket.
  241. *
  242. * @param ticketId
  243. * @param filename
  244. * @return the path to the specified attachment
  245. */
  246. private String toAttachmentPath(long ticketId, String filename) {
  247. return toTicketPath(ticketId) + "/attachments/" + filename;
  248. }
  249. /**
  250. * Reads a file from the tickets branch.
  251. *
  252. * @param db
  253. * @param file
  254. * @return the file content or null
  255. */
  256. private String readTicketsFile(Repository db, String file) {
  257. RevWalk rw = null;
  258. try {
  259. ObjectId treeId = db.resolve(BRANCH + "^{tree}");
  260. if (treeId == null) {
  261. return null;
  262. }
  263. rw = new RevWalk(db);
  264. RevTree tree = rw.lookupTree(treeId);
  265. if (tree != null) {
  266. return JGitUtils.getStringContent(db, tree, file, Constants.ENCODING);
  267. }
  268. } catch (IOException e) {
  269. log.error("failed to read " + file, e);
  270. } finally {
  271. if (rw != null) {
  272. rw.close();
  273. }
  274. }
  275. return null;
  276. }
  277. /**
  278. * Writes a file to the tickets branch.
  279. *
  280. * @param db
  281. * @param file
  282. * @param content
  283. * @param createdBy
  284. * @param msg
  285. */
  286. private void writeTicketsFile(Repository db, String file, String content, String createdBy, String msg) {
  287. if (getTicketsBranch(db) == null) {
  288. createTicketsBranch(db);
  289. }
  290. DirCache newIndex = DirCache.newInCore();
  291. DirCacheBuilder builder = newIndex.builder();
  292. ObjectInserter inserter = db.newObjectInserter();
  293. try {
  294. // create an index entry for the revised index
  295. final DirCacheEntry idIndexEntry = new DirCacheEntry(file);
  296. idIndexEntry.setLength(content.length());
  297. idIndexEntry.setLastModified(System.currentTimeMillis());
  298. idIndexEntry.setFileMode(FileMode.REGULAR_FILE);
  299. // insert new ticket index
  300. idIndexEntry.setObjectId(inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB,
  301. content.getBytes(Constants.ENCODING)));
  302. // add to temporary in-core index
  303. builder.add(idIndexEntry);
  304. Set<String> ignorePaths = new HashSet<String>();
  305. ignorePaths.add(file);
  306. for (DirCacheEntry entry : JGitUtils.getTreeEntries(db, BRANCH, ignorePaths)) {
  307. builder.add(entry);
  308. }
  309. // finish temporary in-core index used for this commit
  310. builder.finish();
  311. // commit the change
  312. commitIndex(db, newIndex, createdBy, msg);
  313. } catch (ConcurrentRefUpdateException e) {
  314. log.error("", e);
  315. } catch (IOException e) {
  316. log.error("", e);
  317. } finally {
  318. inserter.close();
  319. }
  320. }
  321. /**
  322. * Ensures that we have a ticket for this ticket id.
  323. *
  324. * @param repository
  325. * @param ticketId
  326. * @return true if the ticket exists
  327. */
  328. @Override
  329. public boolean hasTicket(RepositoryModel repository, long ticketId) {
  330. boolean hasTicket = false;
  331. Repository db = repositoryManager.getRepository(repository.name);
  332. try {
  333. RefModel ticketsBranch = getTicketsBranch(db);
  334. if (ticketsBranch == null) {
  335. return false;
  336. }
  337. String ticketPath = toTicketPath(ticketId);
  338. RevCommit tip = JGitUtils.getCommit(db, BRANCH);
  339. hasTicket = !JGitUtils.getFilesInPath(db, ticketPath, tip).isEmpty();
  340. } finally {
  341. db.close();
  342. }
  343. return hasTicket;
  344. }
  345. /**
  346. * Returns the assigned ticket ids.
  347. *
  348. * @return the assigned ticket ids
  349. */
  350. @Override
  351. public synchronized Set<Long> getIds(RepositoryModel repository) {
  352. Repository db = repositoryManager.getRepository(repository.name);
  353. try {
  354. if (getTicketsBranch(db) == null) {
  355. return Collections.emptySet();
  356. }
  357. Set<Long> ids = new TreeSet<Long>();
  358. List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
  359. for (PathModel path : paths) {
  360. String name = path.name.substring(path.name.lastIndexOf('/') + 1);
  361. if (!JOURNAL.equals(name)) {
  362. continue;
  363. }
  364. String tid = path.path.split("/")[2];
  365. long ticketId = Long.parseLong(tid);
  366. ids.add(ticketId);
  367. }
  368. return ids;
  369. } finally {
  370. if (db != null) {
  371. db.close();
  372. }
  373. }
  374. }
  375. /**
  376. * Assigns a new ticket id.
  377. *
  378. * @param repository
  379. * @return a new long id
  380. */
  381. @Override
  382. public synchronized long assignNewId(RepositoryModel repository) {
  383. long newId = 0L;
  384. Repository db = repositoryManager.getRepository(repository.name);
  385. try {
  386. if (getTicketsBranch(db) == null) {
  387. createTicketsBranch(db);
  388. }
  389. // identify current highest ticket id by scanning the paths in the tip tree
  390. if (!lastAssignedId.containsKey(repository.name)) {
  391. lastAssignedId.put(repository.name, new AtomicLong(0));
  392. }
  393. AtomicLong lastId = lastAssignedId.get(repository.name);
  394. if (lastId.get() <= 0) {
  395. Set<Long> ids = getIds(repository);
  396. for (long id : ids) {
  397. if (id > lastId.get()) {
  398. lastId.set(id);
  399. }
  400. }
  401. }
  402. // assign the id and touch an empty journal to hold it's place
  403. newId = lastId.incrementAndGet();
  404. String journalPath = toTicketPath(newId) + "/" + JOURNAL;
  405. writeTicketsFile(db, journalPath, "", "gitblit", "assigned id #" + newId);
  406. } finally {
  407. db.close();
  408. }
  409. return newId;
  410. }
  411. /**
  412. * Returns all the tickets in the repository. Querying tickets from the
  413. * repository requires deserializing all tickets. This is an expensive
  414. * process and not recommended. Tickets are indexed by Lucene and queries
  415. * should be executed against that index.
  416. *
  417. * @param repository
  418. * @param filter
  419. * optional filter to only return matching results
  420. * @return a list of tickets
  421. */
  422. @Override
  423. public List<TicketModel> getTickets(RepositoryModel repository, TicketFilter filter) {
  424. List<TicketModel> list = new ArrayList<TicketModel>();
  425. Repository db = repositoryManager.getRepository(repository.name);
  426. try {
  427. RefModel ticketsBranch = getTicketsBranch(db);
  428. if (ticketsBranch == null) {
  429. return list;
  430. }
  431. // Collect the set of all json files
  432. List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
  433. // Deserialize each ticket and optionally filter out unwanted tickets
  434. for (PathModel path : paths) {
  435. String name = path.name.substring(path.name.lastIndexOf('/') + 1);
  436. if (!JOURNAL.equals(name)) {
  437. continue;
  438. }
  439. String json = readTicketsFile(db, path.path);
  440. if (StringUtils.isEmpty(json)) {
  441. // journal was touched but no changes were written
  442. continue;
  443. }
  444. try {
  445. // Reconstruct ticketId from the path
  446. // id/26/326/journal.json
  447. String tid = path.path.split("/")[2];
  448. long ticketId = Long.parseLong(tid);
  449. List<Change> changes = TicketSerializer.deserializeJournal(json);
  450. if (ArrayUtils.isEmpty(changes)) {
  451. log.warn("Empty journal for {}:{}", repository, path.path);
  452. continue;
  453. }
  454. TicketModel ticket = TicketModel.buildTicket(changes);
  455. ticket.project = repository.projectPath;
  456. ticket.repository = repository.name;
  457. ticket.number = ticketId;
  458. // add the ticket, conditionally, to the list
  459. if (filter == null) {
  460. list.add(ticket);
  461. } else {
  462. if (filter.accept(ticket)) {
  463. list.add(ticket);
  464. }
  465. }
  466. } catch (Exception e) {
  467. log.error("failed to deserialize {}/{}\n{}",
  468. new Object [] { repository, path.path, e.getMessage()});
  469. log.error(null, e);
  470. }
  471. }
  472. // sort the tickets by creation
  473. Collections.sort(list);
  474. return list;
  475. } finally {
  476. db.close();
  477. }
  478. }
  479. /**
  480. * Retrieves the ticket from the repository by first looking-up the changeId
  481. * associated with the ticketId.
  482. *
  483. * @param repository
  484. * @param ticketId
  485. * @return a ticket, if it exists, otherwise null
  486. */
  487. @Override
  488. protected TicketModel getTicketImpl(RepositoryModel repository, long ticketId) {
  489. Repository db = repositoryManager.getRepository(repository.name);
  490. try {
  491. List<Change> changes = getJournal(db, ticketId);
  492. if (ArrayUtils.isEmpty(changes)) {
  493. log.warn("Empty journal for {}:{}", repository, ticketId);
  494. return null;
  495. }
  496. TicketModel ticket = TicketModel.buildTicket(changes);
  497. if (ticket != null) {
  498. ticket.project = repository.projectPath;
  499. ticket.repository = repository.name;
  500. ticket.number = ticketId;
  501. }
  502. return ticket;
  503. } finally {
  504. db.close();
  505. }
  506. }
  507. /**
  508. * Retrieves the journal for the ticket.
  509. *
  510. * @param repository
  511. * @param ticketId
  512. * @return a journal, if it exists, otherwise null
  513. */
  514. @Override
  515. protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
  516. Repository db = repositoryManager.getRepository(repository.name);
  517. try {
  518. List<Change> changes = getJournal(db, ticketId);
  519. if (ArrayUtils.isEmpty(changes)) {
  520. log.warn("Empty journal for {}:{}", repository, ticketId);
  521. return null;
  522. }
  523. return changes;
  524. } finally {
  525. db.close();
  526. }
  527. }
  528. /**
  529. * Returns the journal for the specified ticket.
  530. *
  531. * @param db
  532. * @param ticketId
  533. * @return a list of changes
  534. */
  535. private List<Change> getJournal(Repository db, long ticketId) {
  536. RefModel ticketsBranch = getTicketsBranch(db);
  537. if (ticketsBranch == null) {
  538. return new ArrayList<Change>();
  539. }
  540. if (ticketId <= 0L) {
  541. return new ArrayList<Change>();
  542. }
  543. String journalPath = toTicketPath(ticketId) + "/" + JOURNAL;
  544. String json = readTicketsFile(db, journalPath);
  545. if (StringUtils.isEmpty(json)) {
  546. return new ArrayList<Change>();
  547. }
  548. List<Change> list = TicketSerializer.deserializeJournal(json);
  549. return list;
  550. }
  551. @Override
  552. public boolean supportsAttachments() {
  553. return true;
  554. }
  555. /**
  556. * Retrieves the specified attachment from a ticket.
  557. *
  558. * @param repository
  559. * @param ticketId
  560. * @param filename
  561. * @return an attachment, if found, null otherwise
  562. */
  563. @Override
  564. public Attachment getAttachment(RepositoryModel repository, long ticketId, String filename) {
  565. if (ticketId <= 0L) {
  566. return null;
  567. }
  568. // deserialize the ticket model so that we have the attachment metadata
  569. TicketModel ticket = getTicket(repository, ticketId);
  570. Attachment attachment = ticket.getAttachment(filename);
  571. // attachment not found
  572. if (attachment == null) {
  573. return null;
  574. }
  575. // retrieve the attachment content
  576. Repository db = repositoryManager.getRepository(repository.name);
  577. try {
  578. String attachmentPath = toAttachmentPath(ticketId, attachment.name);
  579. RevTree tree = JGitUtils.getCommit(db, BRANCH).getTree();
  580. byte[] content = JGitUtils.getByteContent(db, tree, attachmentPath, false);
  581. attachment.content = content;
  582. attachment.size = content.length;
  583. return attachment;
  584. } finally {
  585. db.close();
  586. }
  587. }
  588. /**
  589. * Deletes a ticket from the repository.
  590. *
  591. * @param ticket
  592. * @return true if successful
  593. */
  594. @Override
  595. protected synchronized boolean deleteTicketImpl(RepositoryModel repository, TicketModel ticket, String deletedBy) {
  596. if (ticket == null) {
  597. throw new RuntimeException("must specify a ticket!");
  598. }
  599. boolean success = false;
  600. Repository db = repositoryManager.getRepository(ticket.repository);
  601. try {
  602. RefModel ticketsBranch = getTicketsBranch(db);
  603. if (ticketsBranch == null) {
  604. throw new RuntimeException(BRANCH + " does not exist!");
  605. }
  606. String ticketPath = toTicketPath(ticket.number);
  607. TreeWalk treeWalk = null;
  608. try {
  609. ObjectId treeId = db.resolve(BRANCH + "^{tree}");
  610. // Create the in-memory index of the new/updated ticket
  611. DirCache index = DirCache.newInCore();
  612. DirCacheBuilder builder = index.builder();
  613. // Traverse HEAD to add all other paths
  614. treeWalk = new TreeWalk(db);
  615. int hIdx = -1;
  616. if (treeId != null) {
  617. hIdx = treeWalk.addTree(treeId);
  618. }
  619. treeWalk.setRecursive(true);
  620. while (treeWalk.next()) {
  621. String path = treeWalk.getPathString();
  622. CanonicalTreeParser hTree = null;
  623. if (hIdx != -1) {
  624. hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
  625. }
  626. if (!path.startsWith(ticketPath)) {
  627. // add entries from HEAD for all other paths
  628. if (hTree != null) {
  629. final DirCacheEntry entry = new DirCacheEntry(path);
  630. entry.setObjectId(hTree.getEntryObjectId());
  631. entry.setFileMode(hTree.getEntryFileMode());
  632. // add to temporary in-core index
  633. builder.add(entry);
  634. }
  635. }
  636. }
  637. // finish temporary in-core index used for this commit
  638. builder.finish();
  639. success = commitIndex(db, index, deletedBy, "- " + ticket.number);
  640. } catch (Throwable t) {
  641. log.error(MessageFormat.format("Failed to delete ticket {0,number,0} from {1}",
  642. ticket.number, db.getDirectory()), t);
  643. } finally {
  644. // release the treewalk
  645. if (treeWalk != null) {
  646. treeWalk.close();
  647. }
  648. }
  649. } finally {
  650. db.close();
  651. }
  652. return success;
  653. }
  654. /**
  655. * Commit a ticket change to the repository.
  656. *
  657. * @param repository
  658. * @param ticketId
  659. * @param change
  660. * @return true, if the change was committed
  661. */
  662. @Override
  663. protected synchronized boolean commitChangeImpl(RepositoryModel repository, long ticketId, Change change) {
  664. boolean success = false;
  665. Repository db = repositoryManager.getRepository(repository.name);
  666. try {
  667. DirCache index = createIndex(db, ticketId, change);
  668. success = commitIndex(db, index, change.author, "#" + ticketId);
  669. } catch (Throwable t) {
  670. log.error(MessageFormat.format("Failed to commit ticket {0,number,0} to {1}",
  671. ticketId, db.getDirectory()), t);
  672. } finally {
  673. db.close();
  674. }
  675. return success;
  676. }
  677. /**
  678. * Creates an in-memory index of the ticket change.
  679. *
  680. * @param changeId
  681. * @param change
  682. * @return an in-memory index
  683. * @throws IOException
  684. */
  685. private DirCache createIndex(Repository db, long ticketId, Change change)
  686. throws IOException, ClassNotFoundException, NoSuchFieldException {
  687. String ticketPath = toTicketPath(ticketId);
  688. DirCache newIndex = DirCache.newInCore();
  689. DirCacheBuilder builder = newIndex.builder();
  690. ObjectInserter inserter = db.newObjectInserter();
  691. Set<String> ignorePaths = new TreeSet<String>();
  692. try {
  693. // create/update the journal
  694. // exclude the attachment content
  695. List<Change> changes = getJournal(db, ticketId);
  696. changes.add(change);
  697. String journal = TicketSerializer.serializeJournal(changes).trim();
  698. byte [] journalBytes = journal.getBytes(Constants.ENCODING);
  699. String journalPath = ticketPath + "/" + JOURNAL;
  700. final DirCacheEntry journalEntry = new DirCacheEntry(journalPath);
  701. journalEntry.setLength(journalBytes.length);
  702. journalEntry.setLastModified(change.date.getTime());
  703. journalEntry.setFileMode(FileMode.REGULAR_FILE);
  704. journalEntry.setObjectId(inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, journalBytes));
  705. // add journal to index
  706. builder.add(journalEntry);
  707. ignorePaths.add(journalEntry.getPathString());
  708. // Add any attachments to the index
  709. if (change.hasAttachments()) {
  710. for (Attachment attachment : change.attachments) {
  711. // build a path name for the attachment and mark as ignored
  712. String path = toAttachmentPath(ticketId, attachment.name);
  713. ignorePaths.add(path);
  714. // create an index entry for this attachment
  715. final DirCacheEntry entry = new DirCacheEntry(path);
  716. entry.setLength(attachment.content.length);
  717. entry.setLastModified(change.date.getTime());
  718. entry.setFileMode(FileMode.REGULAR_FILE);
  719. // insert object
  720. entry.setObjectId(inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, attachment.content));
  721. // add to temporary in-core index
  722. builder.add(entry);
  723. }
  724. }
  725. for (DirCacheEntry entry : JGitUtils.getTreeEntries(db, BRANCH, ignorePaths)) {
  726. builder.add(entry);
  727. }
  728. // finish the index
  729. builder.finish();
  730. } finally {
  731. inserter.close();
  732. }
  733. return newIndex;
  734. }
  735. private boolean commitIndex(Repository db, DirCache index, String author, String message) throws IOException, ConcurrentRefUpdateException {
  736. final boolean forceCommit = true;
  737. boolean success = false;
  738. ObjectId headId = db.resolve(BRANCH + "^{commit}");
  739. if (headId == null) {
  740. // create the branch
  741. createTicketsBranch(db);
  742. }
  743. success = JGitUtils.commitIndex(db, BRANCH, index, headId, forceCommit, author, "gitblit@localhost", message);
  744. return success;
  745. }
  746. @Override
  747. protected boolean deleteAllImpl(RepositoryModel repository) {
  748. Repository db = repositoryManager.getRepository(repository.name);
  749. try {
  750. RefModel branch = getTicketsBranch(db);
  751. if (branch != null) {
  752. return JGitUtils.deleteBranchRef(db, BRANCH);
  753. }
  754. return true;
  755. } catch (Exception e) {
  756. log.error(null, e);
  757. } finally {
  758. if (db != null) {
  759. db.close();
  760. }
  761. }
  762. return false;
  763. }
  764. @Override
  765. protected boolean renameImpl(RepositoryModel oldRepository, RepositoryModel newRepository) {
  766. return true;
  767. }
  768. @Override
  769. public String toString() {
  770. return getClass().getSimpleName();
  771. }
  772. }