--- /dev/null
+/*\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
--- /dev/null
+/*\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
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
}\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
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
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
UnauthorizedException {\r
return retrieveJson(url, type, null, null);\r
}\r
- \r
+\r
/**\r
* Reads a gson object from the specified url.\r
* \r
*/\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
\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
}\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
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
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
--- /dev/null
+/*\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