import org.apache.lucene.search.IndexSearcher;\r
import org.apache.lucene.search.Query;\r
import org.apache.lucene.search.ScoreDoc;\r
-import org.apache.lucene.search.TermQuery;\r
import org.apache.lucene.search.TopScoreDocCollector;\r
import org.apache.lucene.search.highlight.Fragmenter;\r
import org.apache.lucene.search.highlight.Highlighter;\r
import org.slf4j.LoggerFactory;\r
\r
import com.gitblit.Constants.SearchObjectType;\r
-import com.gitblit.models.IssueModel;\r
-import com.gitblit.models.IssueModel.Attachment;\r
import com.gitblit.models.PathModel.PathChangeModel;\r
import com.gitblit.models.RefModel;\r
import com.gitblit.models.RepositoryModel;\r
import com.gitblit.models.SearchResult;\r
import com.gitblit.utils.ArrayUtils;\r
-import com.gitblit.utils.IssueUtils;\r
import com.gitblit.utils.JGitUtils;\r
import com.gitblit.utils.StringUtils;\r
\r
private static final int INDEX_VERSION = 5;\r
\r
private static final String FIELD_OBJECT_TYPE = "type";\r
- private static final String FIELD_ISSUE = "issue";\r
private static final String FIELD_PATH = "path";\r
private static final String FIELD_COMMIT = "commit";\r
private static final String FIELD_BRANCH = "branch";\r
private static final String FIELD_DATE = "date";\r
private static final String FIELD_TAG = "tag";\r
private static final String FIELD_LABEL = "label";\r
- private static final String FIELD_ATTACHMENT = "attachment";\r
\r
private static final String CONF_FILE = "lucene.conf";\r
private static final String LUCENE_DIR = "lucene";\r
&& branch.equals(defaultBranch)) {\r
// indexing "default" branch\r
indexBranch = true;\r
- } else if (IssueUtils.GB_ISSUES.equals(branch)) {\r
- // skip the GB_ISSUES branch because it is indexed later\r
- // note: this is different than updateIndex\r
+ } else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) {\r
+ // skip Gitblit internal branches\r
indexBranch = false;\r
} else {\r
// normal explicit branch check\r
// finished\r
reader.release();\r
\r
- // this repository has a gb-issues branch, index all issues\r
- if (IssueUtils.getIssuesBranch(repository) != null) {\r
- List<IssueModel> issues = IssueUtils.getIssues(repository, null);\r
- if (issues.size() > 0) {\r
- result.branchCount += 1;\r
- }\r
- for (IssueModel issue : issues) {\r
- result.issueCount++;\r
- Document doc = createDocument(issue);\r
- writer.addDocument(doc);\r
- }\r
- }\r
-\r
// commit all changes and reset the searcher\r
config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);\r
config.save();\r
return result;\r
}\r
\r
- /**\r
- * Incrementally update the index with the specified issue for the\r
- * repository.\r
- * \r
- * @param repositoryName\r
- * @param issue\r
- * @return true, if successful\r
- */\r
- public boolean index(String repositoryName, IssueModel issue) {\r
- try {\r
- // delete the old issue from the index, if exists\r
- deleteIssue(repositoryName, issue.id);\r
- Document doc = createDocument(issue);\r
- return index(repositoryName, doc);\r
- } catch (Exception e) {\r
- logger.error(MessageFormat.format("Error while indexing issue {0} in {1}", issue.id, repositoryName), e);\r
- }\r
- return false;\r
- }\r
- \r
- /**\r
- * Delete an issue from the repository index.\r
- * \r
- * @param repositoryName\r
- * @param issueId\r
- * @throws Exception\r
- * @return true, if deleted, false if no record was deleted\r
- */\r
- private boolean deleteIssue(String repositoryName, String issueId) throws Exception {\r
- BooleanQuery query = new BooleanQuery();\r
- Term objectTerm = new Term(FIELD_OBJECT_TYPE, SearchObjectType.issue.name());\r
- query.add(new TermQuery(objectTerm), Occur.MUST);\r
- Term issueidTerm = new Term(FIELD_ISSUE, issueId);\r
- query.add(new TermQuery(issueidTerm), Occur.MUST);\r
- \r
- IndexWriter writer = getIndexWriter(repositoryName);\r
- int numDocsBefore = writer.numDocs();\r
- writer.deleteDocuments(query);\r
- writer.commit();\r
- int numDocsAfter = writer.numDocs();\r
- if (numDocsBefore == numDocsAfter) {\r
- logger.debug(MessageFormat.format("no records found to delete {0}", query.toString()));\r
- return false;\r
- } else {\r
- logger.debug(MessageFormat.format("deleted {0} records with {1}", numDocsBefore - numDocsAfter, query.toString()));\r
- return true;\r
- }\r
- }\r
- \r
/**\r
* Delete a blob from the specified branch of the repository index.\r
* \r
&& branch.equals(defaultBranch)) {\r
// indexing "default" branch\r
indexBranch = true;\r
- } else if (IssueUtils.GB_ISSUES.equals(branch)) {\r
- // update issues modified on the GB_ISSUES branch\r
- // note: this is different than reindex\r
- indexBranch = true;\r
+ } else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) {\r
+ // ignore internal Gitblit branches\r
+ indexBranch = false;\r
} else {\r
// normal explicit branch check\r
indexBranch = model.indexedBranches.contains(branch.getName());\r
result.branchCount += 1;\r
}\r
\r
- // track the issue ids that we have already indexed\r
- Set<String> indexedIssues = new TreeSet<String>();\r
- \r
// reverse the list of commits so we start with the first commit \r
Collections.reverse(revs);\r
for (RevCommit commit : revs) { \r
- if (IssueUtils.GB_ISSUES.equals(branch)) {\r
- // only index an issue once during updateIndex\r
- String issueId = commit.getShortMessage().substring(2).trim();\r
- if (indexedIssues.contains(issueId)) {\r
- continue;\r
- }\r
- indexedIssues.add(issueId);\r
- \r
- IssueModel issue = IssueUtils.getIssue(repository, issueId);\r
- if (issue == null) {\r
- // issue was deleted, remove from index\r
- if (!deleteIssue(model.name, issueId)) {\r
- logger.error(MessageFormat.format("Failed to delete issue {0} from Lucene index!", issueId));\r
- }\r
- } else {\r
- // issue was updated\r
- index(model.name, issue);\r
- result.issueCount++;\r
- }\r
- } else {\r
- // index a commit\r
- result.add(index(model.name, repository, branchName, commit));\r
- }\r
+ // index a commit\r
+ result.add(index(model.name, repository, branchName, commit));\r
}\r
\r
// update the config\r
return result;\r
}\r
\r
- /**\r
- * Creates a Lucene document from an issue.\r
- * \r
- * @param issue\r
- * @return a Lucene document\r
- */\r
- private Document createDocument(IssueModel issue) {\r
- Document doc = new Document();\r
- doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.issue.name(), Store.YES,\r
- Field.Index.NOT_ANALYZED));\r
- doc.add(new Field(FIELD_ISSUE, issue.id, Store.YES, Index.ANALYZED));\r
- doc.add(new Field(FIELD_BRANCH, IssueUtils.GB_ISSUES, Store.YES, Index.ANALYZED));\r
- doc.add(new Field(FIELD_DATE, DateTools.dateToString(issue.created, Resolution.MINUTE),\r
- Store.YES, Field.Index.NO));\r
- doc.add(new Field(FIELD_AUTHOR, issue.reporter, Store.YES, Index.ANALYZED));\r
- List<String> attachments = new ArrayList<String>();\r
- for (Attachment attachment : issue.getAttachments()) {\r
- attachments.add(attachment.name.toLowerCase());\r
- }\r
- doc.add(new Field(FIELD_ATTACHMENT, StringUtils.flattenStrings(attachments), Store.YES,\r
- Index.ANALYZED));\r
- doc.add(new Field(FIELD_SUMMARY, issue.summary, Store.YES, Index.ANALYZED));\r
- doc.add(new Field(FIELD_CONTENT, issue.toString(), Store.YES, Index.ANALYZED));\r
- doc.add(new Field(FIELD_LABEL, StringUtils.flattenStrings(issue.getLabels()), Store.YES,\r
- Index.ANALYZED));\r
- return doc;\r
- }\r
-\r
/**\r
* Creates a Lucene document for a commit\r
* \r
result.type = SearchObjectType.fromName(doc.get(FIELD_OBJECT_TYPE));\r
result.branch = doc.get(FIELD_BRANCH);\r
result.commitId = doc.get(FIELD_COMMIT);\r
- result.issueId = doc.get(FIELD_ISSUE);\r
result.path = doc.get(FIELD_PATH);\r
if (doc.get(FIELD_TAG) != null) {\r
result.tags = StringUtils.getStringsFromValue(doc.get(FIELD_TAG));\r
+++ /dev/null
-/*\r
- * Copyright 2012 gitblit.com.\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-package com.gitblit.models;\r
-\r
-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
- * @author James Moger\r
- * \r
- */\r
-public class IssueModel implements Serializable, Comparable<IssueModel> {\r
-\r
- private static final long serialVersionUID = 1L;\r
-\r
- public String id;\r
-\r
- public Type type;\r
-\r
- public Status status;\r
-\r
- public Priority priority;\r
-\r
- public Date created;\r
-\r
- public String summary;\r
-\r
- public String description;\r
-\r
- public String reporter;\r
-\r
- public String owner;\r
-\r
- public String milestone;\r
-\r
- public List<Change> changes;\r
-\r
- public IssueModel() {\r
- // the first applied change set the date appropriately\r
- created = new Date(0);\r
-\r
- type = Type.Defect;\r
- status = Status.New;\r
- priority = Priority.Medium;\r
-\r
- changes = new ArrayList<Change>();\r
- }\r
-\r
- public String getStatus() {\r
- String s = status.toString();\r
- if (!StringUtils.isEmpty(owner))\r
- s += " (" + owner + ")";\r
- return s;\r
- }\r
-\r
- public 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.hasField(Field.Labels)) {\r
- labels = change.getString(Field.Labels);\r
- }\r
- }\r
- if (!StringUtils.isEmpty(labels)) {\r
- list.addAll(StringUtils.getStringsFromValue(labels, " "));\r
- }\r
- return list;\r
- }\r
-\r
- public Attachment getAttachment(String name) {\r
- Attachment attachment = null;\r
- for (Change change : changes) {\r
- if (change.hasAttachments()) {\r
- Attachment a = change.getAttachment(name);\r
- if (a != null) {\r
- attachment = a;\r
- }\r
- }\r
- }\r
- return attachment;\r
- }\r
-\r
- public List<Attachment> getAttachments() {\r
- List<Attachment> list = new ArrayList<Attachment>();\r
- for (Change change : changes) {\r
- if (change.hasAttachments()) {\r
- list.addAll(change.attachments);\r
- }\r
- }\r
- return list;\r
- }\r
-\r
- public void applyChange(Change change) {\r
- if (changes.size() == 0) {\r
- // first change created the issue\r
- created = change.created;\r
- }\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
- 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
- public int compareTo(IssueModel o) {\r
- return o.created.compareTo(created);\r
- }\r
-\r
- @Override\r
- public boolean equals(Object o) {\r
- if (o instanceof IssueModel)\r
- return id.equals(((IssueModel) o).id);\r
- return super.equals(o);\r
- }\r
-\r
- @Override\r
- public int hashCode() {\r
- return id.hashCode();\r
- }\r
-\r
- public static class Change implements Serializable, Comparable<Change> {\r
-\r
- private static final long serialVersionUID = 1L;\r
-\r
- public final Date created;\r
-\r
- public final String author;\r
-\r
- public String id;\r
-\r
- public char code;\r
-\r
- public Comment comment;\r
-\r
- public Set<FieldChange> fieldChanges;\r
-\r
- public Set<Attachment> attachments;\r
-\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 && !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 Object getField(Field field) {\r
- if (fieldChanges != null) {\r
- for (FieldChange fieldChange : fieldChanges) {\r
- if (fieldChange.field == field) {\r
- return fieldChange.value;\r
- }\r
- }\r
- }\r
- return null;\r
- }\r
-\r
- public void setField(Field field, Object value) {\r
- FieldChange fieldChange = new FieldChange(field, value);\r
- if (fieldChanges == null) {\r
- fieldChanges = new LinkedHashSet<FieldChange>();\r
- }\r
- fieldChanges.add(fieldChange);\r
- }\r
-\r
- public String getString(Field field) {\r
- Object value = getField(field);\r
- if (value == null) {\r
- return null;\r
- }\r
- return value.toString();\r
- }\r
-\r
- @Override\r
- public int compareTo(Change c) {\r
- return created.compareTo(c.created);\r
- }\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 false;\r
- }\r
-\r
- @Override\r
- public String toString() {\r
- StringBuilder sb = new StringBuilder(); \r
- sb.append(new 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
- public static class Comment implements Serializable {\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
- this.text = text;\r
- }\r
-\r
- @Override\r
- public String toString() {\r
- return text;\r
- }\r
- }\r
-\r
- public static class FieldChange implements Serializable {\r
-\r
- private static final long serialVersionUID = 1L;\r
-\r
- public final Field field;\r
-\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
- return field + ": " + value;\r
- }\r
- }\r
-\r
- public static class Attachment implements Serializable {\r
-\r
- private static final long serialVersionUID = 1L;\r
-\r
- public final String name;\r
- public String id;\r
- public long size;\r
- public byte[] content;\r
- public boolean deleted;\r
-\r
- public Attachment(String name) {\r
- this.name = name;\r
- }\r
-\r
- @Override\r
- public 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
-\r
- public static enum Field {\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
-\r
- public boolean exceeds(Status status) {\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
- return Started;\r
- case Accepted:\r
- return Started;\r
- case Started:\r
- return Testing;\r
- case Review:\r
- return Testing;\r
- case Queued:\r
- return Testing;\r
- case Testing:\r
- return Done;\r
- }\r
- return Accepted;\r
- }\r
- }\r
-}\r
+++ /dev/null
-/*\r
- * Copyright 2012 gitblit.com.\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-package com.gitblit.utils;\r
-\r
-import java.io.IOException;\r
-import java.text.MessageFormat;\r
-import java.util.ArrayList;\r
-import java.util.Collection;\r
-import java.util.Collections;\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.api.errors.ConcurrentRefUpdateException;\r
-import org.eclipse.jgit.api.errors.JGitInternalException;\r
-import org.eclipse.jgit.dircache.DirCache;\r
-import org.eclipse.jgit.dircache.DirCacheBuilder;\r
-import org.eclipse.jgit.dircache.DirCacheEntry;\r
-import org.eclipse.jgit.internal.JGitText;\r
-import org.eclipse.jgit.lib.CommitBuilder;\r
-import org.eclipse.jgit.lib.Constants;\r
-import org.eclipse.jgit.lib.FileMode;\r
-import org.eclipse.jgit.lib.ObjectId;\r
-import org.eclipse.jgit.lib.ObjectInserter;\r
-import org.eclipse.jgit.lib.PersonIdent;\r
-import org.eclipse.jgit.lib.RefUpdate;\r
-import org.eclipse.jgit.lib.RefUpdate.Result;\r
-import org.eclipse.jgit.lib.Repository;\r
-import org.eclipse.jgit.revwalk.RevCommit;\r
-import org.eclipse.jgit.revwalk.RevTree;\r
-import org.eclipse.jgit.revwalk.RevWalk;\r
-import org.eclipse.jgit.treewalk.CanonicalTreeParser;\r
-import org.eclipse.jgit.treewalk.TreeWalk;\r
-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.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
- * @author James Moger\r
- * \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/gitblit/issues";\r
-\r
- static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.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
- * @param repository\r
- * @return a refmodel for the gb-issues branch or null\r
- */\r
- public static RefModel getIssuesBranch(Repository repository) {\r
- List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);\r
- for (RefModel ref : refs) {\r
- if (ref.reference.getName().equals(GB_ISSUES)) {\r
- return ref;\r
- }\r
- }\r
- return null;\r
- }\r
-\r
- /**\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
- * optional issue filter to only return matching results\r
- * @return a list of issues\r
- */\r
- public static List<IssueModel> getIssues(Repository repository, IssueFilter filter) {\r
- List<IssueModel> list = new ArrayList<IssueModel>();\r
- RefModel issuesBranch = getIssuesBranch(repository);\r
- if (issuesBranch == null) {\r
- return list;\r
- }\r
-\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
- if (filter.accept(issue)) {\r
- list.add(issue);\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 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
- }\r
-\r
- if (StringUtils.isEmpty(issueId)) {\r
- return null;\r
- }\r
-\r
- String issuePath = getIssuePath(issueId);\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
- * Retrieves the specified attachment from an issue.\r
- * \r
- * @param repository\r
- * @param issueId\r
- * @param filename\r
- * @return an attachment, if found, null otherwise\r
- */\r
- public static Attachment getIssueAttachment(Repository repository, String issueId,\r
- String filename) {\r
- RefModel issuesBranch = getIssuesBranch(repository);\r
- if (issuesBranch == null) {\r
- return null;\r
- }\r
-\r
- if (StringUtils.isEmpty(issueId)) {\r
- return null;\r
- }\r
-\r
- // deserialize the issue model so that we have the attachment metadata\r
- IssueModel issue = getIssue(repository, issueId, true);\r
- Attachment attachment = issue.getAttachment(filename);\r
-\r
- // attachment not found\r
- if (attachment == null) {\r
- return null;\r
- }\r
-\r
- // retrieve the attachment content\r
- String issuePath = getIssuePath(issueId);\r
- RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();\r
- byte[] content = JGitUtils\r
- .getByteContent(repository, tree, issuePath + "/" + attachment.id, false);\r
- attachment.content = content;\r
- attachment.size = content.length;\r
- return attachment;\r
- }\r
-\r
- /**\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
- * @return true if successful\r
- */\r
- public static IssueModel createIssue(Repository repository, Change change) {\r
- RefModel issuesBranch = getIssuesBranch(repository);\r
- if (issuesBranch == null) {\r
- JGitUtils.createOrphanBranch(repository, GB_ISSUES, null);\r
- }\r
-\r
- if (StringUtils.isEmpty(change.author)) {\r
- throw new RuntimeException("Must specify a change author!");\r
- }\r
- if (!change.hasField(Field.Summary)) {\r
- throw new RuntimeException("Must specify a summary!");\r
- }\r
- if (!change.hasField(Field.Description)) {\r
- throw new RuntimeException("Must specify a description!");\r
- }\r
-\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
- boolean success = commit(repository, issueId, change);\r
- if (success) {\r
- return getIssue(repository, issueId, false);\r
- }\r
- return null;\r
- }\r
-\r
- /**\r
- * Updates an issue in the gb-issues branch of the repository.\r
- * \r
- * @param repository\r
- * @param issueId\r
- * @param change\r
- * @return true if successful\r
- */\r
- public static boolean updateIssue(Repository repository, String issueId, Change change) {\r
- boolean success = false;\r
- RefModel issuesBranch = getIssuesBranch(repository);\r
-\r
- if (issuesBranch == null) {\r
- throw new RuntimeException("gb-issues branch does not exist!");\r
- }\r
-\r
- if (change == null) {\r
- throw new RuntimeException("change can not be null!");\r
- }\r
-\r
- if (StringUtils.isEmpty(change.author)) {\r
- throw new RuntimeException("must specify a change author!");\r
- }\r
-\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
- /**\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 + " 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
- * 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
- 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
- /**\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
- /**\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 issueId\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
- 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
- // 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, issuePath, change);\r
- ObjectId indexTreeId = index.writeTree(odi);\r
-\r
- // Create a commit object\r
- PersonIdent ident = new PersonIdent(change.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 commit issue {1} to {0}", issueId);\r
- }\r
- return success;\r
- }\r
-\r
- /**\r
- * Returns the issue path. This follows the same scheme as Git's object\r
- * store path where the first two characters of the hash id are the root\r
- * folder with the remaining characters as a subfolder within that folder.\r
- * \r
- * @param issueId\r
- * @return the root path of the issue content on the gb-issues branch\r
- */\r
- static String getIssuePath(String issueId) {\r
- return issueId.substring(0, 2) + "/" + issueId.substring(2);\r
- }\r
-\r
- /**\r
- * Creates an in-memory index of the issue change.\r
- * \r
- * @param repo\r
- * @param headId\r
- * @param change\r
- * @return an in-memory index\r
- * @throws IOException\r
- */\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 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
- TreeWalk treeWalk = new TreeWalk(repo);\r
- int hIdx = -1;\r
- if (headId != null)\r
- hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));\r
- treeWalk.setRecursive(true);\r
-\r
- while (treeWalk.next()) {\r
- String path = treeWalk.getPathString();\r
- CanonicalTreeParser hTree = null;\r
- if (hIdx != -1)\r
- hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);\r
- if (!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
- // HEAD\r
- final DirCacheEntry dcEntry = new DirCacheEntry(path);\r
- dcEntry.setObjectId(hTree.getEntryObjectId());\r
- dcEntry.setFileMode(hTree.getEntryFileMode());\r
-\r
- // add to temporary in-core index\r
- dcBuilder.add(dcEntry);\r
- }\r
- }\r
- }\r
-\r
- // release the treewalk\r
- treeWalk.release();\r
-\r
- // finish temporary in-core index used for this commit\r
- dcBuilder.finish();\r
- } finally {\r
- inserter.release();\r
- }\r
- return inCoreIndex;\r
- }\r
-}
\ No newline at end of file
MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class,\r
DiffUtilsTest.class, MetricUtilsTest.class, X509UtilsTest.class,\r
GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class,\r
- GroovyScriptTest.class, LuceneExecutorTest.class, IssuesTest.class, RepositoryModelTest.class,\r
+ GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class,\r
FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdUserServiceTest.class,\r
ModelUtilsTest.class, JnaUtilsTest.class })\r
public class GitBlitSuite {\r
+++ /dev/null
-/*\r
- * Copyright 2012 gitblit.com.\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-package com.gitblit.tests;\r
-\r
-import static org.junit.Assert.assertEquals;\r
-import static org.junit.Assert.assertFalse;\r
-import static org.junit.Assert.assertNotNull;\r
-import static org.junit.Assert.assertTrue;\r
-\r
-import java.util.List;\r
-\r
-import org.bouncycastle.util.Arrays;\r
-import org.eclipse.jgit.lib.Repository;\r
-import org.junit.Test;\r
-\r
-import com.gitblit.LuceneExecutor;\r
-import com.gitblit.models.IssueModel;\r
-import com.gitblit.models.IssueModel.Attachment;\r
-import com.gitblit.models.IssueModel.Change;\r
-import com.gitblit.models.IssueModel.Field;\r
-import com.gitblit.models.IssueModel.Priority;\r
-import com.gitblit.models.IssueModel.Status;\r
-import com.gitblit.models.SearchResult;\r
-import com.gitblit.utils.FileUtils;\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 testLifecycle() throws Exception {\r
- Repository repository = GitBlitSuite.getIssuesTestRepository();\r
- String name = FileUtils.getRelativePath(GitBlitSuite.REPOSITORIES, repository.getDirectory());\r
- \r
- // create and insert an issue\r
- Change c1 = newChange("testCreation() " + Long.toHexString(System.currentTimeMillis()));\r
- IssueModel issue = IssueUtils.createIssue(repository, c1);\r
- assertNotNull(issue.id);\r
-\r
- // retrieve issue and compare\r
- IssueModel constructed = IssueUtils.getIssue(repository, issue.id);\r
- compare(issue, constructed);\r
-\r
- assertEquals(1, constructed.changes.size());\r
- \r
- // C1: create the issue\r
- c1 = newChange("testUpdates() " + Long.toHexString(System.currentTimeMillis()));\r
- issue = IssueUtils.createIssue(repository, c1);\r
- assertNotNull(issue.id);\r
-\r
- constructed = IssueUtils.getIssue(repository, issue.id);\r
- compare(issue, constructed);\r
- assertEquals(1, constructed.changes.size());\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(5, constructed.changes.size());\r
- assertTrue(constructed.status.isClosed());\r
-\r
- List<IssueModel> allIssues = IssueUtils.getIssues(repository, null);\r
- List<IssueModel> openIssues = IssueUtils.getIssues(repository, new IssueFilter() {\r
- @Override\r
- public boolean accept(IssueModel issue) {\r
- return !issue.status.isClosed();\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
- assertTrue(allIssues.size() > 0);\r
- assertEquals(1, openIssues.size());\r
- assertEquals(1, closedIssues.size());\r
- \r
- // build a new Lucene index\r
- LuceneExecutor lucene = new LuceneExecutor(null, GitBlitSuite.REPOSITORIES);\r
- lucene.deleteIndex(name);\r
- for (IssueModel anIssue : allIssues) {\r
- lucene.index(name, anIssue);\r
- }\r
- List<SearchResult> hits = lucene.search("working", 1, 10, name);\r
- assertTrue(hits.size() == 1);\r
- \r
- // reindex an issue\r
- issue = allIssues.get(0);\r
- Change change = new Change("reindex");\r
- change.comment("this is a test of reindexing an issue");\r
- IssueUtils.updateIssue(repository, issue.id, change);\r
- issue = IssueUtils.getIssue(repository, issue.id);\r
- lucene.index(name, issue);\r
-\r
- hits = lucene.search("working", 1, 10, name);\r
- assertTrue(hits.size() == 1);\r
-\r
- \r
- // delete all issues\r
- for (IssueModel anIssue : allIssues) {\r
- assertTrue(IssueUtils.deleteIssue(repository, anIssue.id, "D"));\r
- }\r
- \r
- lucene.close();\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
- }\r
-\r
- private Change newChange(String summary) {\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
- 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.summary, constructed.summary);\r
- assertEquals(issue.description, constructed.description);\r
- assertEquals(issue.created, constructed.created);\r
-\r
- assertTrue(issue.hasLabel("helpdesk"));\r
- }\r
-}
\ No newline at end of file