James Moger
2012-10-31 40b07bca7d02438cd0d660f3b1713ffa86f6df76
commit | author | age
0f43a5 1 /*
JM 2  * Copyright 2012 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.utils;
17
18 import java.io.IOException;
19 import java.text.MessageFormat;
20 import java.util.ArrayList;
69a559 21 import java.util.Collection;
0f43a5 22 import java.util.Collections;
JM 23 import java.util.HashMap;
69a559 24 import java.util.HashSet;
JM 25 import java.util.Iterator;
0f43a5 26 import java.util.List;
JM 27 import java.util.Map;
69a559 28 import java.util.Set;
JM 29 import java.util.TreeSet;
0f43a5 30
JM 31 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
32 import org.eclipse.jgit.api.errors.JGitInternalException;
33 import org.eclipse.jgit.dircache.DirCache;
34 import org.eclipse.jgit.dircache.DirCacheBuilder;
35 import org.eclipse.jgit.dircache.DirCacheEntry;
5d9bd7 36 import org.eclipse.jgit.internal.JGitText;
0f43a5 37 import org.eclipse.jgit.lib.CommitBuilder;
JM 38 import org.eclipse.jgit.lib.Constants;
39 import org.eclipse.jgit.lib.FileMode;
40 import org.eclipse.jgit.lib.ObjectId;
41 import org.eclipse.jgit.lib.ObjectInserter;
42 import org.eclipse.jgit.lib.PersonIdent;
43 import org.eclipse.jgit.lib.RefUpdate;
44 import org.eclipse.jgit.lib.RefUpdate.Result;
45 import org.eclipse.jgit.lib.Repository;
46 import org.eclipse.jgit.revwalk.RevCommit;
47 import org.eclipse.jgit.revwalk.RevTree;
48 import org.eclipse.jgit.revwalk.RevWalk;
49 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
50 import org.eclipse.jgit.treewalk.TreeWalk;
69a559 51 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
JM 52 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
53 import org.eclipse.jgit.treewalk.filter.TreeFilter;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
0f43a5 56
JM 57 import com.gitblit.models.IssueModel;
58 import com.gitblit.models.IssueModel.Attachment;
59 import com.gitblit.models.IssueModel.Change;
60 import com.gitblit.models.IssueModel.Field;
69a559 61 import com.gitblit.models.IssueModel.Status;
0f43a5 62 import com.gitblit.models.RefModel;
JM 63 import com.gitblit.utils.JsonUtils.ExcludeField;
64 import com.google.gson.Gson;
69a559 65 import com.google.gson.reflect.TypeToken;
0f43a5 66
JM 67 /**
68  * Utility class for reading Gitblit issues.
69  * 
70  * @author James Moger
71  * 
72  */
73 public class IssueUtils {
74
69a559 75     public static interface IssueFilter {
JM 76         public abstract boolean accept(IssueModel issue);
77     }
78
0f43a5 79     public static final String GB_ISSUES = "refs/heads/gb-issues";
69a559 80
JM 81     static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
82
83     /**
84      * Log an error message and exception.
85      * 
86      * @param t
87      * @param repository
88      *            if repository is not null it MUST be the {0} parameter in the
89      *            pattern.
90      * @param pattern
91      * @param objects
92      */
93     private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
94         List<Object> parameters = new ArrayList<Object>();
95         if (objects != null && objects.length > 0) {
96             for (Object o : objects) {
97                 parameters.add(o);
98             }
99         }
100         if (repository != null) {
101             parameters.add(0, repository.getDirectory().getAbsolutePath());
102         }
103         LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
104     }
0f43a5 105
JM 106     /**
107      * Returns a RefModel for the gb-issues branch in the repository. If the
108      * branch can not be found, null is returned.
109      * 
110      * @param repository
111      * @return a refmodel for the gb-issues branch or null
112      */
113     public static RefModel getIssuesBranch(Repository repository) {
114         return JGitUtils.getBranch(repository, "gb-issues");
115     }
116
117     /**
69a559 118      * Returns all the issues in the repository. Querying issues from the
JM 119      * repository requires deserializing all changes for all issues. This is an
120      * expensive process and not recommended. Issues should be indexed by Lucene
121      * and queries should be executed against that index.
0f43a5 122      * 
JM 123      * @param repository
124      * @param filter
125      *            optional issue filter to only return matching results
126      * @return a list of issues
127      */
128     public static List<IssueModel> getIssues(Repository repository, IssueFilter filter) {
129         List<IssueModel> list = new ArrayList<IssueModel>();
130         RefModel issuesBranch = getIssuesBranch(repository);
131         if (issuesBranch == null) {
132             return list;
133         }
69a559 134
JM 135         // Collect the set of all issue paths
136         Set<String> issuePaths = new HashSet<String>();
137         final TreeWalk tw = new TreeWalk(repository);
138         try {
139             RevCommit head = JGitUtils.getCommit(repository, GB_ISSUES);
140             tw.addTree(head.getTree());
141             tw.setRecursive(false);
142             while (tw.next()) {
143                 if (tw.getDepth() < 2 && tw.isSubtree()) {
144                     tw.enterSubtree();
145                     if (tw.getDepth() == 2) {
146                         issuePaths.add(tw.getPathString());
147                     }
148                 }
149             }
150         } catch (IOException e) {
151             error(e, repository, "{0} failed to query issues");
152         } finally {
153             tw.release();
154         }
155
156         // Build each issue and optionally filter out unwanted issues
157
158         for (String issuePath : issuePaths) {
159             RevWalk rw = new RevWalk(repository);
160             try {
161                 RevCommit start = rw.parseCommit(repository.resolve(GB_ISSUES));
162                 rw.markStart(start);
163             } catch (Exception e) {
164                 error(e, repository, "Failed to find {1} in {0}", GB_ISSUES);
165             }
166             TreeFilter treeFilter = AndTreeFilter.create(
167                     PathFilterGroup.createFromStrings(issuePath), TreeFilter.ANY_DIFF);
168             rw.setTreeFilter(treeFilter);
169             Iterator<RevCommit> revlog = rw.iterator();
170
171             List<RevCommit> commits = new ArrayList<RevCommit>();
172             while (revlog.hasNext()) {
173                 commits.add(revlog.next());
174             }
175
176             // release the revwalk
177             rw.release();
178
179             if (commits.size() == 0) {
180                 LOGGER.warn("Failed to find changes for issue " + issuePath);
181                 continue;
182             }
183
184             // sort by commit order, first commit first
185             Collections.reverse(commits);
186
187             StringBuilder sb = new StringBuilder("[");
188             boolean first = true;
189             for (RevCommit commit : commits) {
190                 if (!first) {
191                     sb.append(',');
192                 }
193                 String message = commit.getFullMessage();
194                 // commit message is formatted: C ISSUEID\n\nJSON
195                 // C is an single char commit code
196                 // ISSUEID is an SHA-1 hash
197                 String json = message.substring(43);
198                 sb.append(json);
199                 first = false;
200             }
201             sb.append(']');
202
203             // Deserialize the JSON array as a Collection<Change>, this seems
204             // slightly faster than deserializing each change by itself.
205             Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
206                     new TypeToken<Collection<Change>>() {
207                     }.getType());
208
209             // create an issue object form the changes
210             IssueModel issue = buildIssue(changes, true);
211
212             // add the issue, conditionally, to the list
0f43a5 213             if (filter == null) {
JM 214                 list.add(issue);
215             } else {
216                 if (filter.accept(issue)) {
217                     list.add(issue);
218                 }
219             }
220         }
69a559 221
JM 222         // sort the issues by creation
0f43a5 223         Collections.sort(list);
JM 224         return list;
225     }
226
227     /**
69a559 228      * Retrieves the specified issue from the repository with all changes
JM 229      * applied to build the effective issue.
0f43a5 230      * 
JM 231      * @param repository
232      * @param issueId
233      * @return an issue, if it exists, otherwise null
234      */
235     public static IssueModel getIssue(Repository repository, String issueId) {
69a559 236         return getIssue(repository, issueId, true);
JM 237     }
238
239     /**
240      * Retrieves the specified issue from the repository.
241      * 
242      * @param repository
243      * @param issueId
244      * @param effective
245      *            if true, the effective issue is built by processing comment
246      *            changes, deletions, etc. if false, the raw issue is built
247      *            without consideration for comment changes, deletions, etc.
248      * @return an issue, if it exists, otherwise null
249      */
250     public static IssueModel getIssue(Repository repository, String issueId, boolean effective) {
0f43a5 251         RefModel issuesBranch = getIssuesBranch(repository);
JM 252         if (issuesBranch == null) {
253             return null;
254         }
255
256         if (StringUtils.isEmpty(issueId)) {
257             return null;
258         }
259
260         String issuePath = getIssuePath(issueId);
69a559 261
JM 262         // Collect all changes as JSON array from commit messages
263         List<RevCommit> commits = JGitUtils.getRevLog(repository, GB_ISSUES, issuePath, 0, -1);
264
265         // sort by commit order, first commit first
266         Collections.reverse(commits);
267
268         StringBuilder sb = new StringBuilder("[");
269         boolean first = true;
270         for (RevCommit commit : commits) {
271             if (!first) {
272                 sb.append(',');
273             }
274             String message = commit.getFullMessage();
275             // commit message is formatted: C ISSUEID\n\nJSON
276             // C is an single char commit code
277             // ISSUEID is an SHA-1 hash
278             String json = message.substring(43);
279             sb.append(json);
280             first = false;
281         }
282         sb.append(']');
283
284         // Deserialize the JSON array as a Collection<Change>, this seems
285         // slightly faster than deserializing each change by itself.
286         Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
287                 new TypeToken<Collection<Change>>() {
288                 }.getType());
289
290         // create an issue object and apply the changes to it
291         IssueModel issue = buildIssue(changes, effective);
292         return issue;
293     }
294
295     /**
296      * Builds an issue from a set of changes.
297      * 
298      * @param changes
299      * @param effective
300      *            if true, the effective issue is built which accounts for
301      *            comment changes, comment deletions, etc. if false, the raw
302      *            issue is built.
303      * @return an issue
304      */
305     private static IssueModel buildIssue(Collection<Change> changes, boolean effective) {
306         IssueModel issue;
307         if (effective) {
308             List<Change> effectiveChanges = new ArrayList<Change>();
309             Map<String, Change> comments = new HashMap<String, Change>();
310             for (Change change : changes) {
311                 if (change.comment != null) {
312                     if (comments.containsKey(change.comment.id)) {
313                         Change original = comments.get(change.comment.id);
314                         Change clone = DeepCopier.copy(original);
315                         clone.comment.text = change.comment.text;
316                         clone.comment.deleted = change.comment.deleted;
317                         int idx = effectiveChanges.indexOf(original);
318                         effectiveChanges.remove(original);
319                         effectiveChanges.add(idx, clone);
320                         comments.put(clone.comment.id, clone);
321                     } else {
322                         effectiveChanges.add(change);
323                         comments.put(change.comment.id, change);
324                     }
325                 } else {
326                     effectiveChanges.add(change);
327                 }
328             }
329
330             // effective issue
331             issue = new IssueModel();
332             for (Change change : effectiveChanges) {
333                 issue.applyChange(change);
334             }
335         } else {
336             // raw issue
337             issue = new IssueModel();
338             for (Change change : changes) {
339                 issue.applyChange(change);
340             }
341         }
0f43a5 342         return issue;
JM 343     }
344
345     /**
346      * Retrieves the specified attachment from an issue.
347      * 
348      * @param repository
349      * @param issueId
350      * @param filename
351      * @return an attachment, if found, null otherwise
352      */
353     public static Attachment getIssueAttachment(Repository repository, String issueId,
354             String filename) {
355         RefModel issuesBranch = getIssuesBranch(repository);
356         if (issuesBranch == null) {
357             return null;
358         }
359
360         if (StringUtils.isEmpty(issueId)) {
361             return null;
362         }
363
364         // deserialize the issue model so that we have the attachment metadata
69a559 365         IssueModel issue = getIssue(repository, issueId, true);
0f43a5 366         Attachment attachment = issue.getAttachment(filename);
JM 367
368         // attachment not found
369         if (attachment == null) {
370             return null;
371         }
372
373         // retrieve the attachment content
69a559 374         String issuePath = getIssuePath(issueId);
JM 375         RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();
376         byte[] content = JGitUtils
377                 .getByteContent(repository, tree, issuePath + "/" + attachment.id);
0f43a5 378         attachment.content = content;
JM 379         attachment.size = content.length;
380         return attachment;
381     }
382
383     /**
69a559 384      * Creates an issue in the gb-issues branch of the repository. The branch is
JM 385      * automatically created if it does not already exist. Your change must
386      * include an author, summary, and description, at a minimum. If your change
387      * does not have those minimum requirements a RuntimeException will be
388      * thrown.
0f43a5 389      * 
JM 390      * @param repository
391      * @param change
392      * @return true if successful
393      */
394     public static IssueModel createIssue(Repository repository, Change change) {
395         RefModel issuesBranch = getIssuesBranch(repository);
396         if (issuesBranch == null) {
397             JGitUtils.createOrphanBranch(repository, "gb-issues", null);
398         }
399
69a559 400         if (StringUtils.isEmpty(change.author)) {
JM 401             throw new RuntimeException("Must specify a change author!");
0f43a5 402         }
69a559 403         if (!change.hasField(Field.Summary)) {
JM 404             throw new RuntimeException("Must specify a summary!");
0f43a5 405         }
69a559 406         if (!change.hasField(Field.Description)) {
JM 407             throw new RuntimeException("Must specify a description!");
0f43a5 408         }
JM 409
69a559 410         change.setField(Field.Reporter, change.author);
0f43a5 411
69a559 412         String issueId = StringUtils.getSHA1(change.created.toString() + change.author
JM 413                 + change.getString(Field.Summary) + change.getField(Field.Description));
414         change.setField(Field.Id, issueId);
415         change.code = '+';
416
417         boolean success = commit(repository, issueId, change);
0f43a5 418         if (success) {
69a559 419             return getIssue(repository, issueId, false);
0f43a5 420         }
JM 421         return null;
422     }
423
424     /**
425      * Updates an issue in the gb-issues branch of the repository.
426      * 
427      * @param repository
98b4ed 428      * @param issueId
0f43a5 429      * @param change
JM 430      * @return true if successful
431      */
432     public static boolean updateIssue(Repository repository, String issueId, Change change) {
433         boolean success = false;
434         RefModel issuesBranch = getIssuesBranch(repository);
435
436         if (issuesBranch == null) {
437             throw new RuntimeException("gb-issues branch does not exist!");
438         }
439
440         if (change == null) {
441             throw new RuntimeException("change can not be null!");
442         }
443
444         if (StringUtils.isEmpty(change.author)) {
69a559 445             throw new RuntimeException("must specify a change author!");
0f43a5 446         }
JM 447
69a559 448         // determine update code
JM 449         // default update code is '=' for a general change
450         change.code = '=';
451         if (change.hasField(Field.Status)) {
452             Status status = Status.fromObject(change.getField(Field.Status));
453             if (status.isClosed()) {
454                 // someone closed the issue
455                 change.code = 'x';
456             }
457         }
458         success = commit(repository, issueId, change);
0f43a5 459         return success;
JM 460     }
461
462     /**
69a559 463      * Deletes an issue from the repository.
0f43a5 464      * 
JM 465      * @param repository
69a559 466      * @param issueId
JM 467      * @return true if successful
0f43a5 468      */
69a559 469     public static boolean deleteIssue(Repository repository, String issueId, String author) {
0f43a5 470         boolean success = false;
69a559 471         RefModel issuesBranch = getIssuesBranch(repository);
JM 472
473         if (issuesBranch == null) {
474             throw new RuntimeException("gb-issues branch does not exist!");
475         }
476
477         if (StringUtils.isEmpty(issueId)) {
478             throw new RuntimeException("must specify an issue id!");
479         }
480
481         String issuePath = getIssuePath(issueId);
482
483         String message = "- " + issueId;
0f43a5 484         try {
JM 485             ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
486             ObjectInserter odi = repository.newObjectInserter();
487             try {
69a559 488                 // Create the in-memory index of the new/updated issue
JM 489                 DirCache index = DirCache.newInCore();
490                 DirCacheBuilder dcBuilder = index.builder();
491                 // Traverse HEAD to add all other paths
492                 TreeWalk treeWalk = new TreeWalk(repository);
493                 int hIdx = -1;
494                 if (headId != null)
495                     hIdx = treeWalk.addTree(new RevWalk(repository).parseTree(headId));
496                 treeWalk.setRecursive(true);
497                 while (treeWalk.next()) {
498                     String path = treeWalk.getPathString();
499                     CanonicalTreeParser hTree = null;
500                     if (hIdx != -1)
501                         hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
502                     if (!path.startsWith(issuePath)) {
503                         // add entries from HEAD for all other paths
504                         if (hTree != null) {
505                             // create a new DirCacheEntry with data retrieved
506                             // from HEAD
507                             final DirCacheEntry dcEntry = new DirCacheEntry(path);
508                             dcEntry.setObjectId(hTree.getEntryObjectId());
509                             dcEntry.setFileMode(hTree.getEntryFileMode());
510
511                             // add to temporary in-core index
512                             dcBuilder.add(dcEntry);
513                         }
514                     }
515                 }
516
517                 // release the treewalk
518                 treeWalk.release();
519
520                 // finish temporary in-core index used for this commit
521                 dcBuilder.finish();
522
0f43a5 523                 ObjectId indexTreeId = index.writeTree(odi);
JM 524
525                 // Create a commit object
69a559 526                 PersonIdent ident = new PersonIdent(author, "gitblit@localhost");
0f43a5 527                 CommitBuilder commit = new CommitBuilder();
69a559 528                 commit.setAuthor(ident);
JM 529                 commit.setCommitter(ident);
0f43a5 530                 commit.setEncoding(Constants.CHARACTER_ENCODING);
69a559 531                 commit.setMessage(message);
0f43a5 532                 commit.setParentId(headId);
JM 533                 commit.setTreeId(indexTreeId);
534
535                 // Insert the commit into the repository
536                 ObjectId commitId = odi.insert(commit);
537                 odi.flush();
538
539                 RevWalk revWalk = new RevWalk(repository);
540                 try {
541                     RevCommit revCommit = revWalk.parseCommit(commitId);
542                     RefUpdate ru = repository.updateRef(GB_ISSUES);
543                     ru.setNewObjectId(commitId);
544                     ru.setExpectedOldObjectId(headId);
545                     ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
546                     Result rc = ru.forceUpdate();
547                     switch (rc) {
548                     case NEW:
549                     case FORCED:
550                     case FAST_FORWARD:
551                         success = true;
552                         break;
553                     case REJECTED:
554                     case LOCK_FAILURE:
555                         throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
556                                 ru.getRef(), rc);
557                     default:
558                         throw new JGitInternalException(MessageFormat.format(
559                                 JGitText.get().updatingRefFailed, GB_ISSUES, commitId.toString(),
560                                 rc));
561                     }
562                 } finally {
563                     revWalk.release();
564                 }
565             } finally {
566                 odi.release();
567             }
568         } catch (Throwable t) {
69a559 569             error(t, repository, "Failed to delete issue {1} to {0}", issueId);
0f43a5 570         }
JM 571         return success;
572     }
573
69a559 574     /**
JM 575      * Changes the text of an issue comment.
576      * 
577      * @param repository
578      * @param issue
579      * @param change
580      *            the change with the comment to change
581      * @param author
582      *            the author of the revision
583      * @param comment
584      *            the revised comment
585      * @return true, if the change was successful
586      */
587     public static boolean changeComment(Repository repository, IssueModel issue, Change change,
588             String author, String comment) {
589         Change revision = new Change(author);
590         revision.comment(comment);
591         revision.comment.id = change.comment.id;
592         return updateIssue(repository, issue.id, revision);
593     }
594
595     /**
596      * Deletes a comment from an issue.
597      * 
598      * @param repository
599      * @param issue
600      * @param change
601      *            the change with the comment to delete
602      * @param author
603      * @return true, if the deletion was successful
604      */
605     public static boolean deleteComment(Repository repository, IssueModel issue, Change change,
606             String author) {
607         Change deletion = new Change(author);
608         deletion.comment(change.comment.text);
609         deletion.comment.id = change.comment.id;
610         deletion.comment.deleted = true;
611         return updateIssue(repository, issue.id, deletion);
612     }
613
614     /**
615      * Commit a change to the repository. Each issue is composed on changes.
616      * Issues are built from applying the changes in the order they were
617      * committed to the repository. The changes are actually specified in the
618      * commit messages and not in the RevTrees which allows for clean,
619      * distributed merging.
620      * 
621      * @param repository
98b4ed 622      * @param issueId
69a559 623      * @param change
JM 624      * @return true, if the change was committed
625      */
626     private static boolean commit(Repository repository, String issueId, Change change) {
627         boolean success = false;
628
0f43a5 629         try {
69a559 630             // assign ids to new attachments
JM 631             // attachments are stored by an SHA1 id
632             if (change.hasAttachments()) {
633                 for (Attachment attachment : change.attachments) {
634                     if (!ArrayUtils.isEmpty(attachment.content)) {
635                         byte[] prefix = (change.created.toString() + change.author).getBytes();
636                         byte[] bytes = new byte[prefix.length + attachment.content.length];
637                         System.arraycopy(prefix, 0, bytes, 0, prefix.length);
638                         System.arraycopy(attachment.content, 0, bytes, prefix.length,
639                                 attachment.content.length);
640                         attachment.id = "attachment-" + StringUtils.getSHA1(bytes);
641                     }
642                 }
643             }
644
645             // serialize the change as json
646             // exclude any attachment from json serialization
0f43a5 647             Gson gson = JsonUtils.gson(new ExcludeField(
JM 648                     "com.gitblit.models.IssueModel$Attachment.content"));
69a559 649             String json = gson.toJson(change);
JM 650
651             // include the json change in the commit message
652             String issuePath = getIssuePath(issueId);
653             String message = change.code + " " + issueId + "\n\n" + json;
654
655             // Create a commit file. This is required for a proper commit and
656             // ensures we can retrieve the commit log of the issue path.
657             //
658             // This file is NOT serialized as part of the Change object.
659             switch (change.code) {
660             case '+': {
661                 // New Issue.
662                 Attachment placeholder = new Attachment("issue");
663                 placeholder.id = placeholder.name;
664                 placeholder.content = "DO NOT REMOVE".getBytes(Constants.CHARACTER_ENCODING);
665                 change.addAttachment(placeholder);
666                 break;
667             }
668             default: {
669                 // Update Issue.
670                 String changeId = StringUtils.getSHA1(json);
671                 Attachment placeholder = new Attachment("change-" + changeId);
672                 placeholder.id = placeholder.name;
673                 placeholder.content = "REMOVABLE".getBytes(Constants.CHARACTER_ENCODING);
674                 change.addAttachment(placeholder);
675                 break;
676             }
677             }
678
679             ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
680             ObjectInserter odi = repository.newObjectInserter();
681             try {
682                 // Create the in-memory index of the new/updated issue
683                 DirCache index = createIndex(repository, headId, issuePath, change);
684                 ObjectId indexTreeId = index.writeTree(odi);
685
686                 // Create a commit object
687                 PersonIdent ident = new PersonIdent(change.author, "gitblit@localhost");
688                 CommitBuilder commit = new CommitBuilder();
689                 commit.setAuthor(ident);
690                 commit.setCommitter(ident);
691                 commit.setEncoding(Constants.CHARACTER_ENCODING);
692                 commit.setMessage(message);
693                 commit.setParentId(headId);
694                 commit.setTreeId(indexTreeId);
695
696                 // Insert the commit into the repository
697                 ObjectId commitId = odi.insert(commit);
698                 odi.flush();
699
700                 RevWalk revWalk = new RevWalk(repository);
701                 try {
702                     RevCommit revCommit = revWalk.parseCommit(commitId);
703                     RefUpdate ru = repository.updateRef(GB_ISSUES);
704                     ru.setNewObjectId(commitId);
705                     ru.setExpectedOldObjectId(headId);
706                     ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
707                     Result rc = ru.forceUpdate();
708                     switch (rc) {
709                     case NEW:
710                     case FORCED:
711                     case FAST_FORWARD:
712                         success = true;
713                         break;
714                     case REJECTED:
715                     case LOCK_FAILURE:
716                         throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
717                                 ru.getRef(), rc);
718                     default:
719                         throw new JGitInternalException(MessageFormat.format(
720                                 JGitText.get().updatingRefFailed, GB_ISSUES, commitId.toString(),
721                                 rc));
722                     }
723                 } finally {
724                     revWalk.release();
725                 }
726             } finally {
727                 odi.release();
728             }
0f43a5 729         } catch (Throwable t) {
69a559 730             error(t, repository, "Failed to commit issue {1} to {0}", issueId);
0f43a5 731         }
69a559 732         return success;
0f43a5 733     }
JM 734
735     /**
736      * Returns the issue path. This follows the same scheme as Git's object
737      * store path where the first two characters of the hash id are the root
738      * folder with the remaining characters as a subfolder within that folder.
739      * 
740      * @param issueId
741      * @return the root path of the issue content on the gb-issues branch
742      */
06ff61 743     static String getIssuePath(String issueId) {
0f43a5 744         return issueId.substring(0, 2) + "/" + issueId.substring(2);
JM 745     }
746
747     /**
748      * Creates an in-memory index of the issue change.
749      * 
750      * @param repo
751      * @param headId
69a559 752      * @param change
0f43a5 753      * @return an in-memory index
JM 754      * @throws IOException
755      */
69a559 756     private static DirCache createIndex(Repository repo, ObjectId headId, String issuePath,
JM 757             Change change) throws IOException {
0f43a5 758
JM 759         DirCache inCoreIndex = DirCache.newInCore();
760         DirCacheBuilder dcBuilder = inCoreIndex.builder();
761         ObjectInserter inserter = repo.newObjectInserter();
762
69a559 763         Set<String> ignorePaths = new TreeSet<String>();
0f43a5 764         try {
69a559 765             // Add any attachments to the temporary index
JM 766             if (change.hasAttachments()) {
767                 for (Attachment attachment : change.attachments) {
768                     // build a path name for the attachment and mark as ignored
769                     String path = issuePath + "/" + attachment.id;
770                     ignorePaths.add(path);
0f43a5 771
69a559 772                     // create an index entry for this attachment
JM 773                     final DirCacheEntry dcEntry = new DirCacheEntry(path);
774                     dcEntry.setLength(attachment.content.length);
775                     dcEntry.setLastModified(change.created.getTime());
776                     dcEntry.setFileMode(FileMode.REGULAR_FILE);
0f43a5 777
69a559 778                     // insert object
JM 779                     dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, attachment.content));
780
781                     // add to temporary in-core index
782                     dcBuilder.add(dcEntry);
783                 }
0f43a5 784             }
JM 785
786             // Traverse HEAD to add all other paths
787             TreeWalk treeWalk = new TreeWalk(repo);
788             int hIdx = -1;
789             if (headId != null)
790                 hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
791             treeWalk.setRecursive(true);
792
793             while (treeWalk.next()) {
794                 String path = treeWalk.getPathString();
795                 CanonicalTreeParser hTree = null;
796                 if (hIdx != -1)
797                     hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
69a559 798                 if (!ignorePaths.contains(path)) {
0f43a5 799                     // add entries from HEAD for all other paths
JM 800                     if (hTree != null) {
801                         // create a new DirCacheEntry with data retrieved from
802                         // HEAD
803                         final DirCacheEntry dcEntry = new DirCacheEntry(path);
804                         dcEntry.setObjectId(hTree.getEntryObjectId());
805                         dcEntry.setFileMode(hTree.getEntryFileMode());
806
807                         // add to temporary in-core index
808                         dcBuilder.add(dcEntry);
809                     }
810                 }
811             }
812
813             // release the treewalk
814             treeWalk.release();
815
816             // finish temporary in-core index used for this commit
817             dcBuilder.finish();
818         } finally {
819             inserter.release();
820         }
821         return inCoreIndex;
822     }
69a559 823 }