James Moger
2015-11-19 e41e8f8c3bc9f5edab1d271464364f95620ece8c
src/main/java/com/gitblit/tickets/BranchTicketService.java
@@ -27,9 +27,8 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -44,21 +43,27 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import com.gitblit.Constants;
import com.gitblit.git.ReceiveCommandEvent;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.models.PathModel;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
@@ -67,6 +72,8 @@
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
 * Implementation of a ticket service based on an orphan branch.  All tickets
@@ -76,21 +83,10 @@
 * @author James Moger
 *
 */
@Singleton
public class BranchTicketService extends ITicketService implements RefsChangedListener {
   /**
    *  The event fired by other classes to allow this service to index tickets.
    */
   public static class TicketsBranchUpdated extends RefsChangedEvent {
      public final RepositoryModel model;
      public TicketsBranchUpdated(RepositoryModel model) {
         this.model = model;
      }
   }
   public static final String BRANCH = "refs/gitblit/tickets";
   public static final String BRANCH = "refs/meta/gitblit/tickets";
   private static final String JOURNAL = "journal.json";
@@ -101,23 +97,26 @@
   @Inject
   public BranchTicketService(
         IRuntimeManager runtimeManager,
         IPluginManager pluginManager,
         INotificationManager notificationManager,
         IUserManager userManager,
         IRepositoryManager repositoryManager) {
      super(runtimeManager,
            pluginManager,
            notificationManager,
            userManager,
            repositoryManager);
      lastAssignedId = new ConcurrentHashMap<String, AtomicLong>();
      // register the branch ticket service for repository ref changes
      Repository.getGlobalListenerList().addRefsChangedListener(this);
   }
   @Override
   public BranchTicketService start() {
      log.info("{} started", getClass().getSimpleName());
      return this;
   }
@@ -138,39 +137,105 @@
   }
   /**
    * Listen for refs changed events and reindex that repository.
    * Listen for tickets branch changes and (re)index tickets, as appropriate
    */
   @Override
   public void onRefsChanged(RefsChangedEvent event) {
      if (!(event instanceof TicketsBranchUpdated)) {
   public synchronized void onRefsChanged(RefsChangedEvent event) {
      if (!(event instanceof ReceiveCommandEvent)) {
         return;
      }
      RepositoryModel repository = ((TicketsBranchUpdated) event).model;
      ReceiveCommandEvent branchUpdate = (ReceiveCommandEvent) event;
      RepositoryModel repository = branchUpdate.model;
      ReceiveCommand cmd = branchUpdate.cmd;
      try {
         reindex(repository);
         switch (cmd.getType()) {
         case CREATE:
         case UPDATE_NONFASTFORWARD:
            // reindex everything
            reindex(repository);
            break;
         case UPDATE:
            // incrementally index ticket updates
            resetCaches(repository);
            long start = System.nanoTime();
            log.info("incrementally indexing {} ticket branch due to received ref update", repository.name);
            Repository db = repositoryManager.getRepository(repository.name);
            try {
               Set<Long> ids = new HashSet<Long>();
               List<PathChangeModel> paths = JGitUtils.getFilesInRange(db,
                     cmd.getOldId().getName(), cmd.getNewId().getName());
               for (PathChangeModel path : paths) {
                  String name = path.name.substring(path.name.lastIndexOf('/') + 1);
                  if (!JOURNAL.equals(name)) {
                     continue;
                  }
                  String tid = path.path.split("/")[2];
                  long ticketId = Long.parseLong(tid);
                  if (!ids.contains(ticketId)) {
                     ids.add(ticketId);
                     TicketModel ticket = getTicket(repository, ticketId);
                     log.info(MessageFormat.format("indexing ticket #{0,number,0}: {1}",
                           ticketId, ticket.title));
                     indexer.index(ticket);
                  }
               }
               long end = System.nanoTime();
               log.info("incremental indexing of {0} ticket(s) completed in {1} msecs",
                     ids.size(), TimeUnit.NANOSECONDS.toMillis(end - start));
            } finally {
               db.close();
            }
            break;
         default:
            log.warn("Unexpected receive type {} in BranchTicketService.onRefsChanged" + cmd.getType());
            break;
         }
      } catch (Exception e) {
         log.error("failed to reindex " + repository.name, e);
      }
   }
   /**
    * Returns a RefModel for the refs/gitblit/tickets branch in the repository.
    * Returns a RefModel for the refs/meta/gitblit/tickets branch in the repository.
    * If the branch can not be found, null is returned.
    *
    * @return a refmodel for the gitblit tickets branch or null
    */
   private RefModel getTicketsBranch(Repository db) {
      List<RefModel> refs = JGitUtils.getRefs(db, Constants.R_GITBLIT);
      List<RefModel> refs = JGitUtils.getRefs(db, "refs/");
      Ref oldRef = null;
      for (RefModel ref : refs) {
         if (ref.reference.getName().equals(BRANCH)) {
            return ref;
         } else if (ref.reference.getName().equals("refs/gitblit/tickets")) {
            oldRef = ref.reference;
         }
      }
      if (oldRef != null) {
         // rename old ref to refs/meta/gitblit/tickets
         RefRename cmd;
         try {
            cmd = db.renameRef(oldRef.getName(), BRANCH);
            cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
            cmd.setRefLogMessage("renamed " + oldRef.getName() + " => " + BRANCH);
            Result res = cmd.rename();
            switch (res) {
            case RENAMED:
               log.info(db.getDirectory() + " " + cmd.getRefLogMessage());
               return getTicketsBranch(db);
            default:
               log.error("failed to rename " + oldRef.getName() + " => " + BRANCH + " (" + res.name() + ")");
            }
         } catch (IOException e) {
            log.error("failed to rename tickets branch", e);
         }
      }
      return null;
   }
   /**
    * Creates the refs/gitblit/tickets branch.
    * Creates the refs/meta/gitblit/tickets branch.
    * @param db
    */
   private void createTicketsBranch(Repository db) {
@@ -183,7 +248,7 @@
    * folder with the remaining characters as a subfolder within that folder.
    *
    * @param ticketId
    * @return the root path of the ticket content on the refs/gitblit/tickets branch
    * @return the root path of the ticket content on the refs/meta/gitblit/tickets branch
    */
   private String toTicketPath(long ticketId) {
      StringBuilder sb = new StringBuilder();
@@ -232,7 +297,7 @@
         log.error("failed to read " + file, e);
      } finally {
         if (rw != null) {
            rw.release();
            rw.close();
         }
      }
      return null;
@@ -288,7 +353,7 @@
      } catch (IOException e) {
         log.error("", e);
      } finally {
         inserter.release();
         inserter.close();
      }
   }
@@ -318,6 +383,37 @@
   }
   /**
    * Returns the assigned ticket ids.
    *
    * @return the assigned ticket ids
    */
   @Override
   public synchronized Set<Long> getIds(RepositoryModel repository) {
      Repository db = repositoryManager.getRepository(repository.name);
      try {
         if (getTicketsBranch(db) == null) {
            return Collections.emptySet();
         }
         Set<Long> ids = new TreeSet<Long>();
         List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
         for (PathModel path : paths) {
            String name = path.name.substring(path.name.lastIndexOf('/') + 1);
            if (!JOURNAL.equals(name)) {
               continue;
            }
            String tid = path.path.split("/")[2];
            long ticketId = Long.parseLong(tid);
            ids.add(ticketId);
         }
         return ids;
      } finally {
         if (db != null) {
            db.close();
         }
      }
   }
   /**
    * Assigns a new ticket id.
    *
    * @param repository
@@ -338,16 +434,10 @@
         }
         AtomicLong lastId = lastAssignedId.get(repository.name);
         if (lastId.get() <= 0) {
            List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
            for (PathModel path : paths) {
               String name = path.name.substring(path.name.lastIndexOf('/') + 1);
               if (!JOURNAL.equals(name)) {
                  continue;
               }
               String tid = path.path.split("/")[2];
               long ticketId = Long.parseLong(tid);
               if (ticketId > lastId.get()) {
                  lastId.set(ticketId);
            Set<Long> ids = getIds(repository);
            for (long id : ids) {
               if (id > lastId.get()) {
                  lastId.set(id);
               }
            }
         }
@@ -460,6 +550,28 @@
            ticket.number = ticketId;
         }
         return ticket;
      } finally {
         db.close();
      }
   }
   /**
    * Retrieves the journal for the ticket.
    *
    * @param repository
    * @param ticketId
    * @return a journal, if it exists, otherwise null
    */
   @Override
   protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
      Repository db = repositoryManager.getRepository(repository.name);
      try {
         List<Change> changes = getJournal(db, ticketId);
         if (ArrayUtils.isEmpty(changes)) {
            log.warn("Empty journal for {}:{}", repository, ticketId);
            return null;
         }
         return changes;
      } finally {
         db.close();
      }
