12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016 |
- /*
- * 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 java.io.IOException;
- import java.util.Collection;
- import java.util.Collections;
-
- 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.AndTreeFilter;
- 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 BlameResult} using {@link BlameResult#create(BlameGenerator)} and
- * incrementally construct the result with {@link 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 {
- 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.release();
-
- if (reverse)
- revPool = new ReverseWalk(getRepository());
- else
- revPool = new RevWalk(getRepository());
-
- revPool.setRetainBody(true);
- SEEN = revPool.newFlag("SEEN"); //$NON-NLS-1$
- reader = revPool.getObjectReader();
- treeWalk = new TreeWalk(reader);
- treeWalk.setRecursive(true);
- }
-
- /** @return repository being scanned for revision history. */
- public Repository getRepository() {
- return repository;
- }
-
- /** @return path file path being processed. */
- public String getResultPath() {
- return resultPath.getPath();
- }
-
- /**
- * Difference algorithm to use when comparing revisions.
- *
- * @param algorithm
- * @return {@code this}
- */
- public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) {
- diffAlgorithm = algorithm;
- return this;
- }
-
- /**
- * Text comparator to use when comparing revisions.
- *
- * @param comparator
- * @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 if {@code setFollowFileRenames(true)}.
- *
- * @return the rename detector, allowing the application to configure its
- * settings for rename score and breaking behavior.
- */
- 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 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 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(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 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(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(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 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 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 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 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(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 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 {
- release();
- }
- }
-
- /**
- * 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 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() {
- release();
- return false;
- }
-
- private boolean result(Candidate n) throws IOException {
- if (n.sourceCommit != null)
- revPool.parseBody(n.sourceCommit);
- 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 (n.sourceCommit != null && n.recursivePath) {
- treeWalk.setFilter(AndTreeFilter.create(n.sourcePath, ID_DIFF));
- treeWalk.reset(n.sourceCommit.getTree(), parent.getTree());
- if (!treeWalk.next())
- return blameEntireRegionOnParent(n, parent);
- if (isFile(treeWalk.getRawMode(1))) {
- treeWalk.getObjectId(idBuf, 1);
- return splitBlameWithParent(n, parent);
- }
- } else 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.setSourcePath(PathFilter.create(r.getOldPath()));
- push(n);
- return false;
- }
-
- Candidate next = n.create(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(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.sourceCommit = parent;
- n.setSourcePath(PathFilter.create(r.getOldPath()));
- push(n);
- return false;
- }
-
- 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(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(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;
- push(p);
- return false;
- }
-
- 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;
- }
-
- /** @return current author being blamed. */
- public PersonIdent getSourceAuthor() {
- return outCandidate.getAuthor();
- }
-
- /** @return current committer being blamed. */
- public PersonIdent getSourceCommitter() {
- RevCommit c = getSourceCommit();
- return c != null ? c.getCommitterIdent() : null;
- }
-
- /** @return path of the file being blamed. */
- public String getSourcePath() {
- return outCandidate.sourcePath.getPath();
- }
-
- /** @return rename score if a rename occurred in {@link #getSourceCommit}. */
- public int getRenameScore() {
- return outCandidate.renameScore;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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;
- }
-
- /**
- * @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 IOException
- * repository cannot be read.
- * @throws IllegalStateException
- * {@link #next()} has already been invoked.
- */
- public RawText getResultContents() throws IOException {
- return queue != null ? queue.sourceText : null;
- }
-
- /** Release the current blame session. */
- public void release() {
- revPool.release();
- 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_FILE) == 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;
- }
-
- private static final TreeFilter ID_DIFF = new TreeFilter() {
- @Override
- public boolean include(TreeWalk tw) {
- return !tw.idEqual(0, 1);
- }
-
- @Override
- public boolean shouldBeRecursive() {
- return false;
- }
-
- @Override
- public TreeFilter clone() {
- return this;
- }
-
- @Override
- public String toString() {
- return "ID_DIFF"; //$NON-NLS-1$
- }
- };
- }
|