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