| /* |
| * 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 java.io.IOException; |
| |
| import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; |
| import org.eclipse.jgit.diff.Edit; |
| import org.eclipse.jgit.diff.EditList; |
| import org.eclipse.jgit.diff.RawText; |
| import org.eclipse.jgit.lib.Constants; |
| 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.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevFlag; |
| import org.eclipse.jgit.treewalk.filter.PathFilter; |
| |
| /** |
| * A source that may have supplied some (or all) of the result file. |
| * <p> |
| * Candidates are kept in a queue by BlameGenerator, allowing the generator to |
| * perform a parallel search down the parents of any merges that are discovered |
| * during the history traversal. Each candidate retains a {@link #regionList} |
| * describing sections of the result file the candidate has taken responsibility |
| * for either directly or indirectly through its history. Actual blame from this |
| * region list will be assigned to the candidate when its ancestor commit(s) are |
| * themselves converted into Candidate objects and the ancestor's candidate uses |
| * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections |
| * of the result. |
| */ |
| class Candidate { |
| /** Next candidate in the candidate queue. */ |
| Candidate queueNext; |
| |
| /** Commit being considered (or blamed, depending on state). */ |
| RevCommit sourceCommit; |
| |
| /** Path of the candidate file in {@link #sourceCommit}. */ |
| PathFilter sourcePath; |
| |
| /** Unique name of the candidate blob in {@link #sourceCommit}. */ |
| ObjectId sourceBlob; |
| |
| /** Complete contents of the file in {@link #sourceCommit}. */ |
| RawText sourceText; |
| |
| /** |
| * Chain of regions this candidate may be blamed for. |
| * <p> |
| * This list is always kept sorted by resultStart order, making it simple to |
| * merge-join with the sorted EditList during blame assignment. |
| */ |
| Region regionList; |
| |
| /** |
| * Score assigned to the rename to this candidate. |
| * <p> |
| * Consider the history "A<-B<-C". If the result file S in C was renamed to |
| * R in B, the rename score for this rename will be held in this field by |
| * the candidate object for B. By storing the score with B, the application |
| * can see what the rename score was as it makes the transition from C/S to |
| * B/R. This may seem backwards since it was C that performed the rename, |
| * but the application doesn't learn about path R until B. |
| */ |
| int renameScore; |
| |
| Candidate(RevCommit commit, PathFilter path) { |
| sourceCommit = commit; |
| sourcePath = path; |
| } |
| |
| int getParentCount() { |
| return sourceCommit.getParentCount(); |
| } |
| |
| RevCommit getParent(int idx) { |
| return sourceCommit.getParent(idx); |
| } |
| |
| Candidate getNextCandidate(@SuppressWarnings("unused") int idx) { |
| return null; |
| } |
| |
| void add(RevFlag flag) { |
| sourceCommit.add(flag); |
| } |
| |
| int getTime() { |
| return sourceCommit.getCommitTime(); |
| } |
| |
| PersonIdent getAuthor() { |
| return sourceCommit.getAuthorIdent(); |
| } |
| |
| Candidate create(RevCommit commit, PathFilter path) { |
| return new Candidate(commit, path); |
| } |
| |
| Candidate copy(RevCommit commit) { |
| Candidate r = create(commit, sourcePath); |
| r.sourceBlob = sourceBlob; |
| r.sourceText = sourceText; |
| r.regionList = regionList; |
| r.renameScore = renameScore; |
| return r; |
| } |
| |
| void loadText(ObjectReader reader) throws IOException { |
| ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB); |
| sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); |
| } |
| |
| void takeBlame(EditList editList, Candidate child) { |
| blame(editList, this, child); |
| } |
| |
| private static void blame(EditList editList, Candidate a, Candidate b) { |
| Region r = b.clearRegionList(); |
| Region aTail = null; |
| Region bTail = null; |
| |
| for (int eIdx = 0; eIdx < editList.size();) { |
| // If there are no more regions left, neither side has any |
| // more responsibility for the result. Remaining edits can |
| // be safely ignored. |
| if (r == null) |
| return; |
| |
| Edit e = editList.get(eIdx); |
| |
| // Edit ends before the next candidate region. Skip the edit. |
| if (e.getEndB() <= r.sourceStart) { |
| eIdx++; |
| continue; |
| } |
| |
| // Next candidate region starts before the edit. Assign some |
| // of the blame onto A, but possibly split and also on B. |
| if (r.sourceStart < e.getBeginB()) { |
| int d = e.getBeginB() - r.sourceStart; |
| if (r.length <= d) { |
| // Pass the blame for this region onto A. |
| Region next = r.next; |
| r.sourceStart = e.getBeginA() - d; |
| aTail = add(aTail, a, r); |
| r = next; |
| continue; |
| } |
| |
| // Split the region and assign some to A, some to B. |
| aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d)); |
| r.slideAndShrink(d); |
| } |
| |
| // At this point e.getBeginB() <= r.sourceStart. |
| |
| // An empty edit on the B side isn't relevant to this split, |
| // as it does not overlap any candidate region. |
| if (e.getLengthB() == 0) { |
| eIdx++; |
| continue; |
| } |
| |
| // If the region ends before the edit, blame on B. |
| int rEnd = r.sourceStart + r.length; |
| if (rEnd <= e.getEndB()) { |
| Region next = r.next; |
| bTail = add(bTail, b, r); |
| r = next; |
| if (rEnd == e.getEndB()) |
| eIdx++; |
| continue; |
| } |
| |
| // This region extends beyond the edit. Blame the first |
| // half of the region on B, and process the rest after. |
| int len = e.getEndB() - r.sourceStart; |
| bTail = add(bTail, b, r.splitFirst(r.sourceStart, len)); |
| r.slideAndShrink(len); |
| eIdx++; |
| } |
| |
| if (r == null) |
| return; |
| |
| // For any remaining region, pass the blame onto A after shifting |
| // the source start to account for the difference between the two. |
| Edit e = editList.get(editList.size() - 1); |
| int endB = e.getEndB(); |
| int d = endB - e.getEndA(); |
| if (aTail == null) |
| a.regionList = r; |
| else |
| aTail.next = r; |
| do { |
| if (endB <= r.sourceStart) |
| r.sourceStart -= d; |
| r = r.next; |
| } while (r != null); |
| } |
| |
| private static Region add(Region aTail, Candidate a, Region n) { |
| // If there is no region on the list, use only this one. |
| if (aTail == null) { |
| a.regionList = n; |
| n.next = null; |
| return n; |
| } |
| |
| // If the prior region ends exactly where the new region begins |
| // in both the result and the source, combine these together into |
| // one contiguous region. This occurs when intermediate commits |
| // have inserted and deleted lines in the middle of a region. Try |
| // to report this region as a single region to the application, |
| // rather than in fragments. |
| if (aTail.resultStart + aTail.length == n.resultStart |
| && aTail.sourceStart + aTail.length == n.sourceStart) { |
| aTail.length += n.length; |
| return aTail; |
| } |
| |
| // Append the region onto the end of the list. |
| aTail.next = n; |
| n.next = null; |
| return n; |
| } |
| |
| private Region clearRegionList() { |
| Region r = regionList; |
| regionList = null; |
| return r; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder r = new StringBuilder(); |
| r.append("Candidate["); |
| r.append(sourcePath.getPath()); |
| if (sourceCommit != null) |
| r.append(" @ ").append(sourceCommit.abbreviate(6).name()); |
| if (regionList != null) |
| r.append(" regions:").append(regionList); |
| r.append("]"); |
| return r.toString(); |
| } |
| |
| /** |
| * Special candidate type used for reverse blame. |
| * <p> |
| * Reverse blame inverts the commit history graph to follow from a commit to |
| * its descendant children, rather than the normal history direction of |
| * child to parent. These types require a {@link ReverseCommit} which keeps |
| * children pointers, allowing reverse navigation of history. |
| */ |
| static final class ReverseCandidate extends Candidate { |
| ReverseCandidate(ReverseCommit commit, PathFilter path) { |
| super(commit, path); |
| } |
| |
| @Override |
| int getParentCount() { |
| return ((ReverseCommit) sourceCommit).getChildCount(); |
| } |
| |
| @Override |
| RevCommit getParent(int idx) { |
| return ((ReverseCommit) sourceCommit).getChild(idx); |
| } |
| |
| @Override |
| int getTime() { |
| // Invert the timestamp so newer dates sort older. |
| return -sourceCommit.getCommitTime(); |
| } |
| |
| @Override |
| Candidate create(RevCommit commit, PathFilter path) { |
| return new ReverseCandidate((ReverseCommit) commit, path); |
| } |
| |
| @Override |
| public String toString() { |
| return "Reverse" + super.toString(); |
| } |
| } |
| |
| /** |
| * Candidate loaded from a file source, and not a commit. |
| * <p> |
| * The {@link Candidate#sourceCommit} field is always null on this type of |
| * candidate. Instead history traversal follows the single {@link #parent} |
| * field to discover the next Candidate. Often this is a normal Candidate |
| * type that has a valid sourceCommit. |
| */ |
| static final class BlobCandidate extends Candidate { |
| /** |
| * Next candidate to pass blame onto. |
| * <p> |
| * When computing the differences that this candidate introduced to the |
| * file content, the parent's sourceText is used as the base. |
| */ |
| Candidate parent; |
| |
| /** Author name to refer to this blob with. */ |
| String description; |
| |
| BlobCandidate(String name, PathFilter path) { |
| super(null, path); |
| description = name; |
| } |
| |
| @Override |
| int getParentCount() { |
| return parent != null ? 1 : 0; |
| } |
| |
| @Override |
| RevCommit getParent(int idx) { |
| return null; |
| } |
| |
| @Override |
| Candidate getNextCandidate(int idx) { |
| return parent; |
| } |
| |
| @Override |
| void add(RevFlag flag) { |
| // Do nothing, sourceCommit is null. |
| } |
| |
| @Override |
| int getTime() { |
| return Integer.MAX_VALUE; |
| } |
| |
| @Override |
| PersonIdent getAuthor() { |
| return new PersonIdent(description, null); |
| } |
| } |
| } |