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