/**
   * Creates an in-memory index of the ticket change.
   *
   * @param changeId
   * @param change
   * @return an in-memory index
   * @throws IOException
   */
  private DirCache createIndex(Repository db, long ticketId, Change change)
      throws IOException, ClassNotFoundException, NoSuchFieldException {

    String ticketPath = toTicketPath(ticketId);
    DirCache newIndex = DirCache.newInCore();
    DirCacheBuilder builder = newIndex.builder();

    Set<String> ignorePaths = new TreeSet<String>();
    try (ObjectInserter inserter = db.newObjectInserter()) {
      // create/update the journal
      // exclude the attachment content
      List<Change> changes = getJournal(db, ticketId);
      changes.add(change);
      String journal = TicketSerializer.serializeJournal(changes).trim();

      byte[] journalBytes = journal.getBytes(Constants.ENCODING);
      String journalPath = ticketPath + "/" + JOURNAL;
      final DirCacheEntry journalEntry = new DirCacheEntry(journalPath);
      journalEntry.setLength(journalBytes.length);
      journalEntry.setLastModified(change.date.getTime());
      journalEntry.setFileMode(FileMode.REGULAR_FILE);
      journalEntry.setObjectId(
          inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, journalBytes));

      // add journal to index
      builder.add(journalEntry);
      ignorePaths.add(journalEntry.getPathString());

      // Add any attachments to the index
      if (change.hasAttachments()) {
        for (Attachment attachment : change.attachments) {
          // build a path name for the attachment and mark as ignored
          String path = toAttachmentPath(ticketId, attachment.name);
          ignorePaths.add(path);

          // create an index entry for this attachment
          final DirCacheEntry entry = new DirCacheEntry(path);
          entry.setLength(attachment.content.length);
          entry.setLastModified(change.date.getTime());
          entry.setFileMode(FileMode.REGULAR_FILE);

          // insert object
          entry.setObjectId(
              inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, attachment.content));

          // add to temporary in-core index
          builder.add(entry);
        }
      }

      for (DirCacheEntry entry : getTreeEntries(db, ignorePaths)) {
        builder.add(entry);
      }

      // finish the index
      builder.finish();
    }
    return newIndex;
  }
  /**
   * Returns the journal for the specified ticket.
   *
   * @param db
   * @param ticketId
   * @return a list of changes
   */
  private List<Change> getJournal(Repository db, long ticketId) {
    RefModel ticketsBranch = getTicketsBranch(db);
    if (ticketsBranch == null) {
      return new ArrayList<Change>();
    }

    if (ticketId <= 0L) {
      return new ArrayList<Change>();
    }

    String journalPath = toTicketPath(ticketId) + "/" + JOURNAL;
    String json = readTicketsFile(db, journalPath);
    if (StringUtils.isEmpty(json)) {
      return new ArrayList<Change>();
    }
    List<Change> list = TicketSerializer.deserializeJournal(json);
    return list;
  }
  /**
   * Returns all the tickets in the repository. Querying tickets from the repository requires
   * deserializing all tickets. This is an expensive process and not recommended. Tickets are
   * indexed by Lucene and queries should be executed against that index.
   *
   * @param repository
   * @param filter optional filter to only return matching results
   * @return a list of tickets
   */
  @Override
  public List<TicketModel> getTickets(RepositoryModel repository, TicketFilter filter) {
    List<TicketModel> list = new ArrayList<TicketModel>();

    Repository db = repositoryManager.getRepository(repository.name);
    try {
      RefModel ticketsBranch = getTicketsBranch(db);
      if (ticketsBranch == null) {
        return list;
      }

      // Collect the set of all json files
      List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);

      // Deserialize each ticket and optionally filter out unwanted tickets
      for (PathModel path : paths) {
        String name = path.name.substring(path.name.lastIndexOf('/') + 1);
        if (!JOURNAL.equals(name)) {
          continue;
        }
        String json = readTicketsFile(db, path.path);
        if (StringUtils.isEmpty(json)) {
          // journal was touched but no changes were written
          continue;
        }
        try {
          // Reconstruct ticketId from the path
          // id/26/326/journal.json
          String tid = path.path.split("/")[2];
          long ticketId = Long.parseLong(tid);
          List<Change> changes = TicketSerializer.deserializeJournal(json);
          if (ArrayUtils.isEmpty(changes)) {
            log.warn("Empty journal for {}:{}", repository, path.path);
            continue;
          }
          TicketModel ticket = TicketModel.buildTicket(changes);
          ticket.project = repository.projectPath;
          ticket.repository = repository.name;
          ticket.number = ticketId;

          // add the ticket, conditionally, to the list
          if (filter == null) {
            list.add(ticket);
          } else {
            if (filter.accept(ticket)) {
              list.add(ticket);
            }
          }
        } catch (Exception e) {
          log.error(
              "failed to deserialize {}/{}\n{}",
              new Object[] {repository, path.path, e.getMessage()});
          log.error(null, e);
        }
      }

      // sort the tickets by creation
      Collections.sort(list);
      return list;
    } finally {
      db.close();
    }
  }