@@ -599,7 +711,9 @@
                  ticket.number, db.getDirectory()), t);
         } finally {
            // release the treewalk
            treeWalk.release();
            if (treeWalk != null) {
               treeWalk.close();
            }
         }
      } finally {
         db.close();
@@ -697,7 +811,7 @@
         // finish the index
         builder.finish();
      } finally {
         inserter.release();
         inserter.close();
      }
      return newIndex;
   }
@@ -714,8 +828,12 @@
      List<DirCacheEntry> list = new ArrayList<DirCacheEntry>();
      TreeWalk tw = null;
      try {
         tw = new TreeWalk(db);
         ObjectId treeId = db.resolve(BRANCH + "^{tree}");
         if (treeId == null) {
            // branch does not exist yet, could be migrating tickets
            return list;
         }
         tw = new TreeWalk(db);
         int hIdx = tw.addTree(treeId);
         tw.setRecursive(true);
@@ -737,7 +855,7 @@
         }
      } finally {
         if (tw != null) {
            tw.release();
            tw.close();
         }
      }
      return list;
@@ -795,10 +913,10 @@
                     rc));
            }
         } finally {
            revWalk.release();
            revWalk.close();
         }
      } finally {
         odi.release();
         odi.close();
      }
      return success;
   }
@@ -815,7 +933,9 @@
      } catch (Exception e) {
         log.error(null, e);
      } finally {
         db.close();
         if (db != null) {
            db.close();
         }
      }
      return false;
   }