From bd01eebfa57b4012bc7a7abc1aaaa1b69278b9de Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sun, 19 Feb 2012 15:38:50 -0500
Subject: [PATCH] Merged issues/lucene branch

---
 src/com/gitblit/utils/IssueUtils.java |  823 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 823 insertions(+), 0 deletions(-)

diff --git a/src/com/gitblit/utils/IssueUtils.java b/src/com/gitblit/utils/IssueUtils.java
new file mode 100644
index 0000000..eb3b347
--- /dev/null
+++ b/src/com/gitblit/utils/IssueUtils.java
@@ -0,0 +1,823 @@
+/*
+ * 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;
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1