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