From 0f43a54527845b5873f35dc80300d578bfe84bb0 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 13 Jan 2012 07:58:12 -0500 Subject: [PATCH] Branch for implementing distributed gb-issues --- src/com/gitblit/models/IssueModel.java | 310 +++++++++++++++ src/com/gitblit/utils/IssueUtils.java | 455 ++++++++++++++++++++++ src/com/gitblit/utils/JGitUtils.java | 33 +- src/com/gitblit/utils/JsonUtils.java | 34 +- tests/com/gitblit/tests/GitBlitSuite.java | 6 + tests/com/gitblit/tests/IssuesTest.java | 115 ++++++ 6 files changed, 942 insertions(+), 11 deletions(-) create mode 100644 src/com/gitblit/models/IssueModel.java create mode 100644 src/com/gitblit/utils/IssueUtils.java create mode 100644 tests/com/gitblit/tests/IssuesTest.java diff --git a/src/com/gitblit/models/IssueModel.java b/src/com/gitblit/models/IssueModel.java new file mode 100644 index 00000000..3c6d9a0b --- /dev/null +++ b/src/com/gitblit/models/IssueModel.java @@ -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 { + + 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 changes; + + public IssueModel() { + created = new Date(); + + type = Type.Defect; + status = Status.New; + priority = Priority.Medium; + + changes = new ArrayList(); + } + + public String getStatus() { + String s = status.toString(); + if (!StringUtils.isEmpty(owner)) + s += " (" + owner + ")"; + return s; + } + + public List getLabels() { + List list = new ArrayList(); + 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(); + } + 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 fieldChanges; + + public List 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(); + } + 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(); + } + 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; + } + } +} diff --git a/src/com/gitblit/utils/IssueUtils.java b/src/com/gitblit/utils/IssueUtils.java new file mode 100644 index 00000000..82170703 --- /dev/null +++ b/src/com/gitblit/utils/IssueUtils.java @@ -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 getIssues(Repository repository, IssueFilter filter) { + List list = new ArrayList(); + RefModel issuesBranch = getIssuesBranch(repository); + if (issuesBranch == null) { + return list; + } + List 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 files = new HashMap(); + 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 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); + } +} diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java index a540c2aa..5d6011a2 100644 --- a/src/com/gitblit/utils/JGitUtils.java +++ b/src/com/gitblit/utils/JGitUtils.java @@ -24,7 +24,6 @@ import java.nio.charset.Charset; 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 @@ public class JGitUtils { } /** - * 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 getDocuments(Repository repository, List 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 getDocuments(Repository repository, List extensions, + String objectId) { List list = new ArrayList(); 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 suffixFilters = new ArrayList(); + List suffixFilters = new ArrayList(); for (String extension : extensions) { if (extension.charAt(0) == '.') { suffixFilters.add(PathSuffixFilter.create("\\" + extension)); @@ -775,7 +789,12 @@ public class JGitUtils { 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); } diff --git a/src/com/gitblit/utils/JsonUtils.java b/src/com/gitblit/utils/JsonUtils.java index da9c99d2..aea46bbb 100644 --- a/src/com/gitblit/utils/JsonUtils.java +++ b/src/com/gitblit/utils/JsonUtils.java @@ -38,6 +38,8 @@ import com.gitblit.GitBlitException.UnauthorizedException; 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 @@ public class JsonUtils { UnauthorizedException { return retrieveJson(url, type, null, null); } - + /** * Reads a gson object from the specified url. * @@ -169,10 +171,11 @@ public class JsonUtils { */ 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 @@ public class JsonUtils { // 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 class JsonUtils { } } } + + 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)); + } + } } diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java index 71947e14..747ce1f3 100644 --- a/tests/com/gitblit/tests/GitBlitSuite.java +++ b/tests/com/gitblit/tests/GitBlitSuite.java @@ -90,6 +90,10 @@ public class GitBlitSuite { 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 @@ public class GitBlitSuite { 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"); diff --git a/tests/com/gitblit/tests/IssuesTest.java b/tests/com/gitblit/tests/IssuesTest.java new file mode 100644 index 00000000..1522ec69 --- /dev/null +++ b/tests/com/gitblit/tests/IssuesTest.java @@ -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 list = IssueUtils.getIssues(repository, null); + List 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")); + } +} \ No newline at end of file -- 2.39.5