]> source.dussan.org Git - gitblit.git/commitdiff
Branch for implementing distributed gb-issues
authorJames Moger <james.moger@gitblit.com>
Fri, 13 Jan 2012 12:58:12 +0000 (07:58 -0500)
committerJames Moger <james.moger@gitblit.com>
Fri, 13 Jan 2012 12:58:12 +0000 (07:58 -0500)
src/com/gitblit/models/IssueModel.java [new file with mode: 0644]
src/com/gitblit/utils/IssueUtils.java [new file with mode: 0644]
src/com/gitblit/utils/JGitUtils.java
src/com/gitblit/utils/JsonUtils.java
tests/com/gitblit/tests/GitBlitSuite.java
tests/com/gitblit/tests/IssuesTest.java [new file with mode: 0644]

diff --git a/src/com/gitblit/models/IssueModel.java b/src/com/gitblit/models/IssueModel.java
new file mode 100644 (file)
index 0000000..3c6d9a0
--- /dev/null
@@ -0,0 +1,310 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.models;\r
+\r
+import java.io.Serializable;\r
+import java.util.ArrayList;\r
+import java.util.Date;\r
+import java.util.List;\r
+\r
+import com.gitblit.utils.ArrayUtils;\r
+import com.gitblit.utils.StringUtils;\r
+\r
+/**\r
+ * The Gitblit Issue model, its component classes, and enums.\r
+ * \r
+ * @author James Moger\r
+ * \r
+ */\r
+public class IssueModel implements Serializable, Comparable<IssueModel> {\r
+\r
+       private static final long serialVersionUID = 1L;;\r
+\r
+       public String id;\r
+\r
+       public Type type;\r
+\r
+       public Status status;\r
+\r
+       public Priority priority;\r
+\r
+       public Date created;\r
+\r
+       public String summary;\r
+\r
+       public String description;\r
+\r
+       public String reporter;\r
+\r
+       public String owner;\r
+\r
+       public String milestone;\r
+\r
+       public List<Change> changes;\r
+\r
+       public IssueModel() {\r
+               created = new Date();\r
+\r
+               type = Type.Defect;\r
+               status = Status.New;\r
+               priority = Priority.Medium;\r
+\r
+               changes = new ArrayList<Change>();\r
+       }\r
+\r
+       public String getStatus() {\r
+               String s = status.toString();\r
+               if (!StringUtils.isEmpty(owner))\r
+                       s += " (" + owner + ")";\r
+               return s;\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
+                       }\r
+               }\r
+               if (!StringUtils.isEmpty(labels)) {\r
+                       list.addAll(StringUtils.getStringsFromValue(labels, " "));\r
+               }\r
+               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
+                       if (change.hasAttachments()) {\r
+                               Attachment a = change.getAttachment(name);\r
+                               if (a != null) {\r
+                                       attachment = a;\r
+                               }\r
+                       }\r
+               }\r
+               return attachment;\r
+       }\r
+\r
+       public void addChange(Change change) {\r
+               if (changes == null) {\r
+                       changes = new ArrayList<Change>();\r
+               }\r
+               changes.add(change);\r
+       }\r
+\r
+       @Override\r
+       public String toString() {\r
+               return summary;\r
+       }\r
+\r
+       @Override\r
+       public int compareTo(IssueModel o) {\r
+               return o.created.compareTo(created);\r
+       }\r
+\r
+       @Override\r
+       public boolean equals(Object o) {\r
+               if (o instanceof IssueModel)\r
+                       return id.equals(((IssueModel) o).id);\r
+               return super.equals(o);\r
+       }\r
+\r
+       @Override\r
+       public int hashCode() {\r
+               return id.hashCode();\r
+       }\r
+\r
+       public static class Change implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+\r
+               public Date created;\r
+\r
+               public String author;\r
+\r
+               public Comment comment;\r
+\r
+               public List<FieldChange> fieldChanges;\r
+\r
+               public List<Attachment> attachments;\r
+\r
+               public void comment(String text) {\r
+                       comment = new Comment(text);\r
+               }\r
+\r
+               public boolean hasComment() {\r
+                       return comment != null;\r
+               }\r
+\r
+               public boolean hasAttachments() {\r
+                       return !ArrayUtils.isEmpty(attachments);\r
+               }\r
+\r
+               public boolean hasFieldChanges() {\r
+                       return !ArrayUtils.isEmpty(fieldChanges);\r
+               }\r
+\r
+               public FieldChange getField(Field field) {\r
+                       if (fieldChanges != null) {\r
+                               for (FieldChange fieldChange : fieldChanges) {\r
+                                       if (fieldChange.field == field) {\r
+                                               return fieldChange;\r
+                                       }\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
+\r
+               public void setField(Field field, Object value) {\r
+                       FieldChange fieldChange = new FieldChange();\r
+                       fieldChange.field = field;\r
+                       fieldChange.value = value;\r
+                       if (fieldChanges == null) {\r
+                               fieldChanges = new ArrayList<FieldChange>();\r
+                       }\r
+                       fieldChanges.add(fieldChange);\r
+               }\r
+\r
+               public String getString(Field field) {\r
+                       FieldChange fieldChange = getField(field);\r
+                       if (fieldChange == null) {\r
+                               return null;\r
+                       }\r
+                       return fieldChange.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
+               }\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
+               @Override\r
+               public String toString() {\r
+                       return created.toString() + " by " + author;\r
+               }\r
+       }\r
+\r
+       public static class Comment implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+\r
+               public String text;\r
+               public boolean deleted;\r
+\r
+               Comment(String text) {\r
+                       this.text = text;\r
+               }\r
+\r
+               @Override\r
+               public String toString() {\r
+                       return text;\r
+               }\r
+       }\r
+\r
+       public static class FieldChange implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+\r
+               public Field field;\r
+\r
+               public Object value;\r
+\r
+               @Override\r
+               public String toString() {\r
+                       return field + ": " + value;\r
+               }\r
+       }\r
+\r
+       public static class Attachment implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+\r
+               public String name;\r
+               public long size;\r
+               public byte[] content;\r
+               public boolean deleted;\r
+\r
+               public Attachment(String name) {\r
+                       this.name = name;\r
+               }\r
+\r
+               @Override\r
+               public String toString() {\r
+                       return name;\r
+               }\r
+       }\r
+\r
+       public static enum Field {\r
+               Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Labels;\r
+       }\r
+\r
+       public static enum Type {\r
+               Defect, Enhancement, Task, Review, Other;\r
+       }\r
+\r
+       public static enum Priority {\r
+               Low, Medium, High, Critical;\r
+       }\r
+\r
+       public static enum Status {\r
+               New, Accepted, Started, Review, Queued, Testing, Done, Fixed, WontFix, Duplicate, Invalid;\r
+\r
+               public boolean atLeast(Status status) {\r
+                       return ordinal() >= status.ordinal();\r
+               }\r
+\r
+               public boolean exceeds(Status status) {\r
+                       return ordinal() > status.ordinal();\r
+               }\r
+\r
+               public Status next() {\r
+                       switch (this) {\r
+                       case New:\r
+                               return Started;\r
+                       case Accepted:\r
+                               return Started;\r
+                       case Started:\r
+                               return Testing;\r
+                       case Review:\r
+                               return Testing;\r
+                       case Queued:\r
+                               return Testing;\r
+                       case Testing:\r
+                               return Done;\r
+                       }\r
+                       return Accepted;\r
+               }\r
+       }\r
+}\r
diff --git a/src/com/gitblit/utils/IssueUtils.java b/src/com/gitblit/utils/IssueUtils.java
new file mode 100644 (file)
index 0000000..8217070
--- /dev/null
@@ -0,0 +1,455 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.utils;\r
+\r
+import java.io.IOException;\r
+import java.text.MessageFormat;\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.Collections;\r
+import java.util.Date;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import org.eclipse.jgit.JGitText;\r
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;\r
+import org.eclipse.jgit.api.errors.JGitInternalException;\r
+import org.eclipse.jgit.dircache.DirCache;\r
+import org.eclipse.jgit.dircache.DirCacheBuilder;\r
+import org.eclipse.jgit.dircache.DirCacheEntry;\r
+import org.eclipse.jgit.lib.CommitBuilder;\r
+import org.eclipse.jgit.lib.Constants;\r
+import org.eclipse.jgit.lib.FileMode;\r
+import org.eclipse.jgit.lib.ObjectId;\r
+import org.eclipse.jgit.lib.ObjectInserter;\r
+import org.eclipse.jgit.lib.PersonIdent;\r
+import org.eclipse.jgit.lib.RefUpdate;\r
+import org.eclipse.jgit.lib.RefUpdate.Result;\r
+import org.eclipse.jgit.lib.Repository;\r
+import org.eclipse.jgit.revwalk.RevCommit;\r
+import org.eclipse.jgit.revwalk.RevTree;\r
+import org.eclipse.jgit.revwalk.RevWalk;\r
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;\r
+import org.eclipse.jgit.treewalk.TreeWalk;\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.RefModel;\r
+import com.gitblit.utils.JsonUtils.ExcludeField;\r
+import com.google.gson.Gson;\r
+\r
+/**\r
+ * Utility class for reading Gitblit issues.\r
+ * \r
+ * @author James Moger\r
+ * \r
+ */\r
+public class IssueUtils {\r
+\r
+       public static final String GB_ISSUES = "refs/heads/gb-issues";\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
+        * \r
+        * @param repository\r
+        * @return a refmodel for the gb-issues branch or null\r
+        */\r
+       public static RefModel getIssuesBranch(Repository repository) {\r
+               return JGitUtils.getBranch(repository, "gb-issues");\r
+       }\r
+\r
+       /**\r
+        * Returns all the issues in the repository.\r
+        * \r
+        * @param repository\r
+        * @param filter\r
+        *            optional issue filter to only return matching results\r
+        * @return a list of issues\r
+        */\r
+       public static List<IssueModel> getIssues(Repository repository, IssueFilter filter) {\r
+               List<IssueModel> list = new ArrayList<IssueModel>();\r
+               RefModel issuesBranch = getIssuesBranch(repository);\r
+               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
+                       if (filter == null) {\r
+                               list.add(issue);\r
+                       } else {\r
+                               if (filter.accept(issue)) {\r
+                                       list.add(issue);\r
+                               }\r
+                       }\r
+               }\r
+               Collections.sort(list);\r
+               return list;\r
+       }\r
+\r
+       /**\r
+        * Retrieves the specified issue from the repository with complete changes\r
+        * history.\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
+               RefModel issuesBranch = getIssuesBranch(repository);\r
+               if (issuesBranch == null) {\r
+                       return null;\r
+               }\r
+\r
+               if (StringUtils.isEmpty(issueId)) {\r
+                       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
+               return issue;\r
+       }\r
+\r
+       /**\r
+        * Retrieves the specified attachment from an issue.\r
+        * \r
+        * @param repository\r
+        * @param issueId\r
+        * @param filename\r
+        * @return an attachment, if found, null otherwise\r
+        */\r
+       public static Attachment getIssueAttachment(Repository repository, String issueId,\r
+                       String filename) {\r
+               RefModel issuesBranch = getIssuesBranch(repository);\r
+               if (issuesBranch == null) {\r
+                       return null;\r
+               }\r
+\r
+               if (StringUtils.isEmpty(issueId)) {\r
+                       return null;\r
+               }\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
+               Attachment attachment = issue.getAttachment(filename);\r
+\r
+               // attachment not found\r
+               if (attachment == null) {\r
+                       return null;\r
+               }\r
+\r
+               // retrieve the attachment content\r
+               byte[] content = JGitUtils.getByteContent(repository, tree, issuePath + "/" + filename);\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
+        * \r
+        * @param repository\r
+        * @param change\r
+        * @return true if successful\r
+        */\r
+       public static IssueModel createIssue(Repository repository, Change change) {\r
+               RefModel issuesBranch = getIssuesBranch(repository);\r
+               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
+               }\r
+               if (StringUtils.isEmpty(change.getString(Field.Description))) {\r
+                       throw new RuntimeException("Must specify an issue description!");\r
+               }\r
+               if (StringUtils.isEmpty(change.getString(Field.Reporter))) {\r
+                       throw new RuntimeException("Must specify an issue reporter!");\r
+               }\r
+\r
+               issue.id = StringUtils.getSHA1(issue.created.toString() + issue.reporter + issue.summary\r
+                               + issue.description);\r
+\r
+               String message = createChangelog('+', issue.id, change);\r
+               boolean success = commit(repository, issue, change, message);\r
+               if (success) {\r
+                       return issue;\r
+               }\r
+               return null;\r
+       }\r
+\r
+       /**\r
+        * Updates an issue in the gb-issues branch of the repository.\r
+        * \r
+        * @param repository\r
+        * @param issue\r
+        * @param change\r
+        * @return true if successful\r
+        */\r
+       public static boolean updateIssue(Repository repository, String issueId, Change change) {\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 (change == null) {\r
+                       throw new RuntimeException("change can not be null!");\r
+               }\r
+\r
+               if (StringUtils.isEmpty(change.author)) {\r
+                       throw new RuntimeException("must specify 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
+               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
+\r
+       /**\r
+        * \r
+        * @param repository\r
+        * @param issue\r
+        * @param change\r
+        * @param changelog\r
+        * @return\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
+\r
+                       // serialize the issue as json\r
+                       String json = toJson(issue);\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
+                       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
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");\r
+\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
+                               ObjectId indexTreeId = index.writeTree(odi);\r
+\r
+                               // Create a commit object\r
+                               PersonIdent author = new PersonIdent(issue.reporter, issue.reporter + "@gitblit");\r
+                               CommitBuilder commit = new CommitBuilder();\r
+                               commit.setAuthor(author);\r
+                               commit.setCommitter(author);\r
+                               commit.setEncoding(Constants.CHARACTER_ENCODING);\r
+                               commit.setMessage(changelog);\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
+                       t.printStackTrace();\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
+        * folder with the remaining characters as a subfolder within that folder.\r
+        * \r
+        * @param issueId\r
+        * @return the root path of the issue content on the gb-issues branch\r
+        */\r
+       private static String getIssuePath(String issueId) {\r
+               return issueId.substring(0, 2) + "/" + issueId.substring(2);\r
+       }\r
+\r
+       /**\r
+        * Creates an in-memory index of the issue change.\r
+        * \r
+        * @param repo\r
+        * @param headId\r
+        * @param files\r
+        * @param time\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
+\r
+               DirCache inCoreIndex = DirCache.newInCore();\r
+               DirCacheBuilder dcBuilder = inCoreIndex.builder();\r
+               ObjectInserter inserter = repo.newObjectInserter();\r
+\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
+                       }\r
+\r
+                       // Traverse HEAD to add all other paths\r
+                       TreeWalk treeWalk = new TreeWalk(repo);\r
+                       int hIdx = -1;\r
+                       if (headId != null)\r
+                               hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));\r
+                       treeWalk.setRecursive(true);\r
+\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 (!files.containsKey(path)) {\r
+                                       // add entries from HEAD for all other paths\r
+                                       if (hTree != null) {\r
+                                               // create a new DirCacheEntry with data retrieved from\r
+                                               // 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
+               } finally {\r
+                       inserter.release();\r
+               }\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
index a540c2aa481b44d4e343f0a2d380b1fbdf329849..5d6011a2c933f320875ac49a844a18bd4a73c3bc 100644 (file)
@@ -24,7 +24,6 @@ import java.nio.charset.Charset;
 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
@@ -748,25 +747,40 @@ public class JGitUtils {
        }\r
 \r
        /**\r
-        * Returns the list of files in the repository that match one of the\r
-        * specified extensions. This is a CASE-SENSITIVE search. If the repository\r
-        * does not exist or is empty, an empty list is returned.\r
+        * Returns the list of files in the repository on the default branch that\r
+        * match one of the specified extensions. This is a CASE-SENSITIVE search.\r
+        * If the repository does not exist or is empty, an empty list is returned.\r
         * \r
         * @param repository\r
         * @param extensions\r
         * @return list of files in repository with a matching extension\r
         */\r
        public static List<PathModel> getDocuments(Repository repository, List<String> extensions) {\r
+               return getDocuments(repository, extensions, null);\r
+       }\r
+\r
+       /**\r
+        * Returns the list of files in the repository in the specified commit that\r
+        * match one of the specified extensions. This is a CASE-SENSITIVE search.\r
+        * If the repository does not exist or is empty, an empty list is returned.\r
+        * \r
+        * @param repository\r
+        * @param extensions\r
+        * @param objectId\r
+        * @return list of files in repository with a matching extension\r
+        */\r
+       public static List<PathModel> getDocuments(Repository repository, List<String> extensions,\r
+                       String objectId) {\r
                List<PathModel> list = new ArrayList<PathModel>();\r
                if (!hasCommits(repository)) {\r
                        return list;\r
                }\r
-               RevCommit commit = getCommit(repository, null);\r
+               RevCommit commit = getCommit(repository, objectId);\r
                final TreeWalk tw = new TreeWalk(repository);\r
                try {\r
                        tw.addTree(commit.getTree());\r
                        if (extensions != null && extensions.size() > 0) {\r
-                               Collection<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();\r
+                               List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();\r
                                for (String extension : extensions) {\r
                                        if (extension.charAt(0) == '.') {\r
                                                suffixFilters.add(PathSuffixFilter.create("\\" + extension));\r
@@ -775,7 +789,12 @@ public class JGitUtils {
                                                suffixFilters.add(PathSuffixFilter.create("\\." + extension));\r
                                        }\r
                                }\r
-                               TreeFilter filter = OrTreeFilter.create(suffixFilters);\r
+                               TreeFilter filter;\r
+                               if (suffixFilters.size() == 1) {\r
+                                       filter = suffixFilters.get(0);\r
+                               } else {\r
+                                       filter = OrTreeFilter.create(suffixFilters);\r
+                               }\r
                                tw.setFilter(filter);\r
                                tw.setRecursive(true);\r
                        }\r
index da9c99d293639badfdd6b8622b5ec196602af66c..aea46bbbe760026ef170b61b2ebc72ff0cc9f292 100644 (file)
@@ -38,6 +38,8 @@ import com.gitblit.GitBlitException.UnauthorizedException;
 import com.gitblit.GitBlitException.UnknownRequestException;\r
 import com.gitblit.models.RepositoryModel;\r
 import com.gitblit.models.UserModel;\r
+import com.google.gson.ExclusionStrategy;\r
+import com.google.gson.FieldAttributes;\r
 import com.google.gson.Gson;\r
 import com.google.gson.GsonBuilder;\r
 import com.google.gson.JsonDeserializationContext;\r
@@ -108,7 +110,7 @@ public class JsonUtils {
                        UnauthorizedException {\r
                return retrieveJson(url, type, null, null);\r
        }\r
-       \r
+\r
        /**\r
         * Reads a gson object from the specified url.\r
         * \r
@@ -169,10 +171,11 @@ public class JsonUtils {
         */\r
        public static String retrieveJsonString(String url, String username, char[] password)\r
                        throws IOException {\r
-               try {                   \r
+               try {\r
                        URLConnection conn = ConnectionUtils.openReadConnection(url, username, password);\r
                        InputStream is = conn.getInputStream();\r
-                       BufferedReader reader = new BufferedReader(new InputStreamReader(is, ConnectionUtils.CHARSET));\r
+                       BufferedReader reader = new BufferedReader(new InputStreamReader(is,\r
+                                       ConnectionUtils.CHARSET));\r
                        StringBuilder json = new StringBuilder();\r
                        char[] buffer = new char[4096];\r
                        int len = 0;\r
@@ -260,10 +263,13 @@ public class JsonUtils {
 \r
        // build custom gson instance with GMT date serializer/deserializer\r
        // http://code.google.com/p/google-gson/issues/detail?id=281\r
-       private static Gson gson() {\r
+       public static Gson gson(ExclusionStrategy... strategies) {\r
                GsonBuilder builder = new GsonBuilder();\r
                builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());\r
                builder.setPrettyPrinting();\r
+               if (!ArrayUtils.isEmpty(strategies)) {\r
+                       builder.setExclusionStrategies(strategies);\r
+               }\r
                return builder.create();\r
        }\r
 \r
@@ -296,4 +302,24 @@ public class JsonUtils {
                        }\r
                }\r
        }\r
+\r
+       public static class ExcludeField implements ExclusionStrategy {\r
+\r
+               private Class<?> c;\r
+               private String fieldName;\r
+\r
+               public ExcludeField(String fqfn) throws SecurityException, NoSuchFieldException,\r
+                               ClassNotFoundException {\r
+                       this.c = Class.forName(fqfn.substring(0, fqfn.lastIndexOf(".")));\r
+                       this.fieldName = fqfn.substring(fqfn.lastIndexOf(".") + 1);\r
+               }\r
+\r
+               public boolean shouldSkipClass(Class<?> arg0) {\r
+                       return false;\r
+               }\r
+\r
+               public boolean shouldSkipField(FieldAttributes f) {\r
+                       return (f.getDeclaringClass() == c && f.getName().equals(fieldName));\r
+               }\r
+       }\r
 }\r
index 71947e14a71bd3df74f808caca9921411810aae5..747ce1f3073a8473fd50b8f5e2ea6866811b9fc9 100644 (file)
@@ -90,6 +90,10 @@ public class GitBlitSuite {
                return new FileRepository(new File(REPOSITORIES, "test/theoretical-physics.git"));\r
        }\r
 \r
+       public static Repository getIssuesTestRepository() throws Exception {\r
+               return new FileRepository(new File(REPOSITORIES, "gb-issues.git"));\r
+       }\r
+\r
        public static boolean startGitblit() throws Exception {\r
                if (started.get()) {\r
                        // already started\r
@@ -134,6 +138,8 @@ public class GitBlitSuite {
                        cloneOrFetch("test/ambition.git", "https://github.com/defunkt/ambition.git");\r
                        cloneOrFetch("test/theoretical-physics.git", "https://github.com/certik/theoretical-physics.git");\r
                        \r
+                       JGitUtils.createRepository(REPOSITORIES, "gb-issues.git").close();\r
+\r
                        enableTickets("ticgit.git");\r
                        enableDocs("ticgit.git");\r
                        showRemoteBranches("ticgit.git");\r
diff --git a/tests/com/gitblit/tests/IssuesTest.java b/tests/com/gitblit/tests/IssuesTest.java
new file mode 100644 (file)
index 0000000..1522ec6
--- /dev/null
@@ -0,0 +1,115 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.tests;\r
+\r
+import static org.junit.Assert.assertEquals;\r
+import static org.junit.Assert.assertNotNull;\r
+import static org.junit.Assert.assertTrue;\r
+\r
+import java.util.List;\r
+\r
+import org.bouncycastle.util.Arrays;\r
+import org.eclipse.jgit.lib.Repository;\r
+import org.junit.Test;\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.IssueModel.Priority;\r
+import com.gitblit.utils.IssueUtils;\r
+import com.gitblit.utils.IssueUtils.IssueFilter;\r
+\r
+public class IssuesTest {\r
+\r
+       @Test\r
+       public void testInsertion() throws Exception {\r
+               Repository repository = GitBlitSuite.getIssuesTestRepository();\r
+               // create and insert the issue\r
+               Change c1 = newChange("Test issue " + Long.toHexString(System.currentTimeMillis()));\r
+               IssueModel issue = IssueUtils.createIssue(repository, c1);\r
+               assertNotNull(issue.id);\r
+\r
+               // retrieve issue and compare\r
+               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
+               assertTrue(IssueUtils.updateIssue(repository, issue.id, c2));\r
+\r
+               // retrieve issue again\r
+               constructed = IssueUtils.getIssue(repository, issue.id);\r
+\r
+               assertEquals(2, constructed.changes.size());\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
+                       @Override\r
+                       public boolean accept(IssueModel issue) {\r
+                               if (!hasFirst) {\r
+                                       hasFirst = true;\r
+                                       return true;\r
+                               }\r
+                               return false;\r
+                       }\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.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 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
+\r
+               assertTrue(issue.hasLabel("helpdesk"));\r
+       }\r
+}
\ No newline at end of file