1 /*
2 * Copyright (C) 2011, Google Inc.
3 * and other copyright owners as documented in the project's IP log.
4 *
5 * This program and the accompanying materials are made available
6 * under the terms of the Eclipse Distribution License v1.0 which
7 * accompanies this distribution, is reproduced below, and is
8 * available at http://www.eclipse.org/org/documents/edl-v10.php
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
14 * conditions are met:
15 *
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 *
19 * - Redistributions in binary form must reproduce the above
20 * copyright notice, this list of conditions and the following
21 * disclaimer in the documentation and/or other materials provided
22 * with the distribution.
23 *
24 * - Neither the name of the Eclipse Foundation, Inc. nor the
25 * names of its contributors may be used to endorse or promote
26 * products derived from this software without specific prior
27 * written permission.
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 */
43
44 package org.eclipse.jgit.blame;
45
46 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
47 import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
48 import static org.eclipse.jgit.lib.FileMode.TYPE_MASK;
49
50 import java.io.IOException;
51 import java.util.Collection;
52 import java.util.Collections;
53
54 import org.eclipse.jgit.blame.Candidate.BlobCandidate;
55 import org.eclipse.jgit.blame.Candidate.ReverseCandidate;
56 import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
57 import org.eclipse.jgit.diff.DiffAlgorithm;
58 import org.eclipse.jgit.diff.DiffEntry;
59 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
60 import org.eclipse.jgit.diff.EditList;
61 import org.eclipse.jgit.diff.HistogramDiff;
62 import org.eclipse.jgit.diff.RawText;
63 import org.eclipse.jgit.diff.RawTextComparator;
64 import org.eclipse.jgit.diff.RenameDetector;
65 import org.eclipse.jgit.internal.JGitText;
66 import org.eclipse.jgit.lib.AnyObjectId;
67 import org.eclipse.jgit.lib.MutableObjectId;
68 import org.eclipse.jgit.lib.ObjectId;
69 import org.eclipse.jgit.lib.ObjectLoader;
70 import org.eclipse.jgit.lib.ObjectReader;
71 import org.eclipse.jgit.lib.PersonIdent;
72 import org.eclipse.jgit.lib.Repository;
73 import org.eclipse.jgit.revwalk.RevCommit;
74 import org.eclipse.jgit.revwalk.RevFlag;
75 import org.eclipse.jgit.revwalk.RevWalk;
76 import org.eclipse.jgit.treewalk.TreeWalk;
77 import org.eclipse.jgit.treewalk.filter.PathFilter;
78 import org.eclipse.jgit.treewalk.filter.TreeFilter;
79
80 /**
81 * Generate author information for lines based on a provided file.
82 * <p>
83 * Applications that want a simple one-shot computation of blame for a file
84 * should use {@link #computeBlameResult()} to prepare the entire result in one
85 * method call. This may block for significant time as the history of the
86 * repository must be traversed until information is gathered for every line.
87 * <p>
88 * Applications that want more incremental update behavior may use either the
89 * raw {@link #next()} streaming approach supported by this class, or construct
90 * a {@link BlameResult} using {@link BlameResult#create(BlameGenerator)} and
91 * incrementally construct the result with {@link BlameResult#computeNext()}.
92 * <p>
93 * This class is not thread-safe.
94 * <p>
95 * An instance of BlameGenerator can only be used once. To blame multiple files
96 * the application must create a new BlameGenerator.
97 * <p>
98 * During blame processing there are two files involved:
99 * <ul>
100 * <li>result - The file whose lines are being examined. This is the revision
101 * the user is trying to view blame/annotation information alongside of.</li>
102 * <li>source - The file that was blamed with supplying one or more lines of
103 * data into result. The source may be a different file path (due to copy or
104 * rename). Source line numbers may differ from result line numbers due to lines
105 * being added/removed in intermediate revisions.</li>
106 * </ul>
107 * <p>
108 * The blame algorithm is implemented by initially assigning responsibility for
109 * all lines of the result to the starting commit. A difference against the
110 * commit's ancestor is computed, and responsibility is passed to the ancestor
111 * commit for any lines that are common. The starting commit is blamed only for
112 * the lines that do not appear in the ancestor, if any. The loop repeats using
113 * the ancestor, until there are no more lines to acquire information on, or the
114 * file's creation point is discovered in history.
115 */
116 public class BlameGenerator implements AutoCloseable {
117 private final Repository repository;
118
119 private final PathFilter resultPath;
120
121 private final MutableObjectId idBuf;
122
123 /** Revision pool used to acquire commits from. */
124 private RevWalk revPool;
125
126 /** Indicates the commit was put into the queue at least once. */
127 private RevFlag SEEN;
128
129 private ObjectReader reader;
130
131 private TreeWalk treeWalk;
132
133 private DiffAlgorithm diffAlgorithm = new HistogramDiff();
134
135 private RawTextComparator textComparator = RawTextComparator.DEFAULT;
136
137 private RenameDetector renameDetector;
138
139 /** Potential candidates, sorted by commit time descending. */
140 private Candidate queue;
141
142 /** Number of lines that still need to be discovered. */
143 private int remaining;
144
145 /** Blame is currently assigned to this source. */
146 private Candidate outCandidate;
147 private Region outRegion;
148
149 /**
150 * Create a blame generator for the repository and path (relative to
151 * repository)
152 *
153 * @param repository
154 * repository to access revision data from.
155 * @param path
156 * initial path of the file to start scanning (relative to the
157 * repository).
158 */
159 public BlameGenerator(Repository repository, String path) {
160 this.repository = repository;
161 this.resultPath = PathFilter.create(path);
162
163 idBuf = new MutableObjectId();
164 setFollowFileRenames(true);
165 initRevPool(false);
166
167 remaining = -1;
168 }
169
170 private void initRevPool(boolean reverse) {
171 if (queue != null)
172 throw new IllegalStateException();
173
174 if (revPool != null)
175 revPool.close();
176
177 if (reverse)
178 revPool = new ReverseWalk(getRepository());
179 else
180 revPool = new RevWalk(getRepository());
181
182 SEEN = revPool.newFlag("SEEN"); //$NON-NLS-1$
183 reader = revPool.getObjectReader();
184 treeWalk = new TreeWalk(reader);
185 treeWalk.setRecursive(true);
186 }
187
188 /** @return repository being scanned for revision history. */
189 public Repository getRepository() {
190 return repository;
191 }
192
193 /** @return path file path being processed. */
194 public String getResultPath() {
195 return resultPath.getPath();
196 }
197
198 /**
199 * Difference algorithm to use when comparing revisions.
200 *
201 * @param algorithm
202 * @return {@code this}
203 */
204 public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) {
205 diffAlgorithm = algorithm;
206 return this;
207 }
208
209 /**
210 * Text comparator to use when comparing revisions.
211 *
212 * @param comparator
213 * @return {@code this}
214 */
215 public BlameGenerator setTextComparator(RawTextComparator comparator) {
216 textComparator = comparator;
217 return this;
218 }
219
220 /**
221 * Enable (or disable) following file renames, on by default.
222 * <p>
223 * If true renames are followed using the standard FollowFilter behavior
224 * used by RevWalk (which matches {@code git log --follow} in the C
225 * implementation). This is not the same as copy/move detection as
226 * implemented by the C implementation's of {@code git blame -M -C}.
227 *
228 * @param follow
229 * enable following.
230 * @return {@code this}
231 */
232 public BlameGenerator setFollowFileRenames(boolean follow) {
233 if (follow)
234 renameDetector = new RenameDetector(getRepository());
235 else
236 renameDetector = null;
237 return this;
238 }
239
240 /**
241 * Obtain the RenameDetector if {@code setFollowFileRenames(true)}.
242 *
243 * @return the rename detector, allowing the application to configure its
244 * settings for rename score and breaking behavior.
245 */
246 public RenameDetector getRenameDetector() {
247 return renameDetector;
248 }
249
250 /**
251 * Push a candidate blob onto the generator's traversal stack.
252 * <p>
253 * Candidates should be pushed in history order from oldest-to-newest.
254 * Applications should push the starting commit first, then the index
255 * revision (if the index is interesting), and finally the working tree
256 * copy (if the working tree is interesting).
257 *
258 * @param description
259 * description of the blob revision, such as "Working Tree".
260 * @param contents
261 * contents of the file.
262 * @return {@code this}
263 * @throws IOException
264 * the repository cannot be read.
265 */
266 public BlameGenerator push(String description, byte[] contents)
267 throws IOException {
268 return push(description, new RawText(contents));
269 }
270
271 /**
272 * Push a candidate blob onto the generator's traversal stack.
273 * <p>
274 * Candidates should be pushed in history order from oldest-to-newest.
275 * Applications should push the starting commit first, then the index
276 * revision (if the index is interesting), and finally the working tree copy
277 * (if the working tree is interesting).
278 *
279 * @param description
280 * description of the blob revision, such as "Working Tree".
281 * @param contents
282 * contents of the file.
283 * @return {@code this}
284 * @throws IOException
285 * the repository cannot be read.
286 */
287 public BlameGenerator push(String description, RawText contents)
288 throws IOException {
289 if (description == null)
290 description = JGitText.get().blameNotCommittedYet;
291 BlobCandidate c = new BlobCandidate(description, resultPath);
292 c.sourceText = contents;
293 c.regionList = new Region(0, 0, contents.size());
294 remaining = contents.size();
295 push(c);
296 return this;
297 }
298
299 /**
300 * Push a candidate object onto the generator's traversal stack.
301 * <p>
302 * Candidates should be pushed in history order from oldest-to-newest.
303 * Applications should push the starting commit first, then the index
304 * revision (if the index is interesting), and finally the working tree copy
305 * (if the working tree is interesting).
306 *
307 * @param description
308 * description of the blob revision, such as "Working Tree".
309 * @param id
310 * may be a commit or a blob.
311 * @return {@code this}
312 * @throws IOException
313 * the repository cannot be read.
314 */
315 public BlameGenerator push(String description, AnyObjectId id)
316 throws IOException {
317 ObjectLoader ldr = reader.open(id);
318 if (ldr.getType() == OBJ_BLOB) {
319 if (description == null)
320 description = JGitText.get().blameNotCommittedYet;
321 BlobCandidate c = new BlobCandidate(description, resultPath);
322 c.sourceBlob = id.toObjectId();
323 c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
324 c.regionList = new Region(0, 0, c.sourceText.size());
325 remaining = c.sourceText.size();
326 push(c);
327 return this;
328 }
329
330 RevCommit commit = revPool.parseCommit(id);
331 if (!find(commit, resultPath))
332 return this;
333
334 Candidate c = new Candidate(commit, resultPath);
335 c.sourceBlob = idBuf.toObjectId();
336 c.loadText(reader);
337 c.regionList = new Region(0, 0, c.sourceText.size());
338 remaining = c.sourceText.size();
339 push(c);
340 return this;
341 }
342
343 /**
344 * Configure the generator to compute reverse blame (history of deletes).
345 * <p>
346 * This method is expensive as it immediately runs a RevWalk over the
347 * history spanning the expression {@code start..end} (end being more recent
348 * than start) and then performs the equivalent operation as
349 * {@link #push(String, AnyObjectId)} to begin blame traversal from the
350 * commit named by {@code start} walking forwards through history until
351 * {@code end} blaming line deletions.
352 * <p>
353 * A reverse blame may produce multiple sources for the same result line,
354 * each of these is a descendant commit that removed the line, typically
355 * this occurs when the same deletion appears in multiple side branches such
356 * as due to a cherry-pick. Applications relying on reverse should use
357 * {@link BlameResult} as it filters these duplicate sources and only
358 * remembers the first (oldest) deletion.
359 *
360 * @param start
361 * oldest commit to traverse from. The result file will be loaded
362 * from this commit's tree.
363 * @param end
364 * most recent commit to stop traversal at. Usually an active
365 * branch tip, tag, or HEAD.
366 * @return {@code this}
367 * @throws IOException
368 * the repository cannot be read.
369 */
370 public BlameGenerator reverse(AnyObjectId start, AnyObjectId end)
371 throws IOException {
372 return reverse(start, Collections.singleton(end.toObjectId()));
373 }
374
375 /**
376 * Configure the generator to compute reverse blame (history of deletes).
377 * <p>
378 * This method is expensive as it immediately runs a RevWalk over the
379 * history spanning the expression {@code start..end} (end being more recent
380 * than start) and then performs the equivalent operation as
381 * {@link #push(String, AnyObjectId)} to begin blame traversal from the
382 * commit named by {@code start} walking forwards through history until
383 * {@code end} blaming line deletions.
384 * <p>
385 * A reverse blame may produce multiple sources for the same result line,
386 * each of these is a descendant commit that removed the line, typically
387 * this occurs when the same deletion appears in multiple side branches such
388 * as due to a cherry-pick. Applications relying on reverse should use
389 * {@link BlameResult} as it filters these duplicate sources and only
390 * remembers the first (oldest) deletion.
391 *
392 * @param start
393 * oldest commit to traverse from. The result file will be loaded
394 * from this commit's tree.
395 * @param end
396 * most recent commits to stop traversal at. Usually an active
397 * branch tip, tag, or HEAD.
398 * @return {@code this}
399 * @throws IOException
400 * the repository cannot be read.
401 */
402 public BlameGenerator reverse(AnyObjectId start,
403 Collection<? extends ObjectId> end) throws IOException {
404 initRevPool(true);
405
406 ReverseCommit result = (ReverseCommit) revPool.parseCommit(start);
407 if (!find(result, resultPath))
408 return this;
409
410 revPool.markUninteresting(result);
411 for (ObjectId id : end)
412 revPool.markStart(revPool.parseCommit(id));
413
414 while (revPool.next() != null) {
415 // just pump the queue
416 }
417
418 ReverseCandidate c = new ReverseCandidate(result, resultPath);
419 c.sourceBlob = idBuf.toObjectId();
420 c.loadText(reader);
421 c.regionList = new Region(0, 0, c.sourceText.size());
422 remaining = c.sourceText.size();
423 push(c);
424 return this;
425 }
426
427 /**
428 * Allocate a new RevFlag for use by the caller.
429 *
430 * @param name
431 * unique name of the flag in the blame context.
432 * @return the newly allocated flag.
433 * @since 3.4
434 */
435 public RevFlag newFlag(String name) {
436 return revPool.newFlag(name);
437 }
438
439 /**
440 * Execute the generator in a blocking fashion until all data is ready.
441 *
442 * @return the complete result. Null if no file exists for the given path.
443 * @throws IOException
444 * the repository cannot be read.
445 */
446 public BlameResult computeBlameResult() throws IOException {
447 try {
448 BlameResult r = BlameResult.create(this);
449 if (r != null)
450 r.computeAll();
451 return r;
452 } finally {
453 close();
454 }
455 }
456
457 /**
458 * Step the blame algorithm one iteration.
459 *
460 * @return true if the generator has found a region's source. The getSource*
461 * and {@link #getResultStart()}, {@link #getResultEnd()} methods
462 * can be used to inspect the region found. False if there are no
463 * more regions to describe.
464 * @throws IOException
465 * repository cannot be read.
466 */
467 public boolean next() throws IOException {
468 // If there is a source still pending, produce the next region.
469 if (outRegion != null) {
470 Region r = outRegion;
471 remaining -= r.length;
472 if (r.next != null) {
473 outRegion = r.next;
474 return true;
475 }
476
477 if (outCandidate.queueNext != null)
478 return result(outCandidate.queueNext);
479
480 outCandidate = null;
481 outRegion = null;
482 }
483
484 // If there are no lines remaining, the entire result is done,
485 // even if there are revisions still available for the path.
486 if (remaining == 0)
487 return done();
488
489 for (;;) {
490 Candidate n = pop();
491 if (n == null)
492 return done();
493
494 int pCnt = n.getParentCount();
495 if (pCnt == 1) {
496 if (processOne(n))
497 return true;
498
499 } else if (1 < pCnt) {
500 if (processMerge(n))
501 return true;
502
503 } else if (n instanceof ReverseCandidate) {
504 // Do not generate a tip of a reverse. The region
505 // survives and should not appear to be deleted.
506
507 } else /* if (pCnt == 0) */{
508 // Root commit, with at least one surviving region.
509 // Assign the remaining blame here.
510 return result(n);
511 }
512 }
513 }
514
515 private boolean done() {
516 close();
517 return false;
518 }
519
520 private boolean result(Candidate n) throws IOException {
521 n.beginResult(revPool);
522 outCandidate = n;
523 outRegion = n.regionList;
524 return true;
525 }
526
527 private boolean reverseResult(Candidate parent, Candidate source)
528 throws IOException {
529 // On a reverse blame present the application the parent
530 // (as this is what did the removals), however the region
531 // list to enumerate is the source's surviving list.
532 Candidate res = parent.copy(parent.sourceCommit);
533 res.regionList = source.regionList;
534 return result(res);
535 }
536
537 private Candidate pop() {
538 Candidate n = queue;
539 if (n != null) {
540 queue = n.queueNext;
541 n.queueNext = null;
542 }
543 return n;
544 }
545
546 private void push(BlobCandidate toInsert) {
547 Candidate c = queue;
548 if (c != null) {
549 c.remove(SEEN); // will be pushed by toInsert
550 c.regionList = null;
551 toInsert.parent = c;
552 }
553 queue = toInsert;
554 }
555
556 private void push(Candidate toInsert) {
557 if (toInsert.has(SEEN)) {
558 // We have already added a Candidate for this commit to the queue,
559 // this can happen if the commit is a merge base for two or more
560 // parallel branches that were merged together.
561 //
562 // It is likely the candidate was not yet processed. The queue
563 // sorts descending by commit time and usually descendant commits
564 // have higher timestamps than the ancestors.
565 //
566 // Find the existing candidate and merge the new candidate's
567 // region list into it.
568 for (Candidate p = queue; p != null; p = p.queueNext) {
569 if (p.canMergeRegions(toInsert)) {
570 p.mergeRegions(toInsert);
571 return;
572 }
573 }
574 }
575 toInsert.add(SEEN);
576
577 // Insert into the queue using descending commit time, so
578 // the most recent commit will pop next.
579 int time = toInsert.getTime();
580 Candidate n = queue;
581 if (n == null || time >= n.getTime()) {
582 toInsert.queueNext = n;
583 queue = toInsert;
584 return;
585 }
586
587 for (Candidate p = n;; p = n) {
588 n = p.queueNext;
589 if (n == null || time >= n.getTime()) {
590 toInsert.queueNext = n;
591 p.queueNext = toInsert;
592 return;
593 }
594 }
595 }
596
597 private boolean processOne(Candidate n) throws IOException {
598 RevCommit parent = n.getParent(0);
599 if (parent == null)
600 return split(n.getNextCandidate(0), n);
601 revPool.parseHeaders(parent);
602
603 if (find(parent, n.sourcePath)) {
604 if (idBuf.equals(n.sourceBlob))
605 return blameEntireRegionOnParent(n, parent);
606 return splitBlameWithParent(n, parent);
607 }
608
609 if (n.sourceCommit == null)
610 return result(n);
611
612 DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
613 if (r == null)
614 return result(n);
615
616 if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
617 // A 100% rename without any content change can also
618 // skip directly to the parent.
619 n.sourceCommit = parent;
620 n.sourcePath = PathFilter.create(r.getOldPath());
621 push(n);
622 return false;
623 }
624
625 Candidate next = n.create(parent, PathFilter.create(r.getOldPath()));
626 next.sourceBlob = r.getOldId().toObjectId();
627 next.renameScore = r.getScore();
628 next.loadText(reader);
629 return split(next, n);
630 }
631
632 private boolean blameEntireRegionOnParent(Candidate n, RevCommit parent) {
633 // File was not modified, blame parent.
634 n.sourceCommit = parent;
635 push(n);
636 return false;
637 }
638
639 private boolean splitBlameWithParent(Candidate n, RevCommit parent)
640 throws IOException {
641 Candidate next = n.create(parent, n.sourcePath);
642 next.sourceBlob = idBuf.toObjectId();
643 next.loadText(reader);
644 return split(next, n);
645 }
646
647 private boolean split(Candidate parent, Candidate source)
648 throws IOException {
649 EditList editList = diffAlgorithm.diff(textComparator,
650 parent.sourceText, source.sourceText);
651 if (editList.isEmpty()) {
652 // Ignoring whitespace (or some other special comparator) can
653 // cause non-identical blobs to have an empty edit list. In
654 // a case like this push the parent alone.
655 parent.regionList = source.regionList;
656 push(parent);
657 return false;
658 }
659
660 parent.takeBlame(editList, source);
661 if (parent.regionList != null)
662 push(parent);
663 if (source.regionList != null) {
664 if (source instanceof ReverseCandidate)
665 return reverseResult(parent, source);
666 return result(source);
667 }
668 return false;
669 }
670
671 private boolean processMerge(Candidate n) throws IOException {
672 int pCnt = n.getParentCount();
673
674 // If any single parent exactly matches the merge, follow only
675 // that one parent through history.
676 ObjectId[] ids = null;
677 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
678 RevCommit parent = n.getParent(pIdx);
679 revPool.parseHeaders(parent);
680 if (!find(parent, n.sourcePath))
681 continue;
682 if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob))
683 return blameEntireRegionOnParent(n, parent);
684 if (ids == null)
685 ids = new ObjectId[pCnt];
686 ids[pIdx] = idBuf.toObjectId();
687 }
688
689 // If rename detection is enabled, search for any relevant names.
690 DiffEntry[] renames = null;
691 if (renameDetector != null) {
692 renames = new DiffEntry[pCnt];
693 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
694 RevCommit parent = n.getParent(pIdx);
695 if (ids != null && ids[pIdx] != null)
696 continue;
697
698 DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
699 if (r == null)
700 continue;
701
702 if (n instanceof ReverseCandidate) {
703 if (ids == null)
704 ids = new ObjectId[pCnt];
705 ids[pCnt] = r.getOldId().toObjectId();
706 } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
707 // A 100% rename without any content change can also
708 // skip directly to the parent. Note this bypasses an
709 // earlier parent that had the path (above) but did not
710 // have an exact content match. For performance reasons
711 // we choose to follow the one parent over trying to do
712 // possibly both parents.
713 n.sourcePath = PathFilter.create(r.getOldPath());
714 return blameEntireRegionOnParent(n, parent);
715 }
716
717 renames[pIdx] = r;
718 }
719 }
720
721 // Construct the candidate for each parent.
722 Candidate[] parents = new Candidate[pCnt];
723 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
724 RevCommit parent = n.getParent(pIdx);
725
726 Candidate p;
727 if (renames != null && renames[pIdx] != null) {
728 p = n.create(parent,
729 PathFilter.create(renames[pIdx].getOldPath()));
730 p.renameScore = renames[pIdx].getScore();
731 p.sourceBlob = renames[pIdx].getOldId().toObjectId();
732 } else if (ids != null && ids[pIdx] != null) {
733 p = n.create(parent, n.sourcePath);
734 p.sourceBlob = ids[pIdx];
735 } else {
736 continue;
737 }
738
739 EditList editList;
740 if (n instanceof ReverseCandidate
741 && p.sourceBlob.equals(n.sourceBlob)) {
742 // This special case happens on ReverseCandidate forks.
743 p.sourceText = n.sourceText;
744 editList = new EditList(0);
745 } else {
746 p.loadText(reader);
747 editList = diffAlgorithm.diff(textComparator,
748 p.sourceText, n.sourceText);
749 }
750
751 if (editList.isEmpty()) {
752 // Ignoring whitespace (or some other special comparator) can
753 // cause non-identical blobs to have an empty edit list. In
754 // a case like this push the parent alone.
755 if (n instanceof ReverseCandidate) {
756 parents[pIdx] = p;
757 continue;
758 }
759
760 p.regionList = n.regionList;
761 n.regionList = null;
762 parents[pIdx] = p;
763 break;
764 }
765
766 p.takeBlame(editList, n);
767
768 // Only remember this parent candidate if there is at least
769 // one region that was blamed on the parent.
770 if (p.regionList != null) {
771 // Reverse blame requires inverting the regions. This puts
772 // the regions the parent deleted from us into the parent,
773 // and retains the common regions to look at other parents
774 // for deletions.
775 if (n instanceof ReverseCandidate) {
776 Region r = p.regionList;
777 p.regionList = n.regionList;
778 n.regionList = r;
779 }
780
781 parents[pIdx] = p;
782 }
783 }
784
785 if (n instanceof ReverseCandidate) {
786 // On a reverse blame report all deletions found in the children,
787 // and pass on to them a copy of our region list.
788 Candidate resultHead = null;
789 Candidate resultTail = null;
790
791 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
792 Candidate p = parents[pIdx];
793 if (p == null)
794 continue;
795
796 if (p.regionList != null) {
797 Candidate r = p.copy(p.sourceCommit);
798 if (resultTail != null) {
799 resultTail.queueNext = r;
800 resultTail = r;
801 } else {
802 resultHead = r;
803 resultTail = r;
804 }
805 }
806
807 if (n.regionList != null) {
808 p.regionList = n.regionList.deepCopy();
809 push(p);
810 }
811 }
812
813 if (resultHead != null)
814 return result(resultHead);
815 return false;
816 }
817
818 // Push any parents that are still candidates.
819 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
820 if (parents[pIdx] != null)
821 push(parents[pIdx]);
822 }
823
824 if (n.regionList != null)
825 return result(n);
826 return false;
827 }
828
829 /**
830 * Get the revision blamed for the current region.
831 * <p>
832 * The source commit may be null if the line was blamed to an uncommitted
833 * revision, such as the working tree copy, or during a reverse blame if the
834 * line survives to the end revision (e.g. the branch tip).
835 *
836 * @return current revision being blamed.
837 */
838 public RevCommit getSourceCommit() {
839 return outCandidate.sourceCommit;
840 }
841
842 /** @return current author being blamed. */
843 public PersonIdent getSourceAuthor() {
844 return outCandidate.getAuthor();
845 }
846
847 /** @return current committer being blamed. */
848 public PersonIdent getSourceCommitter() {
849 RevCommit c = getSourceCommit();
850 return c != null ? c.getCommitterIdent() : null;
851 }
852
853 /** @return path of the file being blamed. */
854 public String getSourcePath() {
855 return outCandidate.sourcePath.getPath();
856 }
857
858 /** @return rename score if a rename occurred in {@link #getSourceCommit}. */
859 public int getRenameScore() {
860 return outCandidate.renameScore;
861 }
862
863 /**
864 * @return first line of the source data that has been blamed for the
865 * current region. This is line number of where the region was added
866 * during {@link #getSourceCommit()} in file
867 * {@link #getSourcePath()}.
868 */
869 public int getSourceStart() {
870 return outRegion.sourceStart;
871 }
872
873 /**
874 * @return one past the range of the source data that has been blamed for
875 * the current region. This is line number of where the region was
876 * added during {@link #getSourceCommit()} in file
877 * {@link #getSourcePath()}.
878 */
879 public int getSourceEnd() {
880 Region r = outRegion;
881 return r.sourceStart + r.length;
882 }
883
884 /**
885 * @return first line of the result that {@link #getSourceCommit()} has been
886 * blamed for providing. Line numbers use 0 based indexing.
887 */
888 public int getResultStart() {
889 return outRegion.resultStart;
890 }
891
892 /**
893 * @return one past the range of the result that {@link #getSourceCommit()}
894 * has been blamed for providing. Line numbers use 0 based indexing.
895 * Because a source cannot be blamed for an empty region of the
896 * result, {@link #getResultEnd()} is always at least one larger
897 * than {@link #getResultStart()}.
898 */
899 public int getResultEnd() {
900 Region r = outRegion;
901 return r.resultStart + r.length;
902 }
903
904 /**
905 * @return number of lines in the current region being blamed to
906 * {@link #getSourceCommit()}. This is always the value of the
907 * expression {@code getResultEnd() - getResultStart()}, but also
908 * {@code getSourceEnd() - getSourceStart()}.
909 */
910 public int getRegionLength() {
911 return outRegion.length;
912 }
913
914 /**
915 * @return complete contents of the source file blamed for the current
916 * output region. This is the contents of {@link #getSourcePath()}
917 * within {@link #getSourceCommit()}. The source contents is
918 * temporarily available as an artifact of the blame algorithm. Most
919 * applications will want the result contents for display to users.
920 */
921 public RawText getSourceContents() {
922 return outCandidate.sourceText;
923 }
924
925 /**
926 * @return complete file contents of the result file blame is annotating.
927 * This value is accessible only after being configured and only
928 * immediately before the first call to {@link #next()}. Returns
929 * null if the path does not exist.
930 * @throws IOException
931 * repository cannot be read.
932 * @throws IllegalStateException
933 * {@link #next()} has already been invoked.
934 */
935 public RawText getResultContents() throws IOException {
936 return queue != null ? queue.sourceText : null;
937 }
938
939 /**
940 * Release the current blame session.
941 *
942 * @since 4.0
943 */
944 @Override
945 public void close() {
946 revPool.close();
947 queue = null;
948 outCandidate = null;
949 outRegion = null;
950 }
951
952 private boolean find(RevCommit commit, PathFilter path) throws IOException {
953 treeWalk.setFilter(path);
954 treeWalk.reset(commit.getTree());
955 if (treeWalk.next() && isFile(treeWalk.getRawMode(0))) {
956 treeWalk.getObjectId(idBuf, 0);
957 return true;
958 }
959 return false;
960 }
961
962 private static final boolean isFile(int rawMode) {
963 return (rawMode & TYPE_MASK) == TYPE_FILE;
964 }
965
966 private DiffEntry findRename(RevCommit parent, RevCommit commit,
967 PathFilter path) throws IOException {
968 if (renameDetector == null)
969 return null;
970
971 treeWalk.setFilter(TreeFilter.ANY_DIFF);
972 treeWalk.reset(parent.getTree(), commit.getTree());
973 renameDetector.reset();
974 renameDetector.addAll(DiffEntry.scan(treeWalk));
975 for (DiffEntry ent : renameDetector.compute()) {
976 if (isRename(ent) && ent.getNewPath().equals(path.getPath()))
977 return ent;
978 }
979 return null;
980 }
981
982 private static boolean isRename(DiffEntry ent) {
983 return ent.getChangeType() == ChangeType.RENAME
984 || ent.getChangeType() == ChangeType.COPY;
985 }
986 }