@@ -56,7 +56,6 @@ import org.apache.lucene.search.BooleanQuery; | |||
import org.apache.lucene.search.IndexSearcher; | |||
import org.apache.lucene.search.Query; | |||
import org.apache.lucene.search.ScoreDoc; | |||
import org.apache.lucene.search.TermQuery; | |||
import org.apache.lucene.search.TopScoreDocCollector; | |||
import org.apache.lucene.search.highlight.Fragmenter; | |||
import org.apache.lucene.search.highlight.Highlighter; | |||
@@ -86,14 +85,11 @@ import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import com.gitblit.Constants.SearchObjectType; | |||
import com.gitblit.models.IssueModel; | |||
import com.gitblit.models.IssueModel.Attachment; | |||
import com.gitblit.models.PathModel.PathChangeModel; | |||
import com.gitblit.models.RefModel; | |||
import com.gitblit.models.RepositoryModel; | |||
import com.gitblit.models.SearchResult; | |||
import com.gitblit.utils.ArrayUtils; | |||
import com.gitblit.utils.IssueUtils; | |||
import com.gitblit.utils.JGitUtils; | |||
import com.gitblit.utils.StringUtils; | |||
@@ -109,7 +105,6 @@ public class LuceneExecutor implements Runnable { | |||
private static final int INDEX_VERSION = 5; | |||
private static final String FIELD_OBJECT_TYPE = "type"; | |||
private static final String FIELD_ISSUE = "issue"; | |||
private static final String FIELD_PATH = "path"; | |||
private static final String FIELD_COMMIT = "commit"; | |||
private static final String FIELD_BRANCH = "branch"; | |||
@@ -120,7 +115,6 @@ public class LuceneExecutor implements Runnable { | |||
private static final String FIELD_DATE = "date"; | |||
private static final String FIELD_TAG = "tag"; | |||
private static final String FIELD_LABEL = "label"; | |||
private static final String FIELD_ATTACHMENT = "attachment"; | |||
private static final String CONF_FILE = "lucene.conf"; | |||
private static final String LUCENE_DIR = "lucene"; | |||
@@ -475,9 +469,8 @@ public class LuceneExecutor implements Runnable { | |||
&& branch.equals(defaultBranch)) { | |||
// indexing "default" branch | |||
indexBranch = true; | |||
} else if (IssueUtils.GB_ISSUES.equals(branch)) { | |||
// skip the GB_ISSUES branch because it is indexed later | |||
// note: this is different than updateIndex | |||
} else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) { | |||
// skip Gitblit internal branches | |||
indexBranch = false; | |||
} else { | |||
// normal explicit branch check | |||
@@ -617,19 +610,6 @@ public class LuceneExecutor implements Runnable { | |||
// finished | |||
reader.release(); | |||
// this repository has a gb-issues branch, index all issues | |||
if (IssueUtils.getIssuesBranch(repository) != null) { | |||
List<IssueModel> issues = IssueUtils.getIssues(repository, null); | |||
if (issues.size() > 0) { | |||
result.branchCount += 1; | |||
} | |||
for (IssueModel issue : issues) { | |||
result.issueCount++; | |||
Document doc = createDocument(issue); | |||
writer.addDocument(doc); | |||
} | |||
} | |||
// commit all changes and reset the searcher | |||
config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION); | |||
config.save(); | |||
@@ -722,55 +702,6 @@ public class LuceneExecutor implements Runnable { | |||
return result; | |||
} | |||
/** | |||
* Incrementally update the index with the specified issue for the | |||
* repository. | |||
* | |||
* @param repositoryName | |||
* @param issue | |||
* @return true, if successful | |||
*/ | |||
public boolean index(String repositoryName, IssueModel issue) { | |||
try { | |||
// delete the old issue from the index, if exists | |||
deleteIssue(repositoryName, issue.id); | |||
Document doc = createDocument(issue); | |||
return index(repositoryName, doc); | |||
} catch (Exception e) { | |||
logger.error(MessageFormat.format("Error while indexing issue {0} in {1}", issue.id, repositoryName), e); | |||
} | |||
return false; | |||
} | |||
/** | |||
* Delete an issue from the repository index. | |||
* | |||
* @param repositoryName | |||
* @param issueId | |||
* @throws Exception | |||
* @return true, if deleted, false if no record was deleted | |||
*/ | |||
private boolean deleteIssue(String repositoryName, String issueId) throws Exception { | |||
BooleanQuery query = new BooleanQuery(); | |||
Term objectTerm = new Term(FIELD_OBJECT_TYPE, SearchObjectType.issue.name()); | |||
query.add(new TermQuery(objectTerm), Occur.MUST); | |||
Term issueidTerm = new Term(FIELD_ISSUE, issueId); | |||
query.add(new TermQuery(issueidTerm), Occur.MUST); | |||
IndexWriter writer = getIndexWriter(repositoryName); | |||
int numDocsBefore = writer.numDocs(); | |||
writer.deleteDocuments(query); | |||
writer.commit(); | |||
int numDocsAfter = writer.numDocs(); | |||
if (numDocsBefore == numDocsAfter) { | |||
logger.debug(MessageFormat.format("no records found to delete {0}", query.toString())); | |||
return false; | |||
} else { | |||
logger.debug(MessageFormat.format("deleted {0} records with {1}", numDocsBefore - numDocsAfter, query.toString())); | |||
return true; | |||
} | |||
} | |||
/** | |||
* Delete a blob from the specified branch of the repository index. | |||
* | |||
@@ -870,10 +801,9 @@ public class LuceneExecutor implements Runnable { | |||
&& branch.equals(defaultBranch)) { | |||
// indexing "default" branch | |||
indexBranch = true; | |||
} else if (IssueUtils.GB_ISSUES.equals(branch)) { | |||
// update issues modified on the GB_ISSUES branch | |||
// note: this is different than reindex | |||
indexBranch = true; | |||
} else if (branch.getName().startsWith(com.gitblit.Constants.R_GITBLIT)) { | |||
// ignore internal Gitblit branches | |||
indexBranch = false; | |||
} else { | |||
// normal explicit branch check | |||
indexBranch = model.indexedBranches.contains(branch.getName()); | |||
@@ -904,35 +834,11 @@ public class LuceneExecutor implements Runnable { | |||
result.branchCount += 1; | |||
} | |||
// track the issue ids that we have already indexed | |||
Set<String> indexedIssues = new TreeSet<String>(); | |||
// reverse the list of commits so we start with the first commit | |||
Collections.reverse(revs); | |||
for (RevCommit commit : revs) { | |||
if (IssueUtils.GB_ISSUES.equals(branch)) { | |||
// only index an issue once during updateIndex | |||
String issueId = commit.getShortMessage().substring(2).trim(); | |||
if (indexedIssues.contains(issueId)) { | |||
continue; | |||
} | |||
indexedIssues.add(issueId); | |||
IssueModel issue = IssueUtils.getIssue(repository, issueId); | |||
if (issue == null) { | |||
// issue was deleted, remove from index | |||
if (!deleteIssue(model.name, issueId)) { | |||
logger.error(MessageFormat.format("Failed to delete issue {0} from Lucene index!", issueId)); | |||
} | |||
} else { | |||
// issue was updated | |||
index(model.name, issue); | |||
result.issueCount++; | |||
} | |||
} else { | |||
// index a commit | |||
result.add(index(model.name, repository, branchName, commit)); | |||
} | |||
// index a commit | |||
result.add(index(model.name, repository, branchName, commit)); | |||
} | |||
// update the config | |||
@@ -958,34 +864,6 @@ public class LuceneExecutor implements Runnable { | |||
return result; | |||
} | |||
/** | |||
* Creates a Lucene document from an issue. | |||
* | |||
* @param issue | |||
* @return a Lucene document | |||
*/ | |||
private Document createDocument(IssueModel issue) { | |||
Document doc = new Document(); | |||
doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.issue.name(), Store.YES, | |||
Field.Index.NOT_ANALYZED)); | |||
doc.add(new Field(FIELD_ISSUE, issue.id, Store.YES, Index.ANALYZED)); | |||
doc.add(new Field(FIELD_BRANCH, IssueUtils.GB_ISSUES, Store.YES, Index.ANALYZED)); | |||
doc.add(new Field(FIELD_DATE, DateTools.dateToString(issue.created, Resolution.MINUTE), | |||
Store.YES, Field.Index.NO)); | |||
doc.add(new Field(FIELD_AUTHOR, issue.reporter, Store.YES, Index.ANALYZED)); | |||
List<String> attachments = new ArrayList<String>(); | |||
for (Attachment attachment : issue.getAttachments()) { | |||
attachments.add(attachment.name.toLowerCase()); | |||
} | |||
doc.add(new Field(FIELD_ATTACHMENT, StringUtils.flattenStrings(attachments), Store.YES, | |||
Index.ANALYZED)); | |||
doc.add(new Field(FIELD_SUMMARY, issue.summary, Store.YES, Index.ANALYZED)); | |||
doc.add(new Field(FIELD_CONTENT, issue.toString(), Store.YES, Index.ANALYZED)); | |||
doc.add(new Field(FIELD_LABEL, StringUtils.flattenStrings(issue.getLabels()), Store.YES, | |||
Index.ANALYZED)); | |||
return doc; | |||
} | |||
/** | |||
* Creates a Lucene document for a commit | |||
* | |||
@@ -1042,7 +920,6 @@ public class LuceneExecutor implements Runnable { | |||
result.type = SearchObjectType.fromName(doc.get(FIELD_OBJECT_TYPE)); | |||
result.branch = doc.get(FIELD_BRANCH); | |||
result.commitId = doc.get(FIELD_COMMIT); | |||
result.issueId = doc.get(FIELD_ISSUE); | |||
result.path = doc.get(FIELD_PATH); | |||
if (doc.get(FIELD_TAG) != null) { | |||
result.tags = StringUtils.getStringsFromValue(doc.get(FIELD_TAG)); |
@@ -1,532 +0,0 @@ | |||
/* | |||
* 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.LinkedHashSet; | |||
import java.util.List; | |||
import java.util.Set; | |||
import com.gitblit.utils.ArrayUtils; | |||
import com.gitblit.utils.StringUtils; | |||
import com.gitblit.utils.TimeUtils; | |||
/** | |||
* 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() { | |||
// the first applied change set the date appropriately | |||
created = new Date(0); | |||
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 boolean hasLabel(String label) { | |||
return getLabels().contains(label); | |||
} | |||
public List<String> getLabels() { | |||
List<String> list = new ArrayList<String>(); | |||
String labels = null; | |||
for (Change change : changes) { | |||
if (change.hasField(Field.Labels)) { | |||
labels = change.getString(Field.Labels); | |||
} | |||
} | |||
if (!StringUtils.isEmpty(labels)) { | |||
list.addAll(StringUtils.getStringsFromValue(labels, " ")); | |||
} | |||
return list; | |||
} | |||
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 List<Attachment> getAttachments() { | |||
List<Attachment> list = new ArrayList<Attachment>(); | |||
for (Change change : changes) { | |||
if (change.hasAttachments()) { | |||
list.addAll(change.attachments); | |||
} | |||
} | |||
return list; | |||
} | |||
public void applyChange(Change change) { | |||
if (changes.size() == 0) { | |||
// first change created the issue | |||
created = change.created; | |||
} | |||
changes.add(change); | |||
if (change.hasFieldChanges()) { | |||
for (FieldChange fieldChange : change.fieldChanges) { | |||
switch (fieldChange.field) { | |||
case Id: | |||
id = fieldChange.value.toString(); | |||
break; | |||
case Type: | |||
type = IssueModel.Type.fromObject(fieldChange.value); | |||
break; | |||
case Status: | |||
status = IssueModel.Status.fromObject(fieldChange.value); | |||
break; | |||
case Priority: | |||
priority = IssueModel.Priority.fromObject(fieldChange.value); | |||
break; | |||
case Summary: | |||
summary = fieldChange.value.toString(); | |||
break; | |||
case Description: | |||
description = fieldChange.value.toString(); | |||
break; | |||
case Reporter: | |||
reporter = fieldChange.value.toString(); | |||
break; | |||
case Owner: | |||
owner = fieldChange.value.toString(); | |||
break; | |||
case Milestone: | |||
milestone = fieldChange.value.toString(); | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
@Override | |||
public String toString() { | |||
StringBuilder sb = new StringBuilder(); | |||
sb.append("issue "); | |||
sb.append(id.substring(0, 8)); | |||
sb.append(" (" + summary + ")\n"); | |||
for (Change change : changes) { | |||
sb.append(change); | |||
sb.append('\n'); | |||
} | |||
return sb.toString(); | |||
} | |||
@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, Comparable<Change> { | |||
private static final long serialVersionUID = 1L; | |||
public final Date created; | |||
public final String author; | |||
public String id; | |||
public char code; | |||
public Comment comment; | |||
public Set<FieldChange> fieldChanges; | |||
public Set<Attachment> attachments; | |||
public Change(String author) { | |||
this.created = new Date((System.currentTimeMillis() / 1000) * 1000); | |||
this.author = author; | |||
this.id = StringUtils.getSHA1(created.toString() + author); | |||
} | |||
public boolean hasComment() { | |||
return comment != null && !comment.deleted; | |||
} | |||
public void comment(String text) { | |||
comment = new Comment(text); | |||
comment.id = StringUtils.getSHA1(created.toString() + author + text); | |||
} | |||
public boolean hasAttachments() { | |||
return !ArrayUtils.isEmpty(attachments); | |||
} | |||
public void addAttachment(Attachment attachment) { | |||
if (attachments == null) { | |||
attachments = new LinkedHashSet<Attachment>(); | |||
} | |||
attachments.add(attachment); | |||
} | |||
public Attachment getAttachment(String name) { | |||
for (Attachment attachment : attachments) { | |||
if (attachment.name.equalsIgnoreCase(name)) { | |||
return attachment; | |||
} | |||
} | |||
return null; | |||
} | |||
public boolean hasField(Field field) { | |||
return !StringUtils.isEmpty(getString(field)); | |||
} | |||
public boolean hasFieldChanges() { | |||
return !ArrayUtils.isEmpty(fieldChanges); | |||
} | |||
public Object getField(Field field) { | |||
if (fieldChanges != null) { | |||
for (FieldChange fieldChange : fieldChanges) { | |||
if (fieldChange.field == field) { | |||
return fieldChange.value; | |||
} | |||
} | |||
} | |||
return null; | |||
} | |||
public void setField(Field field, Object value) { | |||
FieldChange fieldChange = new FieldChange(field, value); | |||
if (fieldChanges == null) { | |||
fieldChanges = new LinkedHashSet<FieldChange>(); | |||
} | |||
fieldChanges.add(fieldChange); | |||
} | |||
public String getString(Field field) { | |||
Object value = getField(field); | |||
if (value == null) { | |||
return null; | |||
} | |||
return value.toString(); | |||
} | |||
@Override | |||
public int compareTo(Change c) { | |||
return created.compareTo(c.created); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return id.hashCode(); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (o instanceof Change) { | |||
return id.equals(((Change) o).id); | |||
} | |||
return false; | |||
} | |||
@Override | |||
public String toString() { | |||
StringBuilder sb = new StringBuilder(); | |||
sb.append(new TimeUtils().timeAgo(created)); | |||
switch (code) { | |||
case '+': | |||
sb.append(" created by "); | |||
break; | |||
default: | |||
if (hasComment()) { | |||
sb.append(" commented on by "); | |||
} else { | |||
sb.append(" changed by "); | |||
} | |||
} | |||
sb.append(author).append(" - "); | |||
if (hasComment()) { | |||
if (comment.deleted) { | |||
sb.append("(deleted) "); | |||
} | |||
sb.append(comment.text).append(" "); | |||
} | |||
if (hasFieldChanges()) { | |||
switch (code) { | |||
case '+': | |||
break; | |||
default: | |||
for (FieldChange fieldChange : fieldChanges) { | |||
sb.append("\n "); | |||
sb.append(fieldChange); | |||
} | |||
break; | |||
} | |||
} | |||
return sb.toString(); | |||
} | |||
} | |||
public static class Comment implements Serializable { | |||
private static final long serialVersionUID = 1L; | |||
public String text; | |||
public String id; | |||
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 final Field field; | |||
public final Object value; | |||
FieldChange(Field field, Object value) { | |||
this.field = field; | |||
this.value = value; | |||
} | |||
@Override | |||
public int hashCode() { | |||
return field.hashCode(); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (o instanceof FieldChange) { | |||
return field.equals(((FieldChange) o).field); | |||
} | |||
return false; | |||
} | |||
@Override | |||
public String toString() { | |||
return field + ": " + value; | |||
} | |||
} | |||
public static class Attachment implements Serializable { | |||
private static final long serialVersionUID = 1L; | |||
public final String name; | |||
public String id; | |||
public long size; | |||
public byte[] content; | |||
public boolean deleted; | |||
public Attachment(String name) { | |||
this.name = name; | |||
} | |||
@Override | |||
public int hashCode() { | |||
return name.hashCode(); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (o instanceof Attachment) { | |||
return name.equalsIgnoreCase(((Attachment) o).name); | |||
} | |||
return false; | |||
} | |||
@Override | |||
public String toString() { | |||
return name; | |||
} | |||
} | |||
public static enum Field { | |||
Id, Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Component, Labels; | |||
} | |||
public static enum Type { | |||
Defect, Enhancement, Task, Review, Other; | |||
public static Type fromObject(Object o) { | |||
if (o instanceof Type) { | |||
// cast and return | |||
return (Type) o; | |||
} else if (o instanceof String) { | |||
// find by name | |||
for (Type type : values()) { | |||
String str = o.toString(); | |||
if (type.toString().equalsIgnoreCase(str)) { | |||
return type; | |||
} | |||
} | |||
} else if (o instanceof Number) { | |||
// by ordinal | |||
int id = ((Number) o).intValue(); | |||
if (id >= 0 && id < values().length) { | |||
return values()[id]; | |||
} | |||
} | |||
return null; | |||
} | |||
} | |||
public static enum Priority { | |||
Low, Medium, High, Critical; | |||
public static Priority fromObject(Object o) { | |||
if (o instanceof Priority) { | |||
// cast and return | |||
return (Priority) o; | |||
} else if (o instanceof String) { | |||
// find by name | |||
for (Priority priority : values()) { | |||
String str = o.toString(); | |||
if (priority.toString().equalsIgnoreCase(str)) { | |||
return priority; | |||
} | |||
} | |||
} else if (o instanceof Number) { | |||
// by ordinal | |||
int id = ((Number) o).intValue(); | |||
if (id >= 0 && id < values().length) { | |||
return values()[id]; | |||
} | |||
} | |||
return null; | |||
} | |||
} | |||
public static enum Status { | |||
New, Accepted, Started, Review, Queued, Testing, Done, Fixed, WontFix, Duplicate, Invalid; | |||
public static Status fromObject(Object o) { | |||
if (o instanceof Status) { | |||
// cast and return | |||
return (Status) o; | |||
} else if (o instanceof String) { | |||
// find by name | |||
for (Status status : values()) { | |||
String str = o.toString(); | |||
if (status.toString().equalsIgnoreCase(str)) { | |||
return status; | |||
} | |||
} | |||
} else if (o instanceof Number) { | |||
// by ordinal | |||
int id = ((Number) o).intValue(); | |||
if (id >= 0 && id < values().length) { | |||
return values()[id]; | |||
} | |||
} | |||
return null; | |||
} | |||
public boolean atLeast(Status status) { | |||
return ordinal() >= status.ordinal(); | |||
} | |||
public boolean exceeds(Status status) { | |||
return ordinal() > status.ordinal(); | |||
} | |||
public boolean isClosed() { | |||
return ordinal() >= Done.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; | |||
} | |||
} | |||
} |
@@ -1,829 +0,0 @@ | |||
/* | |||
* 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.Collection; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.TreeSet; | |||
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.internal.JGitText; | |||
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 org.eclipse.jgit.treewalk.filter.AndTreeFilter; | |||
import org.eclipse.jgit.treewalk.filter.PathFilterGroup; | |||
import org.eclipse.jgit.treewalk.filter.TreeFilter; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
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.Status; | |||
import com.gitblit.models.RefModel; | |||
import com.gitblit.utils.JsonUtils.ExcludeField; | |||
import com.google.gson.Gson; | |||
import com.google.gson.reflect.TypeToken; | |||
/** | |||
* Utility class for reading Gitblit issues. | |||
* | |||
* @author James Moger | |||
* | |||
*/ | |||
public class IssueUtils { | |||
public static interface IssueFilter { | |||
public abstract boolean accept(IssueModel issue); | |||
} | |||
public static final String GB_ISSUES = "refs/gitblit/issues"; | |||
static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.class); | |||
/** | |||
* Log an error message and exception. | |||
* | |||
* @param t | |||
* @param repository | |||
* if repository is not null it MUST be the {0} parameter in the | |||
* pattern. | |||
* @param pattern | |||
* @param objects | |||
*/ | |||
private static void error(Throwable t, Repository repository, String pattern, Object... objects) { | |||
List<Object> parameters = new ArrayList<Object>(); | |||
if (objects != null && objects.length > 0) { | |||
for (Object o : objects) { | |||
parameters.add(o); | |||
} | |||
} | |||
if (repository != null) { | |||
parameters.add(0, repository.getDirectory().getAbsolutePath()); | |||
} | |||
LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t); | |||
} | |||
/** | |||
* 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) { | |||
List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT); | |||
for (RefModel ref : refs) { | |||
if (ref.reference.getName().equals(GB_ISSUES)) { | |||
return ref; | |||
} | |||
} | |||
return null; | |||
} | |||
/** | |||
* Returns all the issues in the repository. Querying issues from the | |||
* repository requires deserializing all changes for all issues. This is an | |||
* expensive process and not recommended. Issues should be indexed by Lucene | |||
* and queries should be executed against that index. | |||
* | |||
* @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; | |||
} | |||
// Collect the set of all issue paths | |||
Set<String> issuePaths = new HashSet<String>(); | |||
final TreeWalk tw = new TreeWalk(repository); | |||
try { | |||
RevCommit head = JGitUtils.getCommit(repository, GB_ISSUES); | |||
tw.addTree(head.getTree()); | |||
tw.setRecursive(false); | |||
while (tw.next()) { | |||
if (tw.getDepth() < 2 && tw.isSubtree()) { | |||
tw.enterSubtree(); | |||
if (tw.getDepth() == 2) { | |||
issuePaths.add(tw.getPathString()); | |||
} | |||
} | |||
} | |||
} catch (IOException e) { | |||
error(e, repository, "{0} failed to query issues"); | |||
} finally { | |||
tw.release(); | |||
} | |||
// Build each issue and optionally filter out unwanted issues | |||
for (String issuePath : issuePaths) { | |||
RevWalk rw = new RevWalk(repository); | |||
try { | |||
RevCommit start = rw.parseCommit(repository.resolve(GB_ISSUES)); | |||
rw.markStart(start); | |||
} catch (Exception e) { | |||
error(e, repository, "Failed to find {1} in {0}", GB_ISSUES); | |||
} | |||
TreeFilter treeFilter = AndTreeFilter.create( | |||
PathFilterGroup.createFromStrings(issuePath), TreeFilter.ANY_DIFF); | |||
rw.setTreeFilter(treeFilter); | |||
Iterator<RevCommit> revlog = rw.iterator(); | |||
List<RevCommit> commits = new ArrayList<RevCommit>(); | |||
while (revlog.hasNext()) { | |||
commits.add(revlog.next()); | |||
} | |||
// release the revwalk | |||
rw.release(); | |||
if (commits.size() == 0) { | |||
LOGGER.warn("Failed to find changes for issue " + issuePath); | |||
continue; | |||
} | |||
// sort by commit order, first commit first | |||
Collections.reverse(commits); | |||
StringBuilder sb = new StringBuilder("["); | |||
boolean first = true; | |||
for (RevCommit commit : commits) { | |||
if (!first) { | |||
sb.append(','); | |||
} | |||
String message = commit.getFullMessage(); | |||
// commit message is formatted: C ISSUEID\n\nJSON | |||
// C is an single char commit code | |||
// ISSUEID is an SHA-1 hash | |||
String json = message.substring(43); | |||
sb.append(json); | |||
first = false; | |||
} | |||
sb.append(']'); | |||
// Deserialize the JSON array as a Collection<Change>, this seems | |||
// slightly faster than deserializing each change by itself. | |||
Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(), | |||
new TypeToken<Collection<Change>>() { | |||
}.getType()); | |||
// create an issue object form the changes | |||
IssueModel issue = buildIssue(changes, true); | |||
// add the issue, conditionally, to the list | |||
if (filter == null) { | |||
list.add(issue); | |||
} else { | |||
if (filter.accept(issue)) { | |||
list.add(issue); | |||
} | |||
} | |||
} | |||
// sort the issues by creation | |||
Collections.sort(list); | |||
return list; | |||
} | |||
/** | |||
* Retrieves the specified issue from the repository with all changes | |||
* applied to build the effective issue. | |||
* | |||
* @param repository | |||
* @param issueId | |||
* @return an issue, if it exists, otherwise null | |||
*/ | |||
public static IssueModel getIssue(Repository repository, String issueId) { | |||
return getIssue(repository, issueId, true); | |||
} | |||
/** | |||
* Retrieves the specified issue from the repository. | |||
* | |||
* @param repository | |||
* @param issueId | |||
* @param effective | |||
* if true, the effective issue is built by processing comment | |||
* changes, deletions, etc. if false, the raw issue is built | |||
* without consideration for comment changes, deletions, etc. | |||
* @return an issue, if it exists, otherwise null | |||
*/ | |||
public static IssueModel getIssue(Repository repository, String issueId, boolean effective) { | |||
RefModel issuesBranch = getIssuesBranch(repository); | |||
if (issuesBranch == null) { | |||
return null; | |||
} | |||
if (StringUtils.isEmpty(issueId)) { | |||
return null; | |||
} | |||
String issuePath = getIssuePath(issueId); | |||
// Collect all changes as JSON array from commit messages | |||
List<RevCommit> commits = JGitUtils.getRevLog(repository, GB_ISSUES, issuePath, 0, -1); | |||
// sort by commit order, first commit first | |||
Collections.reverse(commits); | |||
StringBuilder sb = new StringBuilder("["); | |||
boolean first = true; | |||
for (RevCommit commit : commits) { | |||
if (!first) { | |||
sb.append(','); | |||
} | |||
String message = commit.getFullMessage(); | |||
// commit message is formatted: C ISSUEID\n\nJSON | |||
// C is an single char commit code | |||
// ISSUEID is an SHA-1 hash | |||
String json = message.substring(43); | |||
sb.append(json); | |||
first = false; | |||
} | |||
sb.append(']'); | |||
// Deserialize the JSON array as a Collection<Change>, this seems | |||
// slightly faster than deserializing each change by itself. | |||
Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(), | |||
new TypeToken<Collection<Change>>() { | |||
}.getType()); | |||
// create an issue object and apply the changes to it | |||
IssueModel issue = buildIssue(changes, effective); | |||
return issue; | |||
} | |||
/** | |||
* Builds an issue from a set of changes. | |||
* | |||
* @param changes | |||
* @param effective | |||
* if true, the effective issue is built which accounts for | |||
* comment changes, comment deletions, etc. if false, the raw | |||
* issue is built. | |||
* @return an issue | |||
*/ | |||
private static IssueModel buildIssue(Collection<Change> changes, boolean effective) { | |||
IssueModel issue; | |||
if (effective) { | |||
List<Change> effectiveChanges = new ArrayList<Change>(); | |||
Map<String, Change> comments = new HashMap<String, Change>(); | |||
for (Change change : changes) { | |||
if (change.comment != null) { | |||
if (comments.containsKey(change.comment.id)) { | |||
Change original = comments.get(change.comment.id); | |||
Change clone = DeepCopier.copy(original); | |||
clone.comment.text = change.comment.text; | |||
clone.comment.deleted = change.comment.deleted; | |||
int idx = effectiveChanges.indexOf(original); | |||
effectiveChanges.remove(original); | |||
effectiveChanges.add(idx, clone); | |||
comments.put(clone.comment.id, clone); | |||
} else { | |||
effectiveChanges.add(change); | |||
comments.put(change.comment.id, change); | |||
} | |||
} else { | |||
effectiveChanges.add(change); | |||
} | |||
} | |||
// effective issue | |||
issue = new IssueModel(); | |||
for (Change change : effectiveChanges) { | |||
issue.applyChange(change); | |||
} | |||
} else { | |||
// raw issue | |||
issue = new IssueModel(); | |||
for (Change change : changes) { | |||
issue.applyChange(change); | |||
} | |||
} | |||
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 | |||
IssueModel issue = getIssue(repository, issueId, true); | |||
Attachment attachment = issue.getAttachment(filename); | |||
// attachment not found | |||
if (attachment == null) { | |||
return null; | |||
} | |||
// retrieve the attachment content | |||
String issuePath = getIssuePath(issueId); | |||
RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree(); | |||
byte[] content = JGitUtils | |||
.getByteContent(repository, tree, issuePath + "/" + attachment.id, false); | |||
attachment.content = content; | |||
attachment.size = content.length; | |||
return attachment; | |||
} | |||
/** | |||
* Creates an issue in the gb-issues branch of the repository. The branch is | |||
* automatically created if it does not already exist. Your change must | |||
* include an author, summary, and description, at a minimum. If your change | |||
* does not have those minimum requirements a RuntimeException will be | |||
* thrown. | |||
* | |||
* @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); | |||
} | |||
if (StringUtils.isEmpty(change.author)) { | |||
throw new RuntimeException("Must specify a change author!"); | |||
} | |||
if (!change.hasField(Field.Summary)) { | |||
throw new RuntimeException("Must specify a summary!"); | |||
} | |||
if (!change.hasField(Field.Description)) { | |||
throw new RuntimeException("Must specify a description!"); | |||
} | |||
change.setField(Field.Reporter, change.author); | |||
String issueId = StringUtils.getSHA1(change.created.toString() + change.author | |||
+ change.getString(Field.Summary) + change.getField(Field.Description)); | |||
change.setField(Field.Id, issueId); | |||
change.code = '+'; | |||
boolean success = commit(repository, issueId, change); | |||
if (success) { | |||
return getIssue(repository, issueId, false); | |||
} | |||
return null; | |||
} | |||
/** | |||
* Updates an issue in the gb-issues branch of the repository. | |||
* | |||
* @param repository | |||
* @param issueId | |||
* @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 a change author!"); | |||
} | |||
// determine update code | |||
// default update code is '=' for a general change | |||
change.code = '='; | |||
if (change.hasField(Field.Status)) { | |||
Status status = Status.fromObject(change.getField(Field.Status)); | |||
if (status.isClosed()) { | |||
// someone closed the issue | |||
change.code = 'x'; | |||
} | |||
} | |||
success = commit(repository, issueId, change); | |||
return success; | |||
} | |||
/** | |||
* Deletes an issue from the repository. | |||
* | |||
* @param repository | |||
* @param issueId | |||
* @return true if successful | |||
*/ | |||
public static boolean deleteIssue(Repository repository, String issueId, String author) { | |||
boolean success = false; | |||
RefModel issuesBranch = getIssuesBranch(repository); | |||
if (issuesBranch == null) { | |||
throw new RuntimeException(GB_ISSUES + " does not exist!"); | |||
} | |||
if (StringUtils.isEmpty(issueId)) { | |||
throw new RuntimeException("must specify an issue id!"); | |||
} | |||
String issuePath = getIssuePath(issueId); | |||
String message = "- " + issueId; | |||
try { | |||
ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}"); | |||
ObjectInserter odi = repository.newObjectInserter(); | |||
try { | |||
// Create the in-memory index of the new/updated issue | |||
DirCache index = DirCache.newInCore(); | |||
DirCacheBuilder dcBuilder = index.builder(); | |||
// Traverse HEAD to add all other paths | |||
TreeWalk treeWalk = new TreeWalk(repository); | |||
int hIdx = -1; | |||
if (headId != null) | |||
hIdx = treeWalk.addTree(new RevWalk(repository).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 (!path.startsWith(issuePath)) { | |||
// 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(); | |||
ObjectId indexTreeId = index.writeTree(odi); | |||
// Create a commit object | |||
PersonIdent ident = new PersonIdent(author, "gitblit@localhost"); | |||
CommitBuilder commit = new CommitBuilder(); | |||
commit.setAuthor(ident); | |||
commit.setCommitter(ident); | |||
commit.setEncoding(Constants.CHARACTER_ENCODING); | |||
commit.setMessage(message); | |||
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) { | |||
error(t, repository, "Failed to delete issue {1} to {0}", issueId); | |||
} | |||
return success; | |||
} | |||
/** | |||
* Changes the text of an issue comment. | |||
* | |||
* @param repository | |||
* @param issue | |||
* @param change | |||
* the change with the comment to change | |||
* @param author | |||
* the author of the revision | |||
* @param comment | |||
* the revised comment | |||
* @return true, if the change was successful | |||
*/ | |||
public static boolean changeComment(Repository repository, IssueModel issue, Change change, | |||
String author, String comment) { | |||
Change revision = new Change(author); | |||
revision.comment(comment); | |||
revision.comment.id = change.comment.id; | |||
return updateIssue(repository, issue.id, revision); | |||
} | |||
/** | |||
* Deletes a comment from an issue. | |||
* | |||
* @param repository | |||
* @param issue | |||
* @param change | |||
* the change with the comment to delete | |||
* @param author | |||
* @return true, if the deletion was successful | |||
*/ | |||
public static boolean deleteComment(Repository repository, IssueModel issue, Change change, | |||
String author) { | |||
Change deletion = new Change(author); | |||
deletion.comment(change.comment.text); | |||
deletion.comment.id = change.comment.id; | |||
deletion.comment.deleted = true; | |||
return updateIssue(repository, issue.id, deletion); | |||
} | |||
/** | |||
* Commit a change to the repository. Each issue is composed on changes. | |||
* Issues are built from applying the changes in the order they were | |||
* committed to the repository. The changes are actually specified in the | |||
* commit messages and not in the RevTrees which allows for clean, | |||
* distributed merging. | |||
* | |||
* @param repository | |||
* @param issueId | |||
* @param change | |||
* @return true, if the change was committed | |||
*/ | |||
private static boolean commit(Repository repository, String issueId, Change change) { | |||
boolean success = false; | |||
try { | |||
// assign ids to new attachments | |||
// attachments are stored by an SHA1 id | |||
if (change.hasAttachments()) { | |||
for (Attachment attachment : change.attachments) { | |||
if (!ArrayUtils.isEmpty(attachment.content)) { | |||
byte[] prefix = (change.created.toString() + change.author).getBytes(); | |||
byte[] bytes = new byte[prefix.length + attachment.content.length]; | |||
System.arraycopy(prefix, 0, bytes, 0, prefix.length); | |||
System.arraycopy(attachment.content, 0, bytes, prefix.length, | |||
attachment.content.length); | |||
attachment.id = "attachment-" + StringUtils.getSHA1(bytes); | |||
} | |||
} | |||
} | |||
// serialize the change as json | |||
// exclude any attachment from json serialization | |||
Gson gson = JsonUtils.gson(new ExcludeField( | |||
"com.gitblit.models.IssueModel$Attachment.content")); | |||
String json = gson.toJson(change); | |||
// include the json change in the commit message | |||
String issuePath = getIssuePath(issueId); | |||
String message = change.code + " " + issueId + "\n\n" + json; | |||
// Create a commit file. This is required for a proper commit and | |||
// ensures we can retrieve the commit log of the issue path. | |||
// | |||
// This file is NOT serialized as part of the Change object. | |||
switch (change.code) { | |||
case '+': { | |||
// New Issue. | |||
Attachment placeholder = new Attachment("issue"); | |||
placeholder.id = placeholder.name; | |||
placeholder.content = "DO NOT REMOVE".getBytes(Constants.CHARACTER_ENCODING); | |||
change.addAttachment(placeholder); | |||
break; | |||
} | |||
default: { | |||
// Update Issue. | |||
String changeId = StringUtils.getSHA1(json); | |||
Attachment placeholder = new Attachment("change-" + changeId); | |||
placeholder.id = placeholder.name; | |||
placeholder.content = "REMOVABLE".getBytes(Constants.CHARACTER_ENCODING); | |||
change.addAttachment(placeholder); | |||
break; | |||
} | |||
} | |||
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, issuePath, change); | |||
ObjectId indexTreeId = index.writeTree(odi); | |||
// Create a commit object | |||
PersonIdent ident = new PersonIdent(change.author, "gitblit@localhost"); | |||
CommitBuilder commit = new CommitBuilder(); | |||
commit.setAuthor(ident); | |||
commit.setCommitter(ident); | |||
commit.setEncoding(Constants.CHARACTER_ENCODING); | |||
commit.setMessage(message); | |||
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) { | |||
error(t, repository, "Failed to commit issue {1} to {0}", issueId); | |||
} | |||
return success; | |||
} | |||
/** | |||
* 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 | |||
*/ | |||
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 change | |||
* @return an in-memory index | |||
* @throws IOException | |||
*/ | |||
private static DirCache createIndex(Repository repo, ObjectId headId, String issuePath, | |||
Change change) throws IOException { | |||
DirCache inCoreIndex = DirCache.newInCore(); | |||
DirCacheBuilder dcBuilder = inCoreIndex.builder(); | |||
ObjectInserter inserter = repo.newObjectInserter(); | |||
Set<String> ignorePaths = new TreeSet<String>(); | |||
try { | |||
// Add any attachments to the temporary index | |||
if (change.hasAttachments()) { | |||
for (Attachment attachment : change.attachments) { | |||
// build a path name for the attachment and mark as ignored | |||
String path = issuePath + "/" + attachment.id; | |||
ignorePaths.add(path); | |||
// create an index entry for this attachment | |||
final DirCacheEntry dcEntry = new DirCacheEntry(path); | |||
dcEntry.setLength(attachment.content.length); | |||
dcEntry.setLastModified(change.created.getTime()); | |||
dcEntry.setFileMode(FileMode.REGULAR_FILE); | |||
// insert object | |||
dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, attachment.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 (!ignorePaths.contains(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; | |||
} | |||
} |
@@ -59,7 +59,7 @@ import com.gitblit.utils.JGitUtils; | |||
MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class, | |||
DiffUtilsTest.class, MetricUtilsTest.class, X509UtilsTest.class, | |||
GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class, | |||
GroovyScriptTest.class, LuceneExecutorTest.class, IssuesTest.class, RepositoryModelTest.class, | |||
GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, | |||
FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdUserServiceTest.class, | |||
ModelUtilsTest.class, JnaUtilsTest.class }) | |||
public class GitBlitSuite { |
@@ -1,231 +0,0 @@ | |||
/* | |||
* 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.assertFalse; | |||
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.LuceneExecutor; | |||
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.models.IssueModel.Status; | |||
import com.gitblit.models.SearchResult; | |||
import com.gitblit.utils.FileUtils; | |||
import com.gitblit.utils.IssueUtils; | |||
import com.gitblit.utils.IssueUtils.IssueFilter; | |||
/** | |||
* Tests the mechanics of distributed issue management on the gb-issues branch. | |||
* | |||
* @author James Moger | |||
* | |||
*/ | |||
public class IssuesTest { | |||
@Test | |||
public void testLifecycle() throws Exception { | |||
Repository repository = GitBlitSuite.getIssuesTestRepository(); | |||
String name = FileUtils.getRelativePath(GitBlitSuite.REPOSITORIES, repository.getDirectory()); | |||
// create and insert an issue | |||
Change c1 = newChange("testCreation() " + 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); | |||
assertEquals(1, constructed.changes.size()); | |||
// C1: create the issue | |||
c1 = newChange("testUpdates() " + Long.toHexString(System.currentTimeMillis())); | |||
issue = IssueUtils.createIssue(repository, c1); | |||
assertNotNull(issue.id); | |||
constructed = IssueUtils.getIssue(repository, issue.id); | |||
compare(issue, constructed); | |||
assertEquals(1, constructed.changes.size()); | |||
// C2: set owner | |||
Change c2 = new Change("C2"); | |||
c2.comment("I'll fix this"); | |||
c2.setField(Field.Owner, c2.author); | |||
assertTrue(IssueUtils.updateIssue(repository, issue.id, c2)); | |||
constructed = IssueUtils.getIssue(repository, issue.id); | |||
assertEquals(2, constructed.changes.size()); | |||
assertEquals(c2.author, constructed.owner); | |||
// C3: add a note | |||
Change c3 = new Change("C3"); | |||
c3.comment("yeah, this is working"); | |||
assertTrue(IssueUtils.updateIssue(repository, issue.id, c3)); | |||
constructed = IssueUtils.getIssue(repository, issue.id); | |||
assertEquals(3, constructed.changes.size()); | |||
// C4: add attachment | |||
Change c4 = new Change("C4"); | |||
Attachment a = newAttachment(); | |||
c4.addAttachment(a); | |||
assertTrue(IssueUtils.updateIssue(repository, issue.id, c4)); | |||
Attachment a1 = IssueUtils.getIssueAttachment(repository, issue.id, a.name); | |||
assertEquals(a.content.length, a1.content.length); | |||
assertTrue(Arrays.areEqual(a.content, a1.content)); | |||
// C5: close the issue | |||
Change c5 = new Change("C5"); | |||
c5.comment("closing issue"); | |||
c5.setField(Field.Status, Status.Fixed); | |||
assertTrue(IssueUtils.updateIssue(repository, issue.id, c5)); | |||
// retrieve issue again | |||
constructed = IssueUtils.getIssue(repository, issue.id); | |||
assertEquals(5, constructed.changes.size()); | |||
assertTrue(constructed.status.isClosed()); | |||
List<IssueModel> allIssues = IssueUtils.getIssues(repository, null); | |||
List<IssueModel> openIssues = IssueUtils.getIssues(repository, new IssueFilter() { | |||
@Override | |||
public boolean accept(IssueModel issue) { | |||
return !issue.status.isClosed(); | |||
} | |||
}); | |||
List<IssueModel> closedIssues = IssueUtils.getIssues(repository, new IssueFilter() { | |||
@Override | |||
public boolean accept(IssueModel issue) { | |||
return issue.status.isClosed(); | |||
} | |||
}); | |||
assertTrue(allIssues.size() > 0); | |||
assertEquals(1, openIssues.size()); | |||
assertEquals(1, closedIssues.size()); | |||
// build a new Lucene index | |||
LuceneExecutor lucene = new LuceneExecutor(null, GitBlitSuite.REPOSITORIES); | |||
lucene.deleteIndex(name); | |||
for (IssueModel anIssue : allIssues) { | |||
lucene.index(name, anIssue); | |||
} | |||
List<SearchResult> hits = lucene.search("working", 1, 10, name); | |||
assertTrue(hits.size() == 1); | |||
// reindex an issue | |||
issue = allIssues.get(0); | |||
Change change = new Change("reindex"); | |||
change.comment("this is a test of reindexing an issue"); | |||
IssueUtils.updateIssue(repository, issue.id, change); | |||
issue = IssueUtils.getIssue(repository, issue.id); | |||
lucene.index(name, issue); | |||
hits = lucene.search("working", 1, 10, name); | |||
assertTrue(hits.size() == 1); | |||
// delete all issues | |||
for (IssueModel anIssue : allIssues) { | |||
assertTrue(IssueUtils.deleteIssue(repository, anIssue.id, "D")); | |||
} | |||
lucene.close(); | |||
repository.close(); | |||
} | |||
@Test | |||
public void testChangeComment() throws Exception { | |||
Repository repository = GitBlitSuite.getIssuesTestRepository(); | |||
// C1: create the issue | |||
Change c1 = newChange("testChangeComment() " + Long.toHexString(System.currentTimeMillis())); | |||
IssueModel issue = IssueUtils.createIssue(repository, c1); | |||
assertNotNull(issue.id); | |||
assertTrue(issue.changes.get(0).hasComment()); | |||
assertTrue(IssueUtils.changeComment(repository, issue, c1, "E1", "I changed the comment")); | |||
issue = IssueUtils.getIssue(repository, issue.id); | |||
assertTrue(issue.changes.get(0).hasComment()); | |||
assertEquals("I changed the comment", issue.changes.get(0).comment.text); | |||
assertTrue(IssueUtils.deleteIssue(repository, issue.id, "D")); | |||
repository.close(); | |||
} | |||
@Test | |||
public void testDeleteComment() throws Exception { | |||
Repository repository = GitBlitSuite.getIssuesTestRepository(); | |||
// C1: create the issue | |||
Change c1 = newChange("testDeleteComment() " + Long.toHexString(System.currentTimeMillis())); | |||
IssueModel issue = IssueUtils.createIssue(repository, c1); | |||
assertNotNull(issue.id); | |||
assertTrue(issue.changes.get(0).hasComment()); | |||
assertTrue(IssueUtils.deleteComment(repository, issue, c1, "D1")); | |||
issue = IssueUtils.getIssue(repository, issue.id); | |||
assertEquals(1, issue.changes.size()); | |||
assertFalse(issue.changes.get(0).hasComment()); | |||
issue = IssueUtils.getIssue(repository, issue.id, false); | |||
assertEquals(2, issue.changes.size()); | |||
assertTrue(issue.changes.get(0).hasComment()); | |||
assertFalse(issue.changes.get(1).hasComment()); | |||
assertTrue(IssueUtils.deleteIssue(repository, issue.id, "D")); | |||
repository.close(); | |||
} | |||
private Change newChange(String summary) { | |||
Change change = new Change("C1"); | |||
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"); | |||
return change; | |||
} | |||
private Attachment newAttachment() { | |||
Attachment attachment = new Attachment(Long.toHexString(System.currentTimeMillis()) | |||
+ ".txt"); | |||
attachment.content = new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, | |||
0x4a }; | |||
return attachment; | |||
} | |||
private void compare(IssueModel issue, IssueModel constructed) { | |||
assertEquals(issue.id, constructed.id); | |||
assertEquals(issue.reporter, constructed.reporter); | |||
assertEquals(issue.owner, constructed.owner); | |||
assertEquals(issue.summary, constructed.summary); | |||
assertEquals(issue.description, constructed.description); | |||
assertEquals(issue.created, constructed.created); | |||
assertTrue(issue.hasLabel("helpdesk")); | |||
} | |||
} |