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
*/\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
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
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
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
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
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
}\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
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
\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
\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
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
}\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
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
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
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
*/\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
}\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
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
}\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
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
}\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
}\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
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
}\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
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
* \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
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
}\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
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
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
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