]> source.dussan.org Git - gitblit.git/commitdiff
More functional issues.
authorJames Moger <james.moger@gitblit.com>
Mon, 16 Jan 2012 00:07:34 +0000 (19:07 -0500)
committerJames Moger <james.moger@gitblit.com>
Mon, 16 Jan 2012 00:07:34 +0000 (19:07 -0500)
src/com/gitblit/models/IssueModel.java
src/com/gitblit/utils/IssueUtils.java
src/com/gitblit/utils/JGitUtils.java
tests/com/gitblit/tests/GitBlitSuite.java
tests/com/gitblit/tests/IssuesTest.java

index 3c6d9a0bfa3fefef5042df2347e41894752e24f0..19241c291815d05b56d2af3fba2e7f43477d7bc0 100644 (file)
@@ -18,10 +18,13 @@ package com.gitblit.models;
 import java.io.Serializable;\r
 import java.util.ArrayList;\r
 import java.util.Date;\r
+import java.util.LinkedHashSet;\r
 import java.util.List;\r
+import java.util.Set;\r
 \r
 import com.gitblit.utils.ArrayUtils;\r
 import com.gitblit.utils.StringUtils;\r
+import com.gitblit.utils.TimeUtils;\r
 \r
 /**\r
  * The Gitblit Issue model, its component classes, and enums.\r
@@ -31,7 +34,7 @@ import com.gitblit.utils.StringUtils;
  */\r
 public class IssueModel implements Serializable, Comparable<IssueModel> {\r
 \r
-       private static final long serialVersionUID = 1L;;\r
+       private static final long serialVersionUID = 1L;\r
 \r
        public String id;\r
 \r
@@ -56,7 +59,7 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
        public List<Change> changes;\r
 \r
        public IssueModel() {\r
-               created = new Date();\r
+               created = new Date((System.currentTimeMillis() / 1000) * 1000);\r
 \r
                type = Type.Defect;\r
                status = Status.New;\r
@@ -72,15 +75,16 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                return s;\r
        }\r
 \r
+       public boolean hasLabel(String label) {\r
+               return getLabels().contains(label);\r
+       }\r
+\r
        public List<String> getLabels() {\r
                List<String> list = new ArrayList<String>();\r
                String labels = null;\r
                for (Change change : changes) {\r
-                       if (change.hasFieldChanges()) {\r
-                               FieldChange field = change.getField(Field.Labels);\r
-                               if (field != null) {\r
-                                       labels = field.value.toString();\r
-                               }\r
+                       if (change.hasField(Field.Labels)) {\r
+                               labels = change.getString(Field.Labels);\r
                        }\r
                }\r
                if (!StringUtils.isEmpty(labels)) {\r
@@ -89,10 +93,6 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                return list;\r
        }\r
 \r
-       public boolean hasLabel(String label) {\r
-               return getLabels().contains(label);\r
-       }\r
-\r
        public Attachment getAttachment(String name) {\r
                Attachment attachment = null;\r
                for (Change change : changes) {\r
@@ -106,16 +106,55 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                return attachment;\r
        }\r
 \r
-       public void addChange(Change change) {\r
-               if (changes == null) {\r
-                       changes = new ArrayList<Change>();\r
-               }\r
+       public void applyChange(Change change) {\r
                changes.add(change);\r
+\r
+               if (change.hasFieldChanges()) {\r
+                       for (FieldChange fieldChange : change.fieldChanges) {\r
+                               switch (fieldChange.field) {\r
+                               case Id:\r
+                                       id = fieldChange.value.toString();\r
+                                       break;\r
+                               case Type:\r
+                                       type = IssueModel.Type.fromObject(fieldChange.value);\r
+                                       break;\r
+                               case Status:\r
+                                       status = IssueModel.Status.fromObject(fieldChange.value);\r
+                                       break;\r
+                               case Priority:\r
+                                       priority = IssueModel.Priority.fromObject(fieldChange.value);\r
+                                       break;\r
+                               case Summary:\r
+                                       summary = fieldChange.value.toString();\r
+                                       break;\r
+                               case Description:\r
+                                       description = fieldChange.value.toString();\r
+                                       break;\r
+                               case Reporter:\r
+                                       reporter = fieldChange.value.toString();\r
+                                       break;\r
+                               case Owner:\r
+                                       owner = fieldChange.value.toString();\r
+                                       break;\r
+                               case Milestone:\r
+                                       milestone = fieldChange.value.toString();\r
+                                       break;\r
+                               }\r
+                       }\r
+               }\r
        }\r
 \r
        @Override\r
        public String toString() {\r
-               return summary;\r
+               StringBuilder sb = new StringBuilder();\r
+               sb.append("issue ");\r
+               sb.append(id.substring(0, 8));\r
+               sb.append(" (" + summary + ")\n");\r
+               for (Change change : changes) {\r
+                       sb.append(change);\r
+                       sb.append('\n');\r
+               }\r
+               return sb.toString();\r
        }\r
 \r
        @Override\r
@@ -135,41 +174,72 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                return id.hashCode();\r
        }\r
 \r
-       public static class Change implements Serializable {\r
+       public static class Change implements Serializable, Comparable<Change> {\r
 \r
                private static final long serialVersionUID = 1L;\r
 \r
-               public Date created;\r
+               public final Date created;\r
+\r
+               public final String author;\r
 \r
-               public String author;\r
+               public String id;\r
+\r
+               public char code;\r
 \r
                public Comment comment;\r
 \r
-               public List<FieldChange> fieldChanges;\r
+               public Set<FieldChange> fieldChanges;\r
 \r
-               public List<Attachment> attachments;\r
+               public Set<Attachment> attachments;\r
 \r
-               public void comment(String text) {\r
-                       comment = new Comment(text);\r
+               public Change(String author) {\r
+                       this.created = new Date((System.currentTimeMillis() / 1000) * 1000);\r
+                       this.author = author;\r
+                       this.id = StringUtils.getSHA1(created.toString() + author);\r
                }\r
 \r
                public boolean hasComment() {\r
-                       return comment != null;\r
+                       return comment != null && !comment.deleted;\r
+               }\r
+\r
+               public void comment(String text) {\r
+                       comment = new Comment(text);\r
+                       comment.id = StringUtils.getSHA1(created.toString() + author + text);\r
                }\r
 \r
                public boolean hasAttachments() {\r
                        return !ArrayUtils.isEmpty(attachments);\r
                }\r
 \r
+               public void addAttachment(Attachment attachment) {\r
+                       if (attachments == null) {\r
+                               attachments = new LinkedHashSet<Attachment>();\r
+                       }\r
+                       attachments.add(attachment);\r
+               }\r
+\r
+               public Attachment getAttachment(String name) {\r
+                       for (Attachment attachment : attachments) {\r
+                               if (attachment.name.equalsIgnoreCase(name)) {\r
+                                       return attachment;\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
+\r
+               public boolean hasField(Field field) {\r
+                       return !StringUtils.isEmpty(getString(field));\r
+               }\r
+\r
                public boolean hasFieldChanges() {\r
                        return !ArrayUtils.isEmpty(fieldChanges);\r
                }\r
 \r
-               public FieldChange getField(Field field) {\r
+               public Object getField(Field field) {\r
                        if (fieldChanges != null) {\r
                                for (FieldChange fieldChange : fieldChanges) {\r
                                        if (fieldChange.field == field) {\r
-                                               return fieldChange;\r
+                                               return fieldChange.value;\r
                                        }\r
                                }\r
                        }\r
@@ -177,42 +247,74 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                }\r
 \r
                public void setField(Field field, Object value) {\r
-                       FieldChange fieldChange = new FieldChange();\r
-                       fieldChange.field = field;\r
-                       fieldChange.value = value;\r
+                       FieldChange fieldChange = new FieldChange(field, value);\r
                        if (fieldChanges == null) {\r
-                               fieldChanges = new ArrayList<FieldChange>();\r
+                               fieldChanges = new LinkedHashSet<FieldChange>();\r
                        }\r
                        fieldChanges.add(fieldChange);\r
                }\r
 \r
                public String getString(Field field) {\r
-                       FieldChange fieldChange = getField(field);\r
-                       if (fieldChange == null) {\r
+                       Object value = getField(field);\r
+                       if (value == null) {\r
                                return null;\r
                        }\r
-                       return fieldChange.value.toString();\r
+                       return value.toString();\r
                }\r
 \r
-               public void addAttachment(Attachment attachment) {\r
-                       if (attachments == null) {\r
-                               attachments = new ArrayList<Attachment>();\r
-                       }\r
-                       attachments.add(attachment);\r
+               @Override\r
+               public int compareTo(Change c) {\r
+                       return created.compareTo(c.created);\r
                }\r
 \r
-               public Attachment getAttachment(String name) {\r
-                       for (Attachment attachment : attachments) {\r
-                               if (attachment.name.equalsIgnoreCase(name)) {\r
-                                       return attachment;\r
-                               }\r
+               @Override\r
+               public int hashCode() {\r
+                       return id.hashCode();\r
+               }\r
+\r
+               @Override\r
+               public boolean equals(Object o) {\r
+                       if (o instanceof Change) {\r
+                               return id.equals(((Change) o).id);\r
                        }\r
-                       return null;\r
+                       return false;\r
                }\r
 \r
                @Override\r
                public String toString() {\r
-                       return created.toString() + " by " + author;\r
+                       StringBuilder sb = new StringBuilder();\r
+                       sb.append(TimeUtils.timeAgo(created));\r
+                       switch (code) {\r
+                       case '+':\r
+                               sb.append(" created by ");\r
+                               break;\r
+                       default:\r
+                               if (hasComment()) {\r
+                                       sb.append(" commented on by ");\r
+                               } else {\r
+                                       sb.append(" changed by ");\r
+                               }\r
+                       }\r
+                       sb.append(author).append(" - ");\r
+                       if (hasComment()) {\r
+                               if (comment.deleted) {\r
+                                       sb.append("(deleted) ");\r
+                               }\r
+                               sb.append(comment.text).append(" ");\r
+                       }\r
+                       if (hasFieldChanges()) {\r
+                               switch (code) {\r
+                               case '+':\r
+                                       break;\r
+                               default:\r
+                                       for (FieldChange fieldChange : fieldChanges) {\r
+                                               sb.append("\n  ");\r
+                                               sb.append(fieldChange);\r
+                                       }\r
+                                       break;\r
+                               }\r
+                       }\r
+                       return sb.toString();\r
                }\r
        }\r
 \r
@@ -221,6 +323,9 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                private static final long serialVersionUID = 1L;\r
 \r
                public String text;\r
+\r
+               public String id;\r
+\r
                public boolean deleted;\r
 \r
                Comment(String text) {\r
@@ -237,9 +342,27 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
 \r
                private static final long serialVersionUID = 1L;\r
 \r
-               public Field field;\r
+               public final Field field;\r
 \r
-               public Object value;\r
+               public final Object value;\r
+\r
+               FieldChange(Field field, Object value) {\r
+                       this.field = field;\r
+                       this.value = value;\r
+               }\r
+\r
+               @Override\r
+               public int hashCode() {\r
+                       return field.hashCode();\r
+               }\r
+\r
+               @Override\r
+               public boolean equals(Object o) {\r
+                       if (o instanceof FieldChange) {\r
+                               return field.equals(((FieldChange) o).field);\r
+                       }\r
+                       return false;\r
+               }\r
 \r
                @Override\r
                public String toString() {\r
@@ -251,7 +374,8 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
 \r
                private static final long serialVersionUID = 1L;\r
 \r
-               public String name;\r
+               public final String name;\r
+               public String id;\r
                public long size;\r
                public byte[] content;\r
                public boolean deleted;\r
@@ -260,6 +384,19 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                        this.name = name;\r
                }\r
 \r
+               @Override\r
+               public int hashCode() {\r
+                       return name.hashCode();\r
+               }\r
+\r
+               @Override\r
+               public boolean equals(Object o) {\r
+                       if (o instanceof Attachment) {\r
+                               return name.equalsIgnoreCase(((Attachment) o).name);\r
+                       }\r
+                       return false;\r
+               }\r
+\r
                @Override\r
                public String toString() {\r
                        return name;\r
@@ -267,20 +404,86 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
        }\r
 \r
        public static enum Field {\r
-               Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Labels;\r
+               Id, Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Component, Labels;\r
        }\r
 \r
        public static enum Type {\r
                Defect, Enhancement, Task, Review, Other;\r
+\r
+               public static Type fromObject(Object o) {\r
+                       if (o instanceof Type) {\r
+                               // cast and return\r
+                               return (Type) o;\r
+                       } else if (o instanceof String) {\r
+                               // find by name\r
+                               for (Type type : values()) {\r
+                                       String str = o.toString();\r
+                                       if (type.toString().equalsIgnoreCase(str)) {\r
+                                               return type;\r
+                                       }\r
+                               }\r
+                       } else if (o instanceof Number) {\r
+                               // by ordinal\r
+                               int id = ((Number) o).intValue();\r
+                               if (id >= 0 && id < values().length) {\r
+                                       return values()[id];\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
        }\r
 \r
        public static enum Priority {\r
                Low, Medium, High, Critical;\r
+\r
+               public static Priority fromObject(Object o) {\r
+                       if (o instanceof Priority) {\r
+                               // cast and return\r
+                               return (Priority) o;\r
+                       } else if (o instanceof String) {\r
+                               // find by name\r
+                               for (Priority priority : values()) {\r
+                                       String str = o.toString();\r
+                                       if (priority.toString().equalsIgnoreCase(str)) {\r
+                                               return priority;\r
+                                       }\r
+                               }\r
+                       } else if (o instanceof Number) {\r
+                               // by ordinal\r
+                               int id = ((Number) o).intValue();\r
+                               if (id >= 0 && id < values().length) {\r
+                                       return values()[id];\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
        }\r
 \r
        public static enum Status {\r
                New, Accepted, Started, Review, Queued, Testing, Done, Fixed, WontFix, Duplicate, Invalid;\r
 \r
+               public static Status fromObject(Object o) {\r
+                       if (o instanceof Status) {\r
+                               // cast and return\r
+                               return (Status) o;\r
+                       } else if (o instanceof String) {\r
+                               // find by name\r
+                               for (Status status : values()) {\r
+                                       String str = o.toString();\r
+                                       if (status.toString().equalsIgnoreCase(str)) {\r
+                                               return status;\r
+                                       }\r
+                               }\r
+                       } else if (o instanceof Number) {\r
+                               // by ordinal\r
+                               int id = ((Number) o).intValue();\r
+                               if (id >= 0 && id < values().length) {\r
+                                       return values()[id];\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
+\r
                public boolean atLeast(Status status) {\r
                        return ordinal() >= status.ordinal();\r
                }\r
@@ -289,6 +492,10 @@ public class IssueModel implements Serializable, Comparable<IssueModel> {
                        return ordinal() > status.ordinal();\r
                }\r
 \r
+               public boolean isClosed() {\r
+                       return ordinal() >= Done.ordinal();\r
+               }\r
+\r
                public Status next() {\r
                        switch (this) {\r
                        case New:\r
index 82170703668837964606047be8339be24c94e31b..d0a019925d5582e733469eef95ef8366ba9b3950 100644 (file)
@@ -18,12 +18,15 @@ package com.gitblit.utils;
 import java.io.IOException;\r
 import java.text.MessageFormat;\r
 import java.util.ArrayList;\r
-import java.util.Arrays;\r
+import java.util.Collection;\r
 import java.util.Collections;\r
-import java.util.Date;\r
 import java.util.HashMap;\r
+import java.util.HashSet;\r
+import java.util.Iterator;\r
 import java.util.List;\r
 import java.util.Map;\r
+import java.util.Set;\r
+import java.util.TreeSet;\r
 \r
 import org.eclipse.jgit.JGitText;\r
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;\r
@@ -45,15 +48,21 @@ import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;\r
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;\r
 import org.eclipse.jgit.treewalk.TreeWalk;\r
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;\r
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;\r
+import org.eclipse.jgit.treewalk.filter.TreeFilter;\r
+import org.slf4j.Logger;\r
+import org.slf4j.LoggerFactory;\r
 \r
 import com.gitblit.models.IssueModel;\r
 import com.gitblit.models.IssueModel.Attachment;\r
 import com.gitblit.models.IssueModel.Change;\r
 import com.gitblit.models.IssueModel.Field;\r
-import com.gitblit.models.PathModel;\r
+import com.gitblit.models.IssueModel.Status;\r
 import com.gitblit.models.RefModel;\r
 import com.gitblit.utils.JsonUtils.ExcludeField;\r
 import com.google.gson.Gson;\r
+import com.google.gson.reflect.TypeToken;\r
 \r
 /**\r
  * Utility class for reading Gitblit issues.\r
@@ -63,8 +72,37 @@ import com.google.gson.Gson;
  */\r
 public class IssueUtils {\r
 \r
+       public static interface IssueFilter {\r
+               public abstract boolean accept(IssueModel issue);\r
+       }\r
+\r
        public static final String GB_ISSUES = "refs/heads/gb-issues";\r
 \r
+       static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);\r
+\r
+       /**\r
+        * Log an error message and exception.\r
+        * \r
+        * @param t\r
+        * @param repository\r
+        *            if repository is not null it MUST be the {0} parameter in the\r
+        *            pattern.\r
+        * @param pattern\r
+        * @param objects\r
+        */\r
+       private static void error(Throwable t, Repository repository, String pattern, Object... objects) {\r
+               List<Object> parameters = new ArrayList<Object>();\r
+               if (objects != null && objects.length > 0) {\r
+                       for (Object o : objects) {\r
+                               parameters.add(o);\r
+                       }\r
+               }\r
+               if (repository != null) {\r
+                       parameters.add(0, repository.getDirectory().getAbsolutePath());\r
+               }\r
+               LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);\r
+       }\r
+\r
        /**\r
         * Returns a RefModel for the gb-issues branch in the repository. If the\r
         * branch can not be found, null is returned.\r
@@ -77,7 +115,10 @@ public class IssueUtils {
        }\r
 \r
        /**\r
-        * Returns all the issues in the repository.\r
+        * Returns all the issues in the repository. Querying issues from the\r
+        * repository requires deserializing all changes for all issues. This is an\r
+        * expensive process and not recommended. Issues should be indexed by Lucene\r
+        * and queries should be executed against that index.\r
         * \r
         * @param repository\r
         * @param filter\r
@@ -90,12 +131,85 @@ public class IssueUtils {
                if (issuesBranch == null) {\r
                        return list;\r
                }\r
-               List<PathModel> paths = JGitUtils\r
-                               .getDocuments(repository, Arrays.asList("json"), GB_ISSUES);\r
-               RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();\r
-               for (PathModel path : paths) {\r
-                       String json = JGitUtils.getStringContent(repository, tree, path.path);\r
-                       IssueModel issue = JsonUtils.fromJsonString(json, IssueModel.class);\r
+\r
+               // Collect the set of all issue paths\r
+               Set<String> issuePaths = new HashSet<String>();\r
+               final TreeWalk tw = new TreeWalk(repository);\r
+               try {\r
+                       RevCommit head = JGitUtils.getCommit(repository, GB_ISSUES);\r
+                       tw.addTree(head.getTree());\r
+                       tw.setRecursive(false);\r
+                       while (tw.next()) {\r
+                               if (tw.getDepth() < 2 && tw.isSubtree()) {\r
+                                       tw.enterSubtree();\r
+                                       if (tw.getDepth() == 2) {\r
+                                               issuePaths.add(tw.getPathString());\r
+                                       }\r
+                               }\r
+                       }\r
+               } catch (IOException e) {\r
+                       error(e, repository, "{0} failed to query issues");\r
+               } finally {\r
+                       tw.release();\r
+               }\r
+\r
+               // Build each issue and optionally filter out unwanted issues\r
+\r
+               for (String issuePath : issuePaths) {\r
+                       RevWalk rw = new RevWalk(repository);\r
+                       try {\r
+                               RevCommit start = rw.parseCommit(repository.resolve(GB_ISSUES));\r
+                               rw.markStart(start);\r
+                       } catch (Exception e) {\r
+                               error(e, repository, "Failed to find {1} in {0}", GB_ISSUES);\r
+                       }\r
+                       TreeFilter treeFilter = AndTreeFilter.create(\r
+                                       PathFilterGroup.createFromStrings(issuePath), TreeFilter.ANY_DIFF);\r
+                       rw.setTreeFilter(treeFilter);\r
+                       Iterator<RevCommit> revlog = rw.iterator();\r
+\r
+                       List<RevCommit> commits = new ArrayList<RevCommit>();\r
+                       while (revlog.hasNext()) {\r
+                               commits.add(revlog.next());\r
+                       }\r
+\r
+                       // release the revwalk\r
+                       rw.release();\r
+\r
+                       if (commits.size() == 0) {\r
+                               LOGGER.warn("Failed to find changes for issue " + issuePath);\r
+                               continue;\r
+                       }\r
+\r
+                       // sort by commit order, first commit first\r
+                       Collections.reverse(commits);\r
+\r
+                       StringBuilder sb = new StringBuilder("[");\r
+                       boolean first = true;\r
+                       for (RevCommit commit : commits) {\r
+                               if (!first) {\r
+                                       sb.append(',');\r
+                               }\r
+                               String message = commit.getFullMessage();\r
+                               // commit message is formatted: C ISSUEID\n\nJSON\r
+                               // C is an single char commit code\r
+                               // ISSUEID is an SHA-1 hash\r
+                               String json = message.substring(43);\r
+                               sb.append(json);\r
+                               first = false;\r
+                       }\r
+                       sb.append(']');\r
+\r
+                       // Deserialize the JSON array as a Collection<Change>, this seems\r
+                       // slightly faster than deserializing each change by itself.\r
+                       Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),\r
+                                       new TypeToken<Collection<Change>>() {\r
+                                       }.getType());\r
+\r
+                       // create an issue object form the changes\r
+                       IssueModel issue = buildIssue(changes, true);\r
+\r
+                       // add the issue, conditionally, to the list\r
                        if (filter == null) {\r
                                list.add(issue);\r
                        } else {\r
@@ -104,19 +218,36 @@ public class IssueUtils {
                                }\r
                        }\r
                }\r
+\r
+               // sort the issues by creation\r
                Collections.sort(list);\r
                return list;\r
        }\r
 \r
        /**\r
-        * Retrieves the specified issue from the repository with complete changes\r
-        * history.\r
+        * Retrieves the specified issue from the repository with all changes\r
+        * applied to build the effective issue.\r
         * \r
         * @param repository\r
         * @param issueId\r
         * @return an issue, if it exists, otherwise null\r
         */\r
        public static IssueModel getIssue(Repository repository, String issueId) {\r
+               return getIssue(repository, issueId, true);\r
+       }\r
+\r
+       /**\r
+        * Retrieves the specified issue from the repository.\r
+        * \r
+        * @param repository\r
+        * @param issueId\r
+        * @param effective\r
+        *            if true, the effective issue is built by processing comment\r
+        *            changes, deletions, etc. if false, the raw issue is built\r
+        *            without consideration for comment changes, deletions, etc.\r
+        * @return an issue, if it exists, otherwise null\r
+        */\r
+       public static IssueModel getIssue(Repository repository, String issueId, boolean effective) {\r
                RefModel issuesBranch = getIssuesBranch(repository);\r
                if (issuesBranch == null) {\r
                        return null;\r
@@ -126,12 +257,88 @@ public class IssueUtils {
                        return null;\r
                }\r
 \r
-               // deserialize the issue model object\r
-               IssueModel issue = null;\r
                String issuePath = getIssuePath(issueId);\r
-               RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();\r
-               String json = JGitUtils.getStringContent(repository, tree, issuePath + "/issue.json");\r
-               issue = JsonUtils.fromJsonString(json, IssueModel.class);\r
+\r
+               // Collect all changes as JSON array from commit messages\r
+               List<RevCommit> commits = JGitUtils.getRevLog(repository, GB_ISSUES, issuePath, 0, -1);\r
+\r
+               // sort by commit order, first commit first\r
+               Collections.reverse(commits);\r
+\r
+               StringBuilder sb = new StringBuilder("[");\r
+               boolean first = true;\r
+               for (RevCommit commit : commits) {\r
+                       if (!first) {\r
+                               sb.append(',');\r
+                       }\r
+                       String message = commit.getFullMessage();\r
+                       // commit message is formatted: C ISSUEID\n\nJSON\r
+                       // C is an single char commit code\r
+                       // ISSUEID is an SHA-1 hash\r
+                       String json = message.substring(43);\r
+                       sb.append(json);\r
+                       first = false;\r
+               }\r
+               sb.append(']');\r
+\r
+               // Deserialize the JSON array as a Collection<Change>, this seems\r
+               // slightly faster than deserializing each change by itself.\r
+               Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),\r
+                               new TypeToken<Collection<Change>>() {\r
+                               }.getType());\r
+\r
+               // create an issue object and apply the changes to it\r
+               IssueModel issue = buildIssue(changes, effective);\r
+               return issue;\r
+       }\r
+\r
+       /**\r
+        * Builds an issue from a set of changes.\r
+        * \r
+        * @param changes\r
+        * @param effective\r
+        *            if true, the effective issue is built which accounts for\r
+        *            comment changes, comment deletions, etc. if false, the raw\r
+        *            issue is built.\r
+        * @return an issue\r
+        */\r
+       private static IssueModel buildIssue(Collection<Change> changes, boolean effective) {\r
+               IssueModel issue;\r
+               if (effective) {\r
+                       List<Change> effectiveChanges = new ArrayList<Change>();\r
+                       Map<String, Change> comments = new HashMap<String, Change>();\r
+                       for (Change change : changes) {\r
+                               if (change.comment != null) {\r
+                                       if (comments.containsKey(change.comment.id)) {\r
+                                               Change original = comments.get(change.comment.id);\r
+                                               Change clone = DeepCopier.copy(original);\r
+                                               clone.comment.text = change.comment.text;\r
+                                               clone.comment.deleted = change.comment.deleted;\r
+                                               int idx = effectiveChanges.indexOf(original);\r
+                                               effectiveChanges.remove(original);\r
+                                               effectiveChanges.add(idx, clone);\r
+                                               comments.put(clone.comment.id, clone);\r
+                                       } else {\r
+                                               effectiveChanges.add(change);\r
+                                               comments.put(change.comment.id, change);\r
+                                       }\r
+                               } else {\r
+                                       effectiveChanges.add(change);\r
+                               }\r
+                       }\r
+\r
+                       // effective issue\r
+                       issue = new IssueModel();\r
+                       for (Change change : effectiveChanges) {\r
+                               issue.applyChange(change);\r
+                       }\r
+               } else {\r
+                       // raw issue\r
+                       issue = new IssueModel();\r
+                       for (Change change : changes) {\r
+                               issue.applyChange(change);\r
+                       }\r
+               }\r
                return issue;\r
        }\r
 \r
@@ -155,10 +362,7 @@ public class IssueUtils {
                }\r
 \r
                // deserialize the issue model so that we have the attachment metadata\r
-               String issuePath = getIssuePath(issueId);\r
-               RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();\r
-               String json = JGitUtils.getStringContent(repository, tree, issuePath + "/issue.json");\r
-               IssueModel issue = JsonUtils.fromJsonString(json, IssueModel.class);\r
+               IssueModel issue = getIssue(repository, issueId, true);\r
                Attachment attachment = issue.getAttachment(filename);\r
 \r
                // attachment not found\r
@@ -167,15 +371,21 @@ public class IssueUtils {
                }\r
 \r
                // retrieve the attachment content\r
-               byte[] content = JGitUtils.getByteContent(repository, tree, issuePath + "/" + filename);\r
+               String issuePath = getIssuePath(issueId);\r
+               RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();\r
+               byte[] content = JGitUtils\r
+                               .getByteContent(repository, tree, issuePath + "/" + attachment.id);\r
                attachment.content = content;\r
                attachment.size = content.length;\r
                return attachment;\r
        }\r
 \r
        /**\r
-        * Stores an issue in the gb-issues branch of the repository. The branch is\r
-        * automatically created if it does not already exist.\r
+        * Creates an issue in the gb-issues branch of the repository. The branch is\r
+        * automatically created if it does not already exist. Your change must\r
+        * include an author, summary, and description, at a minimum. If your change\r
+        * does not have those minimum requirements a RuntimeException will be\r
+        * thrown.\r
         * \r
         * @param repository\r
         * @param change\r
@@ -186,31 +396,27 @@ public class IssueUtils {
                if (issuesBranch == null) {\r
                        JGitUtils.createOrphanBranch(repository, "gb-issues", null);\r
                }\r
-               change.created = new Date();\r
 \r
-               IssueModel issue = new IssueModel();\r
-               issue.created = change.created;\r
-               issue.summary = change.getString(Field.Summary);\r
-               issue.description = change.getString(Field.Description);\r
-               issue.reporter = change.getString(Field.Reporter);\r
-\r
-               if (StringUtils.isEmpty(issue.summary)) {\r
-                       throw new RuntimeException("Must specify an issue summary!");\r
+               if (StringUtils.isEmpty(change.author)) {\r
+                       throw new RuntimeException("Must specify a change author!");\r
                }\r
-               if (StringUtils.isEmpty(change.getString(Field.Description))) {\r
-                       throw new RuntimeException("Must specify an issue description!");\r
+               if (!change.hasField(Field.Summary)) {\r
+                       throw new RuntimeException("Must specify a summary!");\r
                }\r
-               if (StringUtils.isEmpty(change.getString(Field.Reporter))) {\r
-                       throw new RuntimeException("Must specify an issue reporter!");\r
+               if (!change.hasField(Field.Description)) {\r
+                       throw new RuntimeException("Must specify a description!");\r
                }\r
 \r
-               issue.id = StringUtils.getSHA1(issue.created.toString() + issue.reporter + issue.summary\r
-                               + issue.description);\r
+               change.setField(Field.Reporter, change.author);\r
+\r
+               String issueId = StringUtils.getSHA1(change.created.toString() + change.author\r
+                               + change.getString(Field.Summary) + change.getField(Field.Description));\r
+               change.setField(Field.Id, issueId);\r
+               change.code = '+';\r
 \r
-               String message = createChangelog('+', issue.id, change);\r
-               boolean success = commit(repository, issue, change, message);\r
+               boolean success = commit(repository, issueId, change);\r
                if (success) {\r
-                       return issue;\r
+                       return getIssue(repository, issueId, false);\r
                }\r
                return null;\r
        }\r
@@ -236,71 +442,254 @@ public class IssueUtils {
                }\r
 \r
                if (StringUtils.isEmpty(change.author)) {\r
-                       throw new RuntimeException("must specify change.author!");\r
+                       throw new RuntimeException("must specify a change author!");\r
                }\r
 \r
-               IssueModel issue = getIssue(repository, issueId);\r
-               change.created = new Date();\r
-\r
-               String message = createChangelog('=', issueId, change);\r
-               success = commit(repository, issue, change, message);\r
+               // determine update code\r
+               // default update code is '=' for a general change\r
+               change.code = '=';\r
+               if (change.hasField(Field.Status)) {\r
+                       Status status = Status.fromObject(change.getField(Field.Status));\r
+                       if (status.isClosed()) {\r
+                               // someone closed the issue\r
+                               change.code = 'x';\r
+                       }\r
+               }\r
+               success = commit(repository, issueId, change);\r
                return success;\r
        }\r
 \r
-       private static String createChangelog(char type, String issueId, Change change) {\r
-               return type + " " + issueId + "\n\n" + toJson(change);\r
+       /**\r
+        * Deletes an issue from the repository.\r
+        * \r
+        * @param repository\r
+        * @param issueId\r
+        * @return true if successful\r
+        */\r
+       public static boolean deleteIssue(Repository repository, String issueId, String author) {\r
+               boolean success = false;\r
+               RefModel issuesBranch = getIssuesBranch(repository);\r
+\r
+               if (issuesBranch == null) {\r
+                       throw new RuntimeException("gb-issues branch does not exist!");\r
+               }\r
+\r
+               if (StringUtils.isEmpty(issueId)) {\r
+                       throw new RuntimeException("must specify an issue id!");\r
+               }\r
+\r
+               String issuePath = getIssuePath(issueId);\r
+\r
+               String message = "- " + issueId;\r
+               try {\r
+                       ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");\r
+                       ObjectInserter odi = repository.newObjectInserter();\r
+                       try {\r
+                               // Create the in-memory index of the new/updated issue\r
+                               DirCache index = DirCache.newInCore();\r
+                               DirCacheBuilder dcBuilder = index.builder();\r
+                               // Traverse HEAD to add all other paths\r
+                               TreeWalk treeWalk = new TreeWalk(repository);\r
+                               int hIdx = -1;\r
+                               if (headId != null)\r
+                                       hIdx = treeWalk.addTree(new RevWalk(repository).parseTree(headId));\r
+                               treeWalk.setRecursive(true);\r
+                               while (treeWalk.next()) {\r
+                                       String path = treeWalk.getPathString();\r
+                                       CanonicalTreeParser hTree = null;\r
+                                       if (hIdx != -1)\r
+                                               hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);\r
+                                       if (!path.startsWith(issuePath)) {\r
+                                               // add entries from HEAD for all other paths\r
+                                               if (hTree != null) {\r
+                                                       // create a new DirCacheEntry with data retrieved\r
+                                                       // from HEAD\r
+                                                       final DirCacheEntry dcEntry = new DirCacheEntry(path);\r
+                                                       dcEntry.setObjectId(hTree.getEntryObjectId());\r
+                                                       dcEntry.setFileMode(hTree.getEntryFileMode());\r
+\r
+                                                       // add to temporary in-core index\r
+                                                       dcBuilder.add(dcEntry);\r
+                                               }\r
+                                       }\r
+                               }\r
+\r
+                               // release the treewalk\r
+                               treeWalk.release();\r
+\r
+                               // finish temporary in-core index used for this commit\r
+                               dcBuilder.finish();\r
+\r
+                               ObjectId indexTreeId = index.writeTree(odi);\r
+\r
+                               // Create a commit object\r
+                               PersonIdent ident = new PersonIdent(author, "gitblit@localhost");\r
+                               CommitBuilder commit = new CommitBuilder();\r
+                               commit.setAuthor(ident);\r
+                               commit.setCommitter(ident);\r
+                               commit.setEncoding(Constants.CHARACTER_ENCODING);\r
+                               commit.setMessage(message);\r
+                               commit.setParentId(headId);\r
+                               commit.setTreeId(indexTreeId);\r
+\r
+                               // Insert the commit into the repository\r
+                               ObjectId commitId = odi.insert(commit);\r
+                               odi.flush();\r
+\r
+                               RevWalk revWalk = new RevWalk(repository);\r
+                               try {\r
+                                       RevCommit revCommit = revWalk.parseCommit(commitId);\r
+                                       RefUpdate ru = repository.updateRef(GB_ISSUES);\r
+                                       ru.setNewObjectId(commitId);\r
+                                       ru.setExpectedOldObjectId(headId);\r
+                                       ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);\r
+                                       Result rc = ru.forceUpdate();\r
+                                       switch (rc) {\r
+                                       case NEW:\r
+                                       case FORCED:\r
+                                       case FAST_FORWARD:\r
+                                               success = true;\r
+                                               break;\r
+                                       case REJECTED:\r
+                                       case LOCK_FAILURE:\r
+                                               throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,\r
+                                                               ru.getRef(), rc);\r
+                                       default:\r
+                                               throw new JGitInternalException(MessageFormat.format(\r
+                                                               JGitText.get().updatingRefFailed, GB_ISSUES, commitId.toString(),\r
+                                                               rc));\r
+                                       }\r
+                               } finally {\r
+                                       revWalk.release();\r
+                               }\r
+                       } finally {\r
+                               odi.release();\r
+                       }\r
+               } catch (Throwable t) {\r
+                       error(t, repository, "Failed to delete issue {1} to {0}", issueId);\r
+               }\r
+               return success;\r
        }\r
 \r
        /**\r
+        * Changes the text of an issue comment.\r
         * \r
         * @param repository\r
         * @param issue\r
         * @param change\r
-        * @param changelog\r
-        * @return\r
+        *            the change with the comment to change\r
+        * @param author\r
+        *            the author of the revision\r
+        * @param comment\r
+        *            the revised comment\r
+        * @return true, if the change was successful\r
         */\r
-       private static boolean commit(Repository repository, IssueModel issue, Change change,\r
-                       String changelog) {\r
-               boolean success = false;\r
-               String issuePath = getIssuePath(issue.id);\r
-               try {\r
-                       issue.addChange(change);\r
+       public static boolean changeComment(Repository repository, IssueModel issue, Change change,\r
+                       String author, String comment) {\r
+               Change revision = new Change(author);\r
+               revision.comment(comment);\r
+               revision.comment.id = change.comment.id;\r
+               return updateIssue(repository, issue.id, revision);\r
+       }\r
 \r
-                       // serialize the issue as json\r
-                       String json = toJson(issue);\r
+       /**\r
+        * Deletes a comment from an issue.\r
+        * \r
+        * @param repository\r
+        * @param issue\r
+        * @param change\r
+        *            the change with the comment to delete\r
+        * @param author\r
+        * @return true, if the deletion was successful\r
+        */\r
+       public static boolean deleteComment(Repository repository, IssueModel issue, Change change,\r
+                       String author) {\r
+               Change deletion = new Change(author);\r
+               deletion.comment(change.comment.text);\r
+               deletion.comment.id = change.comment.id;\r
+               deletion.comment.deleted = true;\r
+               return updateIssue(repository, issue.id, deletion);\r
+       }\r
 \r
-                       // cache the issue "files" in a map\r
-                       Map<String, CommitFile> files = new HashMap<String, CommitFile>();\r
-                       CommitFile issueFile = new CommitFile(issuePath + "/issue.json", change.created);\r
-                       issueFile.content = json.getBytes(Constants.CHARACTER_ENCODING);\r
-                       files.put(issueFile.path, issueFile);\r
+       /**\r
+        * Commit a change to the repository. Each issue is composed on changes.\r
+        * Issues are built from applying the changes in the order they were\r
+        * committed to the repository. The changes are actually specified in the\r
+        * commit messages and not in the RevTrees which allows for clean,\r
+        * distributed merging.\r
+        * \r
+        * @param repository\r
+        * @param issue\r
+        * @param change\r
+        * @return true, if the change was committed\r
+        */\r
+       private static boolean commit(Repository repository, String issueId, Change change) {\r
+               boolean success = false;\r
 \r
+               try {\r
+                       // assign ids to new attachments\r
+                       // attachments are stored by an SHA1 id\r
                        if (change.hasAttachments()) {\r
                                for (Attachment attachment : change.attachments) {\r
                                        if (!ArrayUtils.isEmpty(attachment.content)) {\r
-                                               CommitFile file = new CommitFile(issuePath + "/" + attachment.name,\r
-                                                               change.created);\r
-                                               file.content = attachment.content;\r
-                                               files.put(file.path, file);\r
+                                               byte[] prefix = (change.created.toString() + change.author).getBytes();\r
+                                               byte[] bytes = new byte[prefix.length + attachment.content.length];\r
+                                               System.arraycopy(prefix, 0, bytes, 0, prefix.length);\r
+                                               System.arraycopy(attachment.content, 0, bytes, prefix.length,\r
+                                                               attachment.content.length);\r
+                                               attachment.id = "attachment-" + StringUtils.getSHA1(bytes);\r
                                        }\r
                                }\r
                        }\r
 \r
-                       ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");\r
+                       // serialize the change as json\r
+                       // exclude any attachment from json serialization\r
+                       Gson gson = JsonUtils.gson(new ExcludeField(\r
+                                       "com.gitblit.models.IssueModel$Attachment.content"));\r
+                       String json = gson.toJson(change);\r
+\r
+                       // include the json change in the commit message\r
+                       String issuePath = getIssuePath(issueId);\r
+                       String message = change.code + " " + issueId + "\n\n" + json;\r
+\r
+                       // Create a commit file. This is required for a proper commit and\r
+                       // ensures we can retrieve the commit log of the issue path.\r
+                       //\r
+                       // This file is NOT serialized as part of the Change object.\r
+                       switch (change.code) {\r
+                       case '+': {\r
+                               // New Issue.\r
+                               Attachment placeholder = new Attachment("issue");\r
+                               placeholder.id = placeholder.name;\r
+                               placeholder.content = "DO NOT REMOVE".getBytes(Constants.CHARACTER_ENCODING);\r
+                               change.addAttachment(placeholder);\r
+                               break;\r
+                       }\r
+                       default: {\r
+                               // Update Issue.\r
+                               String changeId = StringUtils.getSHA1(json);\r
+                               Attachment placeholder = new Attachment("change-" + changeId);\r
+                               placeholder.id = placeholder.name;\r
+                               placeholder.content = "REMOVABLE".getBytes(Constants.CHARACTER_ENCODING);\r
+                               change.addAttachment(placeholder);\r
+                               break;\r
+                       }\r
+                       }\r
 \r
+                       ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");\r
                        ObjectInserter odi = repository.newObjectInserter();\r
                        try {\r
-                               // Create the in-memory index of the new/updated issue.\r
-                               DirCache index = createIndex(repository, headId, files);\r
+                               // Create the in-memory index of the new/updated issue\r
+                               DirCache index = createIndex(repository, headId, issuePath, change);\r
                                ObjectId indexTreeId = index.writeTree(odi);\r
 \r
                                // Create a commit object\r
-                               PersonIdent author = new PersonIdent(issue.reporter, issue.reporter + "@gitblit");\r
+                               PersonIdent ident = new PersonIdent(change.author, "gitblit@localhost");\r
                                CommitBuilder commit = new CommitBuilder();\r
-                               commit.setAuthor(author);\r
-                               commit.setCommitter(author);\r
+                               commit.setAuthor(ident);\r
+                               commit.setCommitter(ident);\r
                                commit.setEncoding(Constants.CHARACTER_ENCODING);\r
-                               commit.setMessage(changelog);\r
+                               commit.setMessage(message);\r
                                commit.setParentId(headId);\r
                                commit.setTreeId(indexTreeId);\r
 \r
@@ -338,23 +727,11 @@ public class IssueUtils {
                                odi.release();\r
                        }\r
                } catch (Throwable t) {\r
-                       t.printStackTrace();\r
+                       error(t, repository, "Failed to commit issue {1} to {0}", issueId);\r
                }\r
                return success;\r
        }\r
 \r
-       private static String toJson(Object o) {\r
-               try {\r
-                       // exclude the attachment content field from json serialization\r
-                       Gson gson = JsonUtils.gson(new ExcludeField(\r
-                                       "com.gitblit.models.IssueModel$Attachment.content"));\r
-                       String json = gson.toJson(o);\r
-                       return json;\r
-               } catch (Throwable t) {\r
-                       throw new RuntimeException(t);\r
-               }\r
-       }\r
-\r
        /**\r
         * Returns the issue path. This follows the same scheme as Git's object\r
         * store path where the first two characters of the hash id are the root\r
@@ -372,32 +749,38 @@ public class IssueUtils {
         * \r
         * @param repo\r
         * @param headId\r
-        * @param files\r
-        * @param time\r
+        * @param change\r
         * @return an in-memory index\r
         * @throws IOException\r
         */\r
-       private static DirCache createIndex(Repository repo, ObjectId headId,\r
-                       Map<String, CommitFile> files) throws IOException {\r
+       private static DirCache createIndex(Repository repo, ObjectId headId, String issuePath,\r
+                       Change change) throws IOException {\r
 \r
                DirCache inCoreIndex = DirCache.newInCore();\r
                DirCacheBuilder dcBuilder = inCoreIndex.builder();\r
                ObjectInserter inserter = repo.newObjectInserter();\r
 \r
+               Set<String> ignorePaths = new TreeSet<String>();\r
                try {\r
-                       // Add the issue files to the temporary index\r
-                       for (CommitFile file : files.values()) {\r
-                               // create an index entry for the file\r
-                               final DirCacheEntry dcEntry = new DirCacheEntry(file.path);\r
-                               dcEntry.setLength(file.content.length);\r
-                               dcEntry.setLastModified(file.time);\r
-                               dcEntry.setFileMode(FileMode.REGULAR_FILE);\r
-\r
-                               // insert object\r
-                               dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, file.content));\r
-\r
-                               // add to temporary in-core index\r
-                               dcBuilder.add(dcEntry);\r
+                       // Add any attachments to the temporary index\r
+                       if (change.hasAttachments()) {\r
+                               for (Attachment attachment : change.attachments) {\r
+                                       // build a path name for the attachment and mark as ignored\r
+                                       String path = issuePath + "/" + attachment.id;\r
+                                       ignorePaths.add(path);\r
+\r
+                                       // create an index entry for this attachment\r
+                                       final DirCacheEntry dcEntry = new DirCacheEntry(path);\r
+                                       dcEntry.setLength(attachment.content.length);\r
+                                       dcEntry.setLastModified(change.created.getTime());\r
+                                       dcEntry.setFileMode(FileMode.REGULAR_FILE);\r
+\r
+                                       // insert object\r
+                                       dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, attachment.content));\r
+\r
+                                       // add to temporary in-core index\r
+                                       dcBuilder.add(dcEntry);\r
+                               }\r
                        }\r
 \r
                        // Traverse HEAD to add all other paths\r
@@ -412,7 +795,7 @@ public class IssueUtils {
                                CanonicalTreeParser hTree = null;\r
                                if (hIdx != -1)\r
                                        hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);\r
-                               if (!files.containsKey(path)) {\r
+                               if (!ignorePaths.contains(path)) {\r
                                        // add entries from HEAD for all other paths\r
                                        if (hTree != null) {\r
                                                // create a new DirCacheEntry with data retrieved from\r
@@ -437,19 +820,4 @@ public class IssueUtils {
                }\r
                return inCoreIndex;\r
        }\r
-\r
-       private static class CommitFile {\r
-               String path;\r
-               long time;\r
-               byte[] content;\r
-\r
-               CommitFile(String path, Date date) {\r
-                       this.path = path;\r
-                       this.time = date.getTime();\r
-               }\r
-       }\r
-\r
-       public static interface IssueFilter {\r
-               public abstract boolean accept(IssueModel issue);\r
-       }\r
-}\r
+}
\ No newline at end of file
index 5d6011a2c933f320875ac49a844a18bd4a73c3bc..96e6bf100a58af093b0a69f268029b1545c0c879 100644 (file)
@@ -1408,7 +1408,7 @@ public class JGitUtils {
 \r
                                // Create a tree object to reference from a commit\r
                                TreeFormatter tree = new TreeFormatter();\r
-                               tree.append("NEWBRANCH", FileMode.REGULAR_FILE, blobId);\r
+                               tree.append(".branch", FileMode.REGULAR_FILE, blobId);\r
                                ObjectId treeId = odi.insert(tree);\r
 \r
                                // Create a commit object\r
index 747ce1f3073a8473fd50b8f5e2ea6866811b9fc9..9e5caf0b238874db6a2813ed4d9998e453eaf5fb 100644 (file)
@@ -52,7 +52,7 @@ import com.gitblit.utils.JGitUtils;
                ObjectCacheTest.class, UserServiceTest.class, MarkdownUtilsTest.class, JGitUtilsTest.class,\r
                SyndicationUtilsTest.class, DiffUtilsTest.class, MetricUtilsTest.class,\r
                TicgitUtilsTest.class, GitBlitTest.class, FederationTests.class, RpcTests.class,\r
-               GitServletTest.class, GroovyScriptTest.class })\r
+               GitServletTest.class, GroovyScriptTest.class, IssuesTest.class })\r
 public class GitBlitSuite {\r
 \r
        public static final File REPOSITORIES = new File("git");\r
index 1522ec69892aa2423f4fdf6b1c5e10c68f6586bf..26b599567994e77753d9accc98754089d61820e0 100644 (file)
@@ -16,6 +16,7 @@
 package com.gitblit.tests;\r
 \r
 import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertFalse;\r
 import static org.junit.Assert.assertNotNull;\r
 import static org.junit.Assert.assertTrue;\r
 \r
@@ -30,16 +31,23 @@ import com.gitblit.models.IssueModel.Attachment;
 import com.gitblit.models.IssueModel.Change;\r
 import com.gitblit.models.IssueModel.Field;\r
 import com.gitblit.models.IssueModel.Priority;\r
+import com.gitblit.models.IssueModel.Status;\r
 import com.gitblit.utils.IssueUtils;\r
 import com.gitblit.utils.IssueUtils.IssueFilter;\r
 \r
+/**\r
+ * Tests the mechanics of distributed issue management on the gb-issues branch.\r
+ * \r
+ * @author James Moger\r
+ * \r
+ */\r
 public class IssuesTest {\r
 \r
        @Test\r
-       public void testInsertion() throws Exception {\r
+       public void testCreation() throws Exception {\r
                Repository repository = GitBlitSuite.getIssuesTestRepository();\r
                // create and insert the issue\r
-               Change c1 = newChange("Test issue " + Long.toHexString(System.currentTimeMillis()));\r
+               Change c1 = newChange("testCreation() " + Long.toHexString(System.currentTimeMillis()));\r
                IssueModel issue = IssueUtils.createIssue(repository, c1);\r
                assertNotNull(issue.id);\r
 \r
@@ -47,68 +55,165 @@ public class IssuesTest {
                IssueModel constructed = IssueUtils.getIssue(repository, issue.id);\r
                compare(issue, constructed);\r
 \r
-               // add a note and update\r
-               Change c2 = new Change();\r
-               c2.author = "dave";\r
-               c2.comment("yeah, this is working");            \r
+               assertEquals(1, constructed.changes.size());\r
+       }\r
+\r
+       @Test\r
+       public void testUpdates() throws Exception {\r
+               Repository repository = GitBlitSuite.getIssuesTestRepository();\r
+               // C1: create the issue\r
+               Change c1 = newChange("testUpdates() " + Long.toHexString(System.currentTimeMillis()));\r
+               IssueModel issue = IssueUtils.createIssue(repository, c1);\r
+               assertNotNull(issue.id);\r
+\r
+               IssueModel constructed = IssueUtils.getIssue(repository, issue.id);\r
+               compare(issue, constructed);\r
+\r
+               // C2: set owner\r
+               Change c2 = new Change("C2");\r
+               c2.comment("I'll fix this");\r
+               c2.setField(Field.Owner, c2.author);\r
                assertTrue(IssueUtils.updateIssue(repository, issue.id, c2));\r
+               constructed = IssueUtils.getIssue(repository, issue.id);\r
+               assertEquals(2, constructed.changes.size());\r
+               assertEquals(c2.author, constructed.owner);\r
+\r
+               // C3: add a note\r
+               Change c3 = new Change("C3");\r
+               c3.comment("yeah, this is working");\r
+               assertTrue(IssueUtils.updateIssue(repository, issue.id, c3));\r
+               constructed = IssueUtils.getIssue(repository, issue.id);\r
+               assertEquals(3, constructed.changes.size());\r
+\r
+               // C4: add attachment\r
+               Change c4 = new Change("C4");\r
+               Attachment a = newAttachment();\r
+               c4.addAttachment(a);\r
+               assertTrue(IssueUtils.updateIssue(repository, issue.id, c4));\r
+\r
+               Attachment a1 = IssueUtils.getIssueAttachment(repository, issue.id, a.name);\r
+               assertEquals(a.content.length, a1.content.length);\r
+               assertTrue(Arrays.areEqual(a.content, a1.content));\r
+\r
+               // C5: close the issue\r
+               Change c5 = new Change("C5");\r
+               c5.comment("closing issue");\r
+               c5.setField(Field.Status, Status.Fixed);\r
+               assertTrue(IssueUtils.updateIssue(repository, issue.id, c5));\r
 \r
                // retrieve issue again\r
                constructed = IssueUtils.getIssue(repository, issue.id);\r
 \r
-               assertEquals(2, constructed.changes.size());\r
+               assertEquals(5, constructed.changes.size());\r
+               assertTrue(constructed.status.isClosed());\r
 \r
-               Attachment a = IssueUtils.getIssueAttachment(repository, issue.id, "test.txt");\r
                repository.close();\r
-\r
-               assertEquals(10, a.content.length);\r
-               assertTrue(Arrays.areEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, a.content));\r
        }\r
 \r
        @Test\r
        public void testQuery() throws Exception {\r
                Repository repository = GitBlitSuite.getIssuesTestRepository();\r
-               List<IssueModel> list = IssueUtils.getIssues(repository, null);\r
-               List<IssueModel> list2 = IssueUtils.getIssues(repository, new IssueFilter() {\r
-                       boolean hasFirst = false;\r
+               List<IssueModel> allIssues = IssueUtils.getIssues(repository, null);\r
+\r
+               List<IssueModel> openIssues = IssueUtils.getIssues(repository, new IssueFilter() {\r
                        @Override\r
                        public boolean accept(IssueModel issue) {\r
-                               if (!hasFirst) {\r
-                                       hasFirst = true;\r
-                                       return true;\r
-                               }\r
-                               return false;\r
+                               return !issue.status.isClosed();\r
                        }\r
                });\r
+\r
+               List<IssueModel> closedIssues = IssueUtils.getIssues(repository, new IssueFilter() {\r
+                       @Override\r
+                       public boolean accept(IssueModel issue) {\r
+                               return issue.status.isClosed();\r
+                       }\r
+               });\r
+\r
+               repository.close();\r
+               assertTrue(allIssues.size() > 0);\r
+               assertEquals(1, openIssues.size());\r
+               assertEquals(1, closedIssues.size());\r
+       }\r
+\r
+       @Test\r
+       public void testDelete() throws Exception {\r
+               Repository repository = GitBlitSuite.getIssuesTestRepository();\r
+               List<IssueModel> allIssues = IssueUtils.getIssues(repository, null);\r
+               // delete all issues\r
+               for (IssueModel issue : allIssues) {\r
+                       assertTrue(IssueUtils.deleteIssue(repository, issue.id, "D"));\r
+               }\r
+               repository.close();\r
+       }\r
+\r
+       @Test\r
+       public void testChangeComment() throws Exception {\r
+               Repository repository = GitBlitSuite.getIssuesTestRepository();\r
+               // C1: create the issue\r
+               Change c1 = newChange("testChangeComment() " + Long.toHexString(System.currentTimeMillis()));\r
+               IssueModel issue = IssueUtils.createIssue(repository, c1);\r
+               assertNotNull(issue.id);\r
+               assertTrue(issue.changes.get(0).hasComment());\r
+\r
+               assertTrue(IssueUtils.changeComment(repository, issue, c1, "E1", "I changed the comment"));\r
+               issue = IssueUtils.getIssue(repository, issue.id);\r
+               assertTrue(issue.changes.get(0).hasComment());\r
+               assertEquals("I changed the comment", issue.changes.get(0).comment.text);\r
+\r
+               assertTrue(IssueUtils.deleteIssue(repository, issue.id, "D"));\r
+\r
+               repository.close();\r
+       }\r
+\r
+       @Test\r
+       public void testDeleteComment() throws Exception {\r
+               Repository repository = GitBlitSuite.getIssuesTestRepository();\r
+               // C1: create the issue\r
+               Change c1 = newChange("testDeleteComment() " + Long.toHexString(System.currentTimeMillis()));\r
+               IssueModel issue = IssueUtils.createIssue(repository, c1);\r
+               assertNotNull(issue.id);\r
+               assertTrue(issue.changes.get(0).hasComment());\r
+\r
+               assertTrue(IssueUtils.deleteComment(repository, issue, c1, "D1"));\r
+               issue = IssueUtils.getIssue(repository, issue.id);\r
+               assertEquals(1, issue.changes.size());\r
+               assertFalse(issue.changes.get(0).hasComment());\r
+\r
+               issue = IssueUtils.getIssue(repository, issue.id, false);\r
+               assertEquals(2, issue.changes.size());\r
+               assertTrue(issue.changes.get(0).hasComment());\r
+               assertFalse(issue.changes.get(1).hasComment());\r
+\r
+               assertTrue(IssueUtils.deleteIssue(repository, issue.id, "D"));\r
+\r
                repository.close();\r
-               assertTrue(list.size() > 0);\r
-               assertEquals(1, list2.size());\r
        }\r
 \r
        private Change newChange(String summary) {\r
-               Change change = new Change();\r
-               change.setField(Field.Reporter, "james");\r
-               change.setField(Field.Owner, "dave");\r
+               Change change = new Change("C1");\r
                change.setField(Field.Summary, summary);\r
                change.setField(Field.Description, "this is my description");\r
                change.setField(Field.Priority, Priority.High);\r
                change.setField(Field.Labels, "helpdesk");\r
                change.comment("my comment");\r
-               \r
-               Attachment attachment = new Attachment("test.txt");             \r
-               attachment.content = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };\r
-               change.addAttachment(attachment);               \r
-               \r
                return change;\r
        }\r
 \r
+       private Attachment newAttachment() {\r
+               Attachment attachment = new Attachment(Long.toHexString(System.currentTimeMillis())\r
+                               + ".txt");\r
+               attachment.content = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,\r
+                               0x4a };\r
+               return attachment;\r
+       }\r
+\r
        private void compare(IssueModel issue, IssueModel constructed) {\r
                assertEquals(issue.id, constructed.id);\r
                assertEquals(issue.reporter, constructed.reporter);\r
                assertEquals(issue.owner, constructed.owner);\r
-               assertEquals(issue.created.getTime() / 1000, constructed.created.getTime() / 1000);\r
                assertEquals(issue.summary, constructed.summary);\r
                assertEquals(issue.description, constructed.description);\r
+               assertEquals(issue.created, constructed.created);\r
 \r
                assertTrue(issue.hasLabel("helpdesk"));\r
        }\r