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