Java tutorial
/* * Copyright (C) 2011, 2019 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.io.InputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.blame.Candidate.BlobCandidate; import org.eclipse.jgit.blame.Candidate.HeadCandidate; 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.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; 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.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.IO; /** * 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; } /** * Pushes HEAD, index, and working tree as appropriate for blaming the file * given in the constructor {@link #BlameGenerator(Repository, String)} * against HEAD. Includes special handling in case the file is in conflict * state from an unresolved merge conflict. * * @return {@code this} * @throws NoHeadException * if the repository has no HEAD * @throws IOException * if an error occurs * @since 5.6 */ public BlameGenerator prepareHead() throws NoHeadException, IOException { Repository repo = getRepository(); ObjectId head = repo.resolve(Constants.HEAD); if (head == null) { throw new NoHeadException(MessageFormat.format(JGitText.get().noSuchRefKnown, Constants.HEAD)); } if (repo.isBare()) { return push(null, head); } DirCache dc = repo.readDirCache(); try (TreeWalk walk = new TreeWalk(repo)) { walk.setOperationType(OperationType.CHECKIN_OP); FileTreeIterator iter = new FileTreeIterator(repo); int fileTree = walk.addTree(iter); int indexTree = walk.addTree(new DirCacheIterator(dc)); iter.setDirCacheIterator(walk, indexTree); walk.setFilter(resultPath); walk.setRecursive(true); if (!walk.next()) { return this; } DirCacheIterator dcIter = walk.getTree(indexTree, DirCacheIterator.class); if (dcIter == null) { // Not found in index return this; } iter = walk.getTree(fileTree, FileTreeIterator.class); if (iter == null || !isFile(iter.getEntryRawMode())) { return this; } RawText inTree; long filteredLength = iter.getEntryContentLength(); try (InputStream stream = iter.openEntryStream()) { inTree = new RawText(getBytes(iter.getEntryFile().getPath(), stream, filteredLength)); } DirCacheEntry indexEntry = dcIter.getDirCacheEntry(); if (indexEntry.getStage() == DirCacheEntry.STAGE_0) { push(null, head); push(null, indexEntry.getObjectId()); push(null, inTree); } else { // Create a special candidate using the working tree file as // blob and HEAD and the MERGE_HEADs as parents. HeadCandidate c = new HeadCandidate(getRepository(), resultPath, getHeads(repo, head)); c.sourceText = inTree; c.regionList = new Region(0, 0, inTree.size()); remaining = inTree.size(); push(c); } } return this; } private List<RevCommit> getHeads(Repository repo, ObjectId head) throws NoWorkTreeException, IOException { List<ObjectId> mergeIds = repo.readMergeHeads(); if (mergeIds == null || mergeIds.isEmpty()) { return Collections.singletonList(revPool.parseCommit(head)); } List<RevCommit> heads = new ArrayList<>(mergeIds.size() + 1); heads.add(revPool.parseCommit(head)); for (ObjectId id : mergeIds) { heads.add(revPool.parseCommit(id)); } return heads; } private static byte[] getBytes(String path, InputStream in, long maxLength) throws IOException { if (maxLength > Integer.MAX_VALUE) { throw new IOException(MessageFormat.format(JGitText.get().fileIsTooLarge, path)); } int max = (int) maxLength; byte[] buffer = new byte[max]; int read = IO.readFully(in, buffer, 0); if (read == max) { return buffer; } byte[] copy = new byte[read]; System.arraycopy(buffer, 0, copy, 0, read); return copy; } /** * 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 outRegion != null; } 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; } }