Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
src/main/java/com/gitblit/models/TicketModel.java
@@ -35,6 +35,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
@@ -90,6 +91,10 @@
   public Integer deletions;
   public Priority priority;
   public Severity severity;
   /**
    * Builds an effective ticket from the collection of changes.  A change may
    * Add or Subtract information from a ticket, but the collection of changes
@@ -102,6 +107,31 @@
      TicketModel ticket;
      List<Change> effectiveChanges = new ArrayList<Change>();
      Map<String, Change> comments = new HashMap<String, Change>();
      Map<String, Change> references = new HashMap<String, Change>();
      Map<Integer, Integer> latestRevisions = new HashMap<Integer, Integer>();
      int latestPatchsetNumber = -1;
      List<Integer> deletedPatchsets = new ArrayList<Integer>();
      for (Change change : changes) {
         if (change.patchset != null) {
            if (change.patchset.isDeleted()) {
               deletedPatchsets.add(change.patchset.number);
            } else {
               Integer latestRev = latestRevisions.get(change.patchset.number);
               if (latestRev == null || change.patchset.rev > latestRev) {
                  latestRevisions.put(change.patchset.number, change.patchset.rev);
               }
               if (change.patchset.number > latestPatchsetNumber) {
                  latestPatchsetNumber = change.patchset.number;
               }
            }
         }
      }
      for (Change change : changes) {
         if (change.comment != null) {
            if (comments.containsKey(change.comment.id)) {
@@ -117,6 +147,31 @@
               effectiveChanges.add(change);
               comments.put(change.comment.id, change);
            }
         } else if (change.patchset != null) {
            //All revisions of a deleted patchset are not displayed
            if (!deletedPatchsets.contains(change.patchset.number)) {
               Integer latestRev = latestRevisions.get(change.patchset.number);
               if (    (change.patchset.number < latestPatchsetNumber)
                   && (change.patchset.rev == latestRev)) {
                  change.patchset.canDelete = true;
               }
               effectiveChanges.add(change);
            }
         } else if (change.reference != null){
            if (references.containsKey(change.reference.toString())) {
               Change original = references.get(change.reference.toString());
               Change clone = copy(original);
               clone.reference.deleted = change.reference.deleted;
               int idx = effectiveChanges.indexOf(original);
               effectiveChanges.remove(original);
               effectiveChanges.add(idx, clone);
            } else {
               effectiveChanges.add(change);
               references.put(change.reference.toString(), change);
            }
         } else {
            effectiveChanges.add(change);
         }
@@ -125,9 +180,15 @@
      // effective ticket
      ticket = new TicketModel();
      for (Change change : effectiveChanges) {
         //Ensure deleted items are not included
         if (!change.hasComment()) {
            // ensure we do not include a deleted comment
            change.comment = null;
         }
         if (!change.hasReference()) {
            change.reference = null;
         }
         if (!change.hasPatchset()) {
            change.patchset = null;
         }
         ticket.applyChange(change);
      }
@@ -140,6 +201,8 @@
      changes = new ArrayList<Change>();
      status = Status.New;
      type = Type.defaultType;
      priority = Priority.defaultPriority;
      severity = Severity.defaultSeverity;
   }
   public boolean isOpen() {
@@ -310,6 +373,15 @@
      return false;
   }
   public boolean hasReferences() {
      for (Change change : changes) {
         if (change.hasReference()) {
            return true;
         }
      }
      return false;
   }
   public List<Attachment> getAttachments() {
      List<Attachment> list = new ArrayList<Attachment>();
      for (Change change : changes) {
@@ -320,6 +392,16 @@
      return list;
   }
   public List<Reference> getReferences() {
      List<Reference> list = new ArrayList<Reference>();
      for (Change change : changes) {
         if (change.hasReference()) {
            list.add(change.reference);
         }
      }
      return list;
   }
   public List<Patchset> getPatchsets() {
      List<Patchset> list = new ArrayList<Patchset>();
      for (Change change : changes) {
@@ -516,6 +598,12 @@
            case mergeSha:
               mergeSha = toString(value);
               break;
            case priority:
               priority = TicketModel.Priority.fromObject(value, priority);
               break;
            case severity:
               severity = TicketModel.Severity.fromObject(value, severity);
               break;
            default:
               // unknown
               break;
@@ -523,8 +611,12 @@
         }
      }
      // add the change to the ticket
      changes.add(change);
      // add real changes to the ticket and ensure deleted changes are removed
      if (change.isEmptyChange()) {
         changes.remove(change);
      } else {
         changes.add(change);
      }
   }
   protected String toString(Object value) {
@@ -595,6 +687,8 @@
      public Comment comment;
      public Reference reference;
      public Map<Field, String> fields;
      public Set<Attachment> attachments;
@@ -604,6 +698,10 @@
      public Review review;
      private transient String id;
      //Once links have been made they become a reference on the target ticket
      //The ticket service handles promoting links to references
      public transient List<TicketLink> pendingLinks;
      public Change(String author) {
         this(author, new Date());
@@ -628,7 +726,7 @@
      }
      public boolean hasPatchset() {
         return patchset != null;
         return patchset != null && !patchset.isDeleted();
      }
      public boolean hasReview() {
@@ -636,13 +734,44 @@
      }
      public boolean hasComment() {
         return comment != null && !comment.isDeleted();
         return comment != null && !comment.isDeleted() && comment.text != null;
      }
      public boolean hasReference() {
         return reference != null && !reference.isDeleted();
      }
      public boolean hasPendingLinks() {
         return pendingLinks != null && pendingLinks.size() > 0;
      }
      public Comment comment(String text) {
         comment = new Comment(text);
         comment.id = TicketModel.getSHA1(date.toString() + author + text);
         // parse comment looking for ref #n
         //TODO: Ideally set via settings
         String x = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";
         try {
            Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
            Matcher m = p.matcher(text);
            while (m.find()) {
               String val = m.group(1);
               long targetTicketId = Long.parseLong(val);
               if (targetTicketId > 0) {
                  if (pendingLinks == null) {
                     pendingLinks = new ArrayList<TicketLink>();
                  }
                  pendingLinks.add(new TicketLink(targetTicketId, TicketAction.Comment));
               }
            }
         } catch (Exception e) {
            // ignore
         }
         try {
            Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
            Matcher m = mentions.matcher(text);
@@ -656,6 +785,16 @@
         return comment;
      }
      public Reference referenceCommit(String commitHash) {
         reference = new Reference(commitHash);
         return reference;
      }
      public Reference referenceTicket(long ticketId, String changeHash) {
         reference = new Reference(ticketId, changeHash);
         return reference;
      }
      public Review review(Patchset patchset, Score score, boolean addReviewer) {
         if (addReviewer) {
            plusList(Field.reviewers, author);
@@ -785,7 +924,21 @@
         for (String item : items) {
            list.add(prefix + item);
         }
         setField(field, join(list, ","));
         if (hasField(field)) {
            String flat = getString(field);
            if (isEmpty(flat)) {
               // field is empty, use this list
               setField(field, join(list, ","));
            } else {
               // merge this list into the existing field list
               Set<String> set = new TreeSet<String>(Arrays.asList(flat.split(",")));
               set.addAll(list);
               setField(field, join(set, ","));
            }
         } else {
            // does not have a list for this field
            setField(field, join(list, ","));
         }
      }
      public String getId() {
@@ -812,6 +965,17 @@
         }
         return false;
      }
      /*
       * Identify if this is an empty change. i.e. only an author and date is defined.
       * This can occur when items have been deleted
       * @returns true if the change is empty
       */
      private boolean isEmptyChange() {
         return ((comment == null) && (reference == null) &&
               (fields == null) && (attachments == null) &&
               (patchset == null) && (review == null));
      }
      @Override
      public String toString() {
@@ -821,6 +985,8 @@
            sb.append(" commented on by ");
         } else if (hasPatchset()) {
            sb.append(MessageFormat.format(" {0} uploaded by ", patchset));
         } else if (hasReference()) {
            sb.append(MessageFormat.format(" referenced in {0} by ", reference));
         } else {
            sb.append(" changed by ");
         }
