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