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 |
} |