@@ -1006,8 +1172,14 @@
      public int added;
      public PatchsetType type;
      public transient boolean canDelete = false;
      public boolean isFF() {
         return PatchsetType.FastForward == type;
      }
      public boolean isDeleted() {
         return PatchsetType.Delete == type;
      }
      @Override
@@ -1075,6 +1247,114 @@
         return text;
      }
   }
   public static enum TicketAction {
      Commit, Comment, Patchset, Close
   }
   //Intentionally not serialized, links are persisted as "references"
   public static class TicketLink {
      public long targetTicketId;
      public String hash;
      public TicketAction action;
      public boolean success;
      public boolean isDelete;
      public TicketLink(long targetTicketId, TicketAction action) {
         this.targetTicketId = targetTicketId;
         this.action = action;
         success = false;
         isDelete = false;
      }
      public TicketLink(long targetTicketId, TicketAction action, String hash) {
         this.targetTicketId = targetTicketId;
         this.action = action;
         this.hash = hash;
         success = false;
         isDelete = false;
      }
   }
   public static enum ReferenceType {
      Undefined, Commit, Ticket;
      @Override
      public String toString() {
         return name().toLowerCase().replace('_', ' ');
      }
      public static ReferenceType fromObject(Object o, ReferenceType defaultType) {
         if (o instanceof ReferenceType) {
            // cast and return
            return (ReferenceType) o;
         } else if (o instanceof String) {
            // find by name
            for (ReferenceType type : values()) {
               String str = o.toString();
               if (type.name().equalsIgnoreCase(str)
                     || type.toString().equalsIgnoreCase(str)) {
                  return type;
               }
            }
         } else if (o instanceof Number) {
            // by ordinal
            int id = ((Number) o).intValue();
            if (id >= 0 && id < values().length) {
               return values()[id];
            }
         }
         return defaultType;
      }
   }
   public static class Reference implements Serializable {
      private static final long serialVersionUID = 1L;
      public String hash;
      public Long ticketId;
      public Boolean deleted;
      Reference(String commitHash) {
         this.hash = commitHash;
      }
      Reference(long ticketId, String changeHash) {
         this.ticketId = ticketId;
         this.hash = changeHash;
      }
      public ReferenceType getSourceType(){
         if (hash != null) {
            if (ticketId != null) {
               return ReferenceType.Ticket;
            } else {
               return ReferenceType.Commit;
            }
         }
         return ReferenceType.Undefined;
      }
      public boolean isDeleted() {
         return deleted != null && deleted;
      }
      @Override
      public String toString() {
         switch (getSourceType()) {
            case Commit: return hash;
            case Ticket: return ticketId.toString() + "#" + hash;
            default: {} break;
         }
         return String.format("Unknown Reference Type");
      }
   }
   public static class Attachment implements Serializable {
@@ -1138,7 +1418,8 @@
   }
   public static enum Score {
      approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(-2);
      approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(
            -2);
      final int value;
@@ -1154,20 +1435,29 @@
      public String toString() {
         return name().toLowerCase().replace('_', ' ');
      }
      public static Score fromScore(int score) {
         for (Score s : values()) {
            if (s.getValue() == score) {
               return s;
            }
         }
         throw new NoSuchElementException(String.valueOf(score));
      }
   }
   public static enum Field {
      title, body, responsible, type, status, milestone, mergeSha, mergeTo,
      topic, labels, watchers, reviewers, voters, mentions;
      topic, labels, watchers, reviewers, voters, mentions, priority, severity;
   }
   public static enum Type {
      Enhancement, Task, Bug, Proposal, Question;
      Enhancement, Task, Bug, Proposal, Question, Maintenance;
      public static Type defaultType = Task;
      public static Type [] choices() {
         return new Type [] { Enhancement, Task, Bug, Question };
         return new Type [] { Enhancement, Task, Bug, Question, Maintenance };
      }
      @Override
@@ -1201,13 +1491,15 @@
   }
   public static enum Status {
      New, Open, Resolved, Fixed, Merged, Wontfix, Declined, Duplicate, Invalid, On_Hold;
      New, Open, Closed, Resolved, Fixed, Merged, Wontfix, Declined, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required;
      public static Status [] requestWorkflow = { Open, Resolved, Declined, Duplicate, Invalid, On_Hold };
      public static Status [] requestWorkflow = { Open, Resolved, Declined, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required };
      public static Status [] bugWorkflow = { Open, Fixed, Wontfix, Duplicate, Invalid, On_Hold };
      public static Status [] bugWorkflow = { Open, Fixed, Wontfix, Duplicate, Invalid, Abandoned, On_Hold, No_Change_Required };
      public static Status [] proposalWorkflow = { Open, Resolved, Declined, On_Hold};
      public static Status [] proposalWorkflow = { Open, Resolved, Declined, Abandoned, On_Hold, No_Change_Required };
      public static Status [] milestoneWorkflow = { Open, Closed, Abandoned, On_Hold };
      @Override
      public String toString() {
@@ -1248,7 +1540,7 @@
   }
   public static enum PatchsetType {
      Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend;
      Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend, Delete;
      public boolean isRewrite() {
         return (this != FastForward) && (this != Proposal);
@@ -1283,4 +1575,110 @@
         return null;
      }
   }
   public static enum Priority {
      Low(-1), Normal(0), High(1), Urgent(2);
      public static Priority defaultPriority = Normal;
      final int value;
      Priority(int value) {
         this.value = value;
      }
      public int getValue() {
         return value;
      }
      public static Priority [] choices() {
         return new Priority [] { Urgent, High, Normal, Low };
      }
      @Override
      public String toString() {
         return name().toLowerCase().replace('_', ' ');
      }
      public static Priority fromObject(Object o, Priority defaultPriority) {
         if (o instanceof Priority) {
            // cast and return
            return (Priority) o;
         } else if (o instanceof String) {
            // find by name
            for (Priority priority : values()) {
               String str = o.toString();
               if (priority.name().equalsIgnoreCase(str)
                     || priority.toString().equalsIgnoreCase(str)) {
                  return priority;
               }
            }
         } else if (o instanceof Number) {
            switch (((Number) o).intValue()) {
               case -1: return Priority.Low;
               case 0:  return Priority.Normal;
               case 1:  return Priority.High;
               case 2:  return Priority.Urgent;
               default: return Priority.Normal;
            }
         }
         return defaultPriority;
      }
   }
   public static enum Severity {
      Unrated(-1), Negligible(1), Minor(2), Serious(3), Critical(4), Catastrophic(5);
      public static Severity defaultSeverity = Unrated;
      final int value;
      Severity(int value) {
         this.value = value;
      }
      public int getValue() {
         return value;
      }
      public static Severity [] choices() {
         return new Severity [] { Unrated, Negligible, Minor, Serious, Critical, Catastrophic };
      }
      @Override
      public String toString() {
         return name().toLowerCase().replace('_', ' ');
      }
      public static Severity fromObject(Object o, Severity defaultSeverity) {
         if (o instanceof Severity) {
            // cast and return
            return (Severity) o;
         } else if (o instanceof String) {
            // find by name
            for (Severity severity : values()) {
               String str = o.toString();
               if (severity.name().equalsIgnoreCase(str)
                     || severity.toString().equalsIgnoreCase(str)) {
                  return severity;
               }
            }
         } else if (o instanceof Number) {
            switch (((Number) o).intValue()) {
               case -1: return Severity.Unrated;
               case 1:  return Severity.Negligible;
               case 2:  return Severity.Minor;
               case 3:  return Severity.Serious;
               case 4:  return Severity.Critical;
               case 5:  return Severity.Catastrophic;
               default: return Severity.Unrated;
            }
         }
         return defaultSeverity;
      }
   }
}