/* | |
* 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.JGitText; | |
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; | |
import org.eclipse.jgit.api.errors.JGitInternalException; | |
import org.eclipse.jgit.dircache.DirCache; | |
import org.eclipse.jgit.dircache.DirCacheBuilder; | |
import org.eclipse.jgit.dircache.DirCacheEntry; | |
import org.eclipse.jgit.lib.CommitBuilder; | |
import org.eclipse.jgit.lib.Constants; | |
import org.eclipse.jgit.lib.FileMode; | |
import org.eclipse.jgit.lib.ObjectId; | |
import org.eclipse.jgit.lib.ObjectInserter; | |
import org.eclipse.jgit.lib.PersonIdent; | |
import org.eclipse.jgit.lib.RefUpdate; | |
import org.eclipse.jgit.lib.RefUpdate.Result; | |
import org.eclipse.jgit.lib.Repository; | |
import org.eclipse.jgit.revwalk.RevCommit; | |
import org.eclipse.jgit.revwalk.RevTree; | |
import org.eclipse.jgit.revwalk.RevWalk; | |
import org.eclipse.jgit.treewalk.CanonicalTreeParser; | |
import org.eclipse.jgit.treewalk.TreeWalk; | |
import 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/heads/gb-issues"; | |
static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.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) { | |
return JGitUtils.getBranch(repository, "gb-issues"); | |
} | |
/** | |
* 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); | |
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 issue | |
* @param change | |
* @return true if successful | |
*/ | |
public static boolean updateIssue(Repository repository, String issueId, Change change) { | |
boolean success = false; | |
RefModel issuesBranch = getIssuesBranch(repository); | |
if (issuesBranch == null) { | |
throw new RuntimeException("gb-issues branch does not exist!"); | |
} | |
if (change == null) { | |
throw new RuntimeException("change can not be null!"); | |
} | |
if (StringUtils.isEmpty(change.author)) { | |
throw new RuntimeException("must specify 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 branch 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 issue | |
* @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; | |
} | |
} |