| /* |
| * Copyright (C) 2011, Google Inc. |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.eclipse.jgit.blame; |
| |
| import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; |
| import static org.eclipse.jgit.lib.FileMode.TYPE_FILE; |
| import static org.eclipse.jgit.lib.FileMode.TYPE_MASK; |
| |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| |
| import org.eclipse.jgit.annotations.Nullable; |
| import org.eclipse.jgit.blame.Candidate.BlobCandidate; |
| import org.eclipse.jgit.blame.Candidate.ReverseCandidate; |
| import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; |
| import org.eclipse.jgit.diff.DiffAlgorithm; |
| import org.eclipse.jgit.diff.DiffEntry; |
| import org.eclipse.jgit.diff.DiffEntry.ChangeType; |
| import org.eclipse.jgit.diff.EditList; |
| import org.eclipse.jgit.diff.HistogramDiff; |
| import org.eclipse.jgit.diff.RawText; |
| import org.eclipse.jgit.diff.RawTextComparator; |
| import org.eclipse.jgit.diff.RenameDetector; |
| import org.eclipse.jgit.internal.JGitText; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.MutableObjectId; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectLoader; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevFlag; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.treewalk.filter.PathFilter; |
| import org.eclipse.jgit.treewalk.filter.TreeFilter; |
| |
| /** |
| * Generate author information for lines based on a provided file. |
| * <p> |
| * Applications that want a simple one-shot computation of blame for a file |
| * should use {@link #computeBlameResult()} to prepare the entire result in one |
| * method call. This may block for significant time as the history of the |
| * repository must be traversed until information is gathered for every line. |
| * <p> |
| * Applications that want more incremental update behavior may use either the |
| * raw {@link #next()} streaming approach supported by this class, or construct |
| * a {@link org.eclipse.jgit.blame.BlameResult} using |
| * {@link org.eclipse.jgit.blame.BlameResult#create(BlameGenerator)} and |
| * incrementally construct the result with |
| * {@link org.eclipse.jgit.blame.BlameResult#computeNext()}. |
| * <p> |
| * This class is not thread-safe. |
| * <p> |
| * An instance of BlameGenerator can only be used once. To blame multiple files |
| * the application must create a new BlameGenerator. |
| * <p> |
| * During blame processing there are two files involved: |
| * <ul> |
| * <li>result - The file whose lines are being examined. This is the revision |
| * the user is trying to view blame/annotation information alongside of.</li> |
| * <li>source - The file that was blamed with supplying one or more lines of |
| * data into result. The source may be a different file path (due to copy or |
| * rename). Source line numbers may differ from result line numbers due to lines |
| * being added/removed in intermediate revisions.</li> |
| * </ul> |
| * <p> |
| * The blame algorithm is implemented by initially assigning responsibility for |
| * all lines of the result to the starting commit. A difference against the |
| * commit's ancestor is computed, and responsibility is passed to the ancestor |
| * commit for any lines that are common. The starting commit is blamed only for |
| * the lines that do not appear in the ancestor, if any. The loop repeats using |
| * the ancestor, until there are no more lines to acquire information on, or the |
| * file's creation point is discovered in history. |
| */ |
| public class BlameGenerator implements AutoCloseable { |
| private final Repository repository; |
| |
| private final PathFilter resultPath; |
| |
| private final MutableObjectId idBuf; |
| |
| /** Revision pool used to acquire commits from. */ |
| private RevWalk revPool; |
| |
| /** Indicates the commit was put into the queue at least once. */ |
| private RevFlag SEEN; |
| |
| private ObjectReader reader; |
| |
| private TreeWalk treeWalk; |
| |
| private DiffAlgorithm diffAlgorithm = new HistogramDiff(); |
| |
| private RawTextComparator textComparator = RawTextComparator.DEFAULT; |
| |
| private RenameDetector renameDetector; |
| |
| /** Potential candidates, sorted by commit time descending. */ |
| private Candidate queue; |
| |
| /** Number of lines that still need to be discovered. */ |
| private int remaining; |
| |
| /** Blame is currently assigned to this source. */ |
| private Candidate outCandidate; |
| private Region outRegion; |
| |
| /** |
| * Create a blame generator for the repository and path (relative to |
| * repository) |
| * |
| * @param repository |
| * repository to access revision data from. |
| * @param path |
| * initial path of the file to start scanning (relative to the |
| * repository). |
| */ |
| public BlameGenerator(Repository repository, String path) { |
| this.repository = repository; |
| this.resultPath = PathFilter.create(path); |
| |
| idBuf = new MutableObjectId(); |
| setFollowFileRenames(true); |
| initRevPool(false); |
| |
| remaining = -1; |
| } |
| |
| private void initRevPool(boolean reverse) { |
| if (queue != null) |
| throw new IllegalStateException(); |
| |
| if (revPool != null) |
| revPool.close(); |
| |
| if (reverse) |
| revPool = new ReverseWalk(getRepository()); |
| else |
| revPool = new RevWalk(getRepository()); |
| |
| SEEN = revPool.newFlag("SEEN"); //$NON-NLS-1$ |
| reader = revPool.getObjectReader(); |
| treeWalk = new TreeWalk(reader); |
| treeWalk.setRecursive(true); |
| } |
| |
| /** |
| * Get repository |
| * |
| * @return repository being scanned for revision history |
| */ |
| public Repository getRepository() { |
| return repository; |
| } |
| |
| /** |
| * Get result path |
| * |
| * @return path file path being processed |
| */ |
| public String getResultPath() { |
| return resultPath.getPath(); |
| } |
| |
| /** |
| * Difference algorithm to use when comparing revisions. |
| * |
| * @param algorithm |
| * a {@link org.eclipse.jgit.diff.DiffAlgorithm} |
| * @return {@code this} |
| */ |
| public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) { |
| diffAlgorithm = algorithm; |
| return this; |
| } |
| |
| /** |
| * Text comparator to use when comparing revisions. |
| * |
| * @param comparator |
| * a {@link org.eclipse.jgit.diff.RawTextComparator} |
| * @return {@code this} |
| */ |
| public BlameGenerator setTextComparator(RawTextComparator comparator) { |
| textComparator = comparator; |
| return this; |
| } |
| |
| /** |
| * Enable (or disable) following file renames, on by default. |
| * <p> |
| * If true renames are followed using the standard FollowFilter behavior |
| * used by RevWalk (which matches {@code git log --follow} in the C |
| * implementation). This is not the same as copy/move detection as |
| * implemented by the C implementation's of {@code git blame -M -C}. |
| * |
| * @param follow |
| * enable following. |
| * @return {@code this} |
| */ |
| public BlameGenerator setFollowFileRenames(boolean follow) { |
| if (follow) |
| renameDetector = new RenameDetector(getRepository()); |
| else |
| renameDetector = null; |
| return this; |
| } |
| |
| /** |
| * Obtain the RenameDetector, allowing the application to configure its |
| * settings for rename score and breaking behavior. |
| * |
| * @return the rename detector, or {@code null} if |
| * {@code setFollowFileRenames(false)}. |
| */ |
| @Nullable |
| public RenameDetector getRenameDetector() { |
| return renameDetector; |
| } |
| |
| /** |
| * Push a candidate blob onto the generator's traversal stack. |
| * <p> |
| * Candidates should be pushed in history order from oldest-to-newest. |
| * Applications should push the starting commit first, then the index |
| * revision (if the index is interesting), and finally the working tree copy |
| * (if the working tree is interesting). |
| * |
| * @param description |
| * description of the blob revision, such as "Working Tree". |
| * @param contents |
| * contents of the file. |
| * @return {@code this} |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameGenerator push(String description, byte[] contents) |
| throws IOException { |
| return push(description, new RawText(contents)); |
| } |
| |
| /** |
| * Push a candidate blob onto the generator's traversal stack. |
| * <p> |
| * Candidates should be pushed in history order from oldest-to-newest. |
| * Applications should push the starting commit first, then the index |
| * revision (if the index is interesting), and finally the working tree copy |
| * (if the working tree is interesting). |
| * |
| * @param description |
| * description of the blob revision, such as "Working Tree". |
| * @param contents |
| * contents of the file. |
| * @return {@code this} |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameGenerator push(String description, RawText contents) |
| throws IOException { |
| if (description == null) |
| description = JGitText.get().blameNotCommittedYet; |
| BlobCandidate c = new BlobCandidate(getRepository(), description, |
| resultPath); |
| c.sourceText = contents; |
| c.regionList = new Region(0, 0, contents.size()); |
| remaining = contents.size(); |
| push(c); |
| return this; |
| } |
| |
| /** |
| * Push a candidate object onto the generator's traversal stack. |
| * <p> |
| * Candidates should be pushed in history order from oldest-to-newest. |
| * Applications should push the starting commit first, then the index |
| * revision (if the index is interesting), and finally the working tree copy |
| * (if the working tree is interesting). |
| * |
| * @param description |
| * description of the blob revision, such as "Working Tree". |
| * @param id |
| * may be a commit or a blob. |
| * @return {@code this} |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameGenerator push(String description, AnyObjectId id) |
| throws IOException { |
| ObjectLoader ldr = reader.open(id); |
| if (ldr.getType() == OBJ_BLOB) { |
| if (description == null) |
| description = JGitText.get().blameNotCommittedYet; |
| BlobCandidate c = new BlobCandidate(getRepository(), description, |
| resultPath); |
| c.sourceBlob = id.toObjectId(); |
| c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); |
| c.regionList = new Region(0, 0, c.sourceText.size()); |
| remaining = c.sourceText.size(); |
| push(c); |
| return this; |
| } |
| |
| RevCommit commit = revPool.parseCommit(id); |
| if (!find(commit, resultPath)) |
| return this; |
| |
| Candidate c = new Candidate(getRepository(), commit, resultPath); |
| c.sourceBlob = idBuf.toObjectId(); |
| c.loadText(reader); |
| c.regionList = new Region(0, 0, c.sourceText.size()); |
| remaining = c.sourceText.size(); |
| push(c); |
| return this; |
| } |
| |
| /** |
| * Configure the generator to compute reverse blame (history of deletes). |
| * <p> |
| * This method is expensive as it immediately runs a RevWalk over the |
| * history spanning the expression {@code start..end} (end being more recent |
| * than start) and then performs the equivalent operation as |
| * {@link #push(String, AnyObjectId)} to begin blame traversal from the |
| * commit named by {@code start} walking forwards through history until |
| * {@code end} blaming line deletions. |
| * <p> |
| * A reverse blame may produce multiple sources for the same result line, |
| * each of these is a descendant commit that removed the line, typically |
| * this occurs when the same deletion appears in multiple side branches such |
| * as due to a cherry-pick. Applications relying on reverse should use |
| * {@link org.eclipse.jgit.blame.BlameResult} as it filters these duplicate |
| * sources and only remembers the first (oldest) deletion. |
| * |
| * @param start |
| * oldest commit to traverse from. The result file will be loaded |
| * from this commit's tree. |
| * @param end |
| * most recent commit to stop traversal at. Usually an active |
| * branch tip, tag, or HEAD. |
| * @return {@code this} |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameGenerator reverse(AnyObjectId start, AnyObjectId end) |
| throws IOException { |
| return reverse(start, Collections.singleton(end.toObjectId())); |
| } |
| |
| /** |
| * Configure the generator to compute reverse blame (history of deletes). |
| * <p> |
| * This method is expensive as it immediately runs a RevWalk over the |
| * history spanning the expression {@code start..end} (end being more recent |
| * than start) and then performs the equivalent operation as |
| * {@link #push(String, AnyObjectId)} to begin blame traversal from the |
| * commit named by {@code start} walking forwards through history until |
| * {@code end} blaming line deletions. |
| * <p> |
| * A reverse blame may produce multiple sources for the same result line, |
| * each of these is a descendant commit that removed the line, typically |
| * this occurs when the same deletion appears in multiple side branches such |
| * as due to a cherry-pick. Applications relying on reverse should use |
| * {@link org.eclipse.jgit.blame.BlameResult} as it filters these duplicate |
| * sources and only remembers the first (oldest) deletion. |
| * |
| * @param start |
| * oldest commit to traverse from. The result file will be loaded |
| * from this commit's tree. |
| * @param end |
| * most recent commits to stop traversal at. Usually an active |
| * branch tip, tag, or HEAD. |
| * @return {@code this} |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameGenerator reverse(AnyObjectId start, |
| Collection<? extends ObjectId> end) throws IOException { |
| initRevPool(true); |
| |
| ReverseCommit result = (ReverseCommit) revPool.parseCommit(start); |
| if (!find(result, resultPath)) |
| return this; |
| |
| revPool.markUninteresting(result); |
| for (ObjectId id : end) |
| revPool.markStart(revPool.parseCommit(id)); |
| |
| while (revPool.next() != null) { |
| // just pump the queue |
| } |
| |
| ReverseCandidate c = new ReverseCandidate(getRepository(), result, |
| resultPath); |
| c.sourceBlob = idBuf.toObjectId(); |
| c.loadText(reader); |
| c.regionList = new Region(0, 0, c.sourceText.size()); |
| remaining = c.sourceText.size(); |
| push(c); |
| return this; |
| } |
| |
| /** |
| * Allocate a new RevFlag for use by the caller. |
| * |
| * @param name |
| * unique name of the flag in the blame context. |
| * @return the newly allocated flag. |
| * @since 3.4 |
| */ |
| public RevFlag newFlag(String name) { |
| return revPool.newFlag(name); |
| } |
| |
| /** |
| * Execute the generator in a blocking fashion until all data is ready. |
| * |
| * @return the complete result. Null if no file exists for the given path. |
| * @throws java.io.IOException |
| * the repository cannot be read. |
| */ |
| public BlameResult computeBlameResult() throws IOException { |
| try { |
| BlameResult r = BlameResult.create(this); |
| if (r != null) |
| r.computeAll(); |
| return r; |
| } finally { |
| close(); |
| } |
| } |
| |
| /** |
| * Step the blame algorithm one iteration. |
| * |
| * @return true if the generator has found a region's source. The getSource* |
| * and {@link #getResultStart()}, {@link #getResultEnd()} methods |
| * can be used to inspect the region found. False if there are no |
| * more regions to describe. |
| * @throws java.io.IOException |
| * repository cannot be read. |
| */ |
| public boolean next() throws IOException { |
| // If there is a source still pending, produce the next region. |
| if (outRegion != null) { |
| Region r = outRegion; |
| remaining -= r.length; |
| if (r.next != null) { |
| outRegion = r.next; |
| return true; |
| } |
| |
| if (outCandidate.queueNext != null) |
| return result(outCandidate.queueNext); |
| |
| outCandidate = null; |
| outRegion = null; |
| } |
| |
| // If there are no lines remaining, the entire result is done, |
| // even if there are revisions still available for the path. |
| if (remaining == 0) |
| return done(); |
| |
| for (;;) { |
| Candidate n = pop(); |
| if (n == null) |
| return done(); |
| |
| int pCnt = n.getParentCount(); |
| if (pCnt == 1) { |
| if (processOne(n)) |
| return true; |
| |
| } else if (1 < pCnt) { |
| if (processMerge(n)) |
| return true; |
| |
| } else if (n instanceof ReverseCandidate) { |
| // Do not generate a tip of a reverse. The region |
| // survives and should not appear to be deleted. |
| |
| } else /* if (pCnt == 0) */{ |
| // Root commit, with at least one surviving region. |
| // Assign the remaining blame here. |
| return result(n); |
| } |
| } |
| } |
| |
| private boolean done() { |
| close(); |
| return false; |
| } |
| |
| private boolean result(Candidate n) throws IOException { |
| n.beginResult(revPool); |
| outCandidate = n; |
| outRegion = n.regionList; |
| return true; |
| } |
| |
| private boolean reverseResult(Candidate parent, Candidate source) |
| throws IOException { |
| // On a reverse blame present the application the parent |
| // (as this is what did the removals), however the region |
| // list to enumerate is the source's surviving list. |
| Candidate res = parent.copy(parent.sourceCommit); |
| res.regionList = source.regionList; |
| return result(res); |
| } |
| |
| private Candidate pop() { |
| Candidate n = queue; |
| if (n != null) { |
| queue = n.queueNext; |
| n.queueNext = null; |
| } |
| return n; |
| } |
| |
| private void push(BlobCandidate toInsert) { |
| Candidate c = queue; |
| if (c != null) { |
| c.remove(SEEN); // will be pushed by toInsert |
| c.regionList = null; |
| toInsert.parent = c; |
| } |
| queue = toInsert; |
| } |
| |
| private void push(Candidate toInsert) { |
| if (toInsert.has(SEEN)) { |
| // We have already added a Candidate for this commit to the queue, |
| // this can happen if the commit is a merge base for two or more |
| // parallel branches that were merged together. |
| // |
| // It is likely the candidate was not yet processed. The queue |
| // sorts descending by commit time and usually descendant commits |
| // have higher timestamps than the ancestors. |
| // |
| // Find the existing candidate and merge the new candidate's |
| // region list into it. |
| for (Candidate p = queue; p != null; p = p.queueNext) { |
| if (p.canMergeRegions(toInsert)) { |
| p.mergeRegions(toInsert); |
| return; |
| } |
| } |
| } |
| toInsert.add(SEEN); |
| |
| // Insert into the queue using descending commit time, so |
| // the most recent commit will pop next. |
| int time = toInsert.getTime(); |
| Candidate n = queue; |
| if (n == null || time >= n.getTime()) { |
| toInsert.queueNext = n; |
| queue = toInsert; |
| return; |
| } |
| |
| for (Candidate p = n;; p = n) { |
| n = p.queueNext; |
| if (n == null || time >= n.getTime()) { |
| toInsert.queueNext = n; |
| p.queueNext = toInsert; |
| return; |
| } |
| } |
| } |
| |
| private boolean processOne(Candidate n) throws IOException { |
| RevCommit parent = n.getParent(0); |
| if (parent == null) |
| return split(n.getNextCandidate(0), n); |
| revPool.parseHeaders(parent); |
| |
| if (find(parent, n.sourcePath)) { |
| if (idBuf.equals(n.sourceBlob)) |
| return blameEntireRegionOnParent(n, parent); |
| return splitBlameWithParent(n, parent); |
| } |
| |
| if (n.sourceCommit == null) |
| return result(n); |
| |
| DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); |
| if (r == null) |
| return result(n); |
| |
| if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { |
| // A 100% rename without any content change can also |
| // skip directly to the parent. |
| n.sourceCommit = parent; |
| n.sourcePath = PathFilter.create(r.getOldPath()); |
| push(n); |
| return false; |
| } |
| |
| Candidate next = n.create(getRepository(), parent, |
| PathFilter.create(r.getOldPath())); |
| next.sourceBlob = r.getOldId().toObjectId(); |
| next.renameScore = r.getScore(); |
| next.loadText(reader); |
| return split(next, n); |
| } |
| |
| private boolean blameEntireRegionOnParent(Candidate n, RevCommit parent) { |
| // File was not modified, blame parent. |
| n.sourceCommit = parent; |
| push(n); |
| return false; |
| } |
| |
| private boolean splitBlameWithParent(Candidate n, RevCommit parent) |
| throws IOException { |
| Candidate next = n.create(getRepository(), parent, n.sourcePath); |
| next.sourceBlob = idBuf.toObjectId(); |
| next.loadText(reader); |
| return split(next, n); |
| } |
| |
| private boolean split(Candidate parent, Candidate source) |
| throws IOException { |
| EditList editList = diffAlgorithm.diff(textComparator, |
| parent.sourceText, source.sourceText); |
| if (editList.isEmpty()) { |
| // Ignoring whitespace (or some other special comparator) can |
| // cause non-identical blobs to have an empty edit list. In |
| // a case like this push the parent alone. |
| parent.regionList = source.regionList; |
| push(parent); |
| return false; |
| } |
| |
| parent.takeBlame(editList, source); |
| if (parent.regionList != null) |
| push(parent); |
| if (source.regionList != null) { |
| if (source instanceof ReverseCandidate) |
| return reverseResult(parent, source); |
| return result(source); |
| } |
| return false; |
| } |
| |
| private boolean processMerge(Candidate n) throws IOException { |
| int pCnt = n.getParentCount(); |
| |
| // If any single parent exactly matches the merge, follow only |
| // that one parent through history. |
| ObjectId[] ids = null; |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| RevCommit parent = n.getParent(pIdx); |
| revPool.parseHeaders(parent); |
| if (!find(parent, n.sourcePath)) |
| continue; |
| if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob)) |
| return blameEntireRegionOnParent(n, parent); |
| if (ids == null) |
| ids = new ObjectId[pCnt]; |
| ids[pIdx] = idBuf.toObjectId(); |
| } |
| |
| // If rename detection is enabled, search for any relevant names. |
| DiffEntry[] renames = null; |
| if (renameDetector != null) { |
| renames = new DiffEntry[pCnt]; |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| RevCommit parent = n.getParent(pIdx); |
| if (ids != null && ids[pIdx] != null) |
| continue; |
| |
| DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); |
| if (r == null) |
| continue; |
| |
| if (n instanceof ReverseCandidate) { |
| if (ids == null) |
| ids = new ObjectId[pCnt]; |
| ids[pCnt] = r.getOldId().toObjectId(); |
| } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { |
| // A 100% rename without any content change can also |
| // skip directly to the parent. Note this bypasses an |
| // earlier parent that had the path (above) but did not |
| // have an exact content match. For performance reasons |
| // we choose to follow the one parent over trying to do |
| // possibly both parents. |
| n.sourcePath = PathFilter.create(r.getOldPath()); |
| return blameEntireRegionOnParent(n, parent); |
| } |
| |
| renames[pIdx] = r; |
| } |
| } |
| |
| // Construct the candidate for each parent. |
| Candidate[] parents = new Candidate[pCnt]; |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| RevCommit parent = n.getParent(pIdx); |
| |
| Candidate p; |
| if (renames != null && renames[pIdx] != null) { |
| p = n.create(getRepository(), parent, |
| PathFilter.create(renames[pIdx].getOldPath())); |
| p.renameScore = renames[pIdx].getScore(); |
| p.sourceBlob = renames[pIdx].getOldId().toObjectId(); |
| } else if (ids != null && ids[pIdx] != null) { |
| p = n.create(getRepository(), parent, n.sourcePath); |
| p.sourceBlob = ids[pIdx]; |
| } else { |
| continue; |
| } |
| |
| EditList editList; |
| if (n instanceof ReverseCandidate |
| && p.sourceBlob.equals(n.sourceBlob)) { |
| // This special case happens on ReverseCandidate forks. |
| p.sourceText = n.sourceText; |
| editList = new EditList(0); |
| } else { |
| p.loadText(reader); |
| editList = diffAlgorithm.diff(textComparator, |
| p.sourceText, n.sourceText); |
| } |
| |
| if (editList.isEmpty()) { |
| // Ignoring whitespace (or some other special comparator) can |
| // cause non-identical blobs to have an empty edit list. In |
| // a case like this push the parent alone. |
| if (n instanceof ReverseCandidate) { |
| parents[pIdx] = p; |
| continue; |
| } |
| |
| p.regionList = n.regionList; |
| n.regionList = null; |
| parents[pIdx] = p; |
| break; |
| } |
| |
| p.takeBlame(editList, n); |
| |
| // Only remember this parent candidate if there is at least |
| // one region that was blamed on the parent. |
| if (p.regionList != null) { |
| // Reverse blame requires inverting the regions. This puts |
| // the regions the parent deleted from us into the parent, |
| // and retains the common regions to look at other parents |
| // for deletions. |
| if (n instanceof ReverseCandidate) { |
| Region r = p.regionList; |
| p.regionList = n.regionList; |
| n.regionList = r; |
| } |
| |
| parents[pIdx] = p; |
| } |
| } |
| |
| if (n instanceof ReverseCandidate) { |
| // On a reverse blame report all deletions found in the children, |
| // and pass on to them a copy of our region list. |
| Candidate resultHead = null; |
| Candidate resultTail = null; |
| |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| Candidate p = parents[pIdx]; |
| if (p == null) |
| continue; |
| |
| if (p.regionList != null) { |
| Candidate r = p.copy(p.sourceCommit); |
| if (resultTail != null) { |
| resultTail.queueNext = r; |
| resultTail = r; |
| } else { |
| resultHead = r; |
| resultTail = r; |
| } |
| } |
| |
| if (n.regionList != null) { |
| p.regionList = n.regionList.deepCopy(); |
| push(p); |
| } |
| } |
| |
| if (resultHead != null) |
| return result(resultHead); |
| return false; |
| } |
| |
| // Push any parents that are still candidates. |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| if (parents[pIdx] != null) |
| push(parents[pIdx]); |
| } |
| |
| if (n.regionList != null) |
| return result(n); |
| return false; |
| } |
| |
| /** |
| * Get the revision blamed for the current region. |
| * <p> |
| * The source commit may be null if the line was blamed to an uncommitted |
| * revision, such as the working tree copy, or during a reverse blame if the |
| * line survives to the end revision (e.g. the branch tip). |
| * |
| * @return current revision being blamed. |
| */ |
| public RevCommit getSourceCommit() { |
| return outCandidate.sourceCommit; |
| } |
| |
| /** |
| * Get source author |
| * |
| * @return current author being blamed |
| */ |
| public PersonIdent getSourceAuthor() { |
| return outCandidate.getAuthor(); |
| } |
| |
| /** |
| * Get source committer |
| * |
| * @return current committer being blamed |
| */ |
| public PersonIdent getSourceCommitter() { |
| RevCommit c = getSourceCommit(); |
| return c != null ? c.getCommitterIdent() : null; |
| } |
| |
| /** |
| * Get source path |
| * |
| * @return path of the file being blamed |
| */ |
| public String getSourcePath() { |
| return outCandidate.sourcePath.getPath(); |
| } |
| |
| /** |
| * Get rename score |
| * |
| * @return rename score if a rename occurred in {@link #getSourceCommit} |
| */ |
| public int getRenameScore() { |
| return outCandidate.renameScore; |
| } |
| |
| /** |
| * Get first line of the source data that has been blamed for the current |
| * region |
| * |
| * @return first line of the source data that has been blamed for the |
| * current region. This is line number of where the region was added |
| * during {@link #getSourceCommit()} in file |
| * {@link #getSourcePath()}. |
| */ |
| public int getSourceStart() { |
| return outRegion.sourceStart; |
| } |
| |
| /** |
| * Get one past the range of the source data that has been blamed for the |
| * current region |
| * |
| * @return one past the range of the source data that has been blamed for |
| * the current region. This is line number of where the region was |
| * added during {@link #getSourceCommit()} in file |
| * {@link #getSourcePath()}. |
| */ |
| public int getSourceEnd() { |
| Region r = outRegion; |
| return r.sourceStart + r.length; |
| } |
| |
| /** |
| * Get first line of the result that {@link #getSourceCommit()} has been |
| * blamed for providing |
| * |
| * @return first line of the result that {@link #getSourceCommit()} has been |
| * blamed for providing. Line numbers use 0 based indexing. |
| */ |
| public int getResultStart() { |
| return outRegion.resultStart; |
| } |
| |
| /** |
| * Get one past the range of the result that {@link #getSourceCommit()} has |
| * been blamed for providing |
| * |
| * @return one past the range of the result that {@link #getSourceCommit()} |
| * has been blamed for providing. Line numbers use 0 based indexing. |
| * Because a source cannot be blamed for an empty region of the |
| * result, {@link #getResultEnd()} is always at least one larger |
| * than {@link #getResultStart()}. |
| */ |
| public int getResultEnd() { |
| Region r = outRegion; |
| return r.resultStart + r.length; |
| } |
| |
| /** |
| * Get number of lines in the current region being blamed to |
| * {@link #getSourceCommit()} |
| * |
| * @return number of lines in the current region being blamed to |
| * {@link #getSourceCommit()}. This is always the value of the |
| * expression {@code getResultEnd() - getResultStart()}, but also |
| * {@code getSourceEnd() - getSourceStart()}. |
| */ |
| public int getRegionLength() { |
| return outRegion.length; |
| } |
| |
| /** |
| * Get complete contents of the source file blamed for the current output |
| * region |
| * |
| * @return complete contents of the source file blamed for the current |
| * output region. This is the contents of {@link #getSourcePath()} |
| * within {@link #getSourceCommit()}. The source contents is |
| * temporarily available as an artifact of the blame algorithm. Most |
| * applications will want the result contents for display to users. |
| */ |
| public RawText getSourceContents() { |
| return outCandidate.sourceText; |
| } |
| |
| /** |
| * Get complete file contents of the result file blame is annotating |
| * |
| * @return complete file contents of the result file blame is annotating. |
| * This value is accessible only after being configured and only |
| * immediately before the first call to {@link #next()}. Returns |
| * null if the path does not exist. |
| * @throws java.io.IOException |
| * repository cannot be read. |
| * @throws java.lang.IllegalStateException |
| * {@link #next()} has already been invoked. |
| */ |
| public RawText getResultContents() throws IOException { |
| return queue != null ? queue.sourceText : null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Release the current blame session. |
| * |
| * @since 4.0 |
| */ |
| @Override |
| public void close() { |
| revPool.close(); |
| queue = null; |
| outCandidate = null; |
| outRegion = null; |
| } |
| |
| private boolean find(RevCommit commit, PathFilter path) throws IOException { |
| treeWalk.setFilter(path); |
| treeWalk.reset(commit.getTree()); |
| if (treeWalk.next() && isFile(treeWalk.getRawMode(0))) { |
| treeWalk.getObjectId(idBuf, 0); |
| return true; |
| } |
| return false; |
| } |
| |
| private static final boolean isFile(int rawMode) { |
| return (rawMode & TYPE_MASK) == TYPE_FILE; |
| } |
| |
| private DiffEntry findRename(RevCommit parent, RevCommit commit, |
| PathFilter path) throws IOException { |
| if (renameDetector == null) |
| return null; |
| |
| treeWalk.setFilter(TreeFilter.ANY_DIFF); |
| treeWalk.reset(parent.getTree(), commit.getTree()); |
| renameDetector.reset(); |
| renameDetector.addAll(DiffEntry.scan(treeWalk)); |
| for (DiffEntry ent : renameDetector.compute()) { |
| if (isRename(ent) && ent.getNewPath().equals(path.getPath())) |
| return ent; |
| } |
| return null; |
| } |
| |
| private static boolean isRename(DiffEntry ent) { |
| return ent.getChangeType() == ChangeType.RENAME |
| || ent.getChangeType() == ChangeType.COPY; |
| } |
| } |