View Javadoc
1   /*
2    * Copyright (C) 2011, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.blame;
45  
46  import java.io.IOException;
47  
48  import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
49  import org.eclipse.jgit.diff.Edit;
50  import org.eclipse.jgit.diff.EditList;
51  import org.eclipse.jgit.diff.RawText;
52  import org.eclipse.jgit.errors.MissingObjectException;
53  import org.eclipse.jgit.lib.Constants;
54  import org.eclipse.jgit.lib.ObjectId;
55  import org.eclipse.jgit.lib.ObjectLoader;
56  import org.eclipse.jgit.lib.ObjectReader;
57  import org.eclipse.jgit.lib.PersonIdent;
58  import org.eclipse.jgit.lib.Repository;
59  import org.eclipse.jgit.revwalk.RevCommit;
60  import org.eclipse.jgit.revwalk.RevFlag;
61  import org.eclipse.jgit.revwalk.RevWalk;
62  import org.eclipse.jgit.treewalk.filter.PathFilter;
63  import org.eclipse.jgit.util.LfsFactory;
64  
65  /**
66   * A source that may have supplied some (or all) of the result file.
67   * <p>
68   * Candidates are kept in a queue by BlameGenerator, allowing the generator to
69   * perform a parallel search down the parents of any merges that are discovered
70   * during the history traversal. Each candidate retains a {@link #regionList}
71   * describing sections of the result file the candidate has taken responsibility
72   * for either directly or indirectly through its history. Actual blame from this
73   * region list will be assigned to the candidate when its ancestor commit(s) are
74   * themselves converted into Candidate objects and the ancestor's candidate uses
75   * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections
76   * of the result.
77   */
78  class Candidate {
79  	/** Next candidate in the candidate queue. */
80  	Candidate queueNext;
81  
82  	/** Commit being considered (or blamed, depending on state). */
83  	RevCommit sourceCommit;
84  
85  	/** Path of the candidate file in {@link #sourceCommit}. */
86  	PathFilter sourcePath;
87  
88  	/** Unique name of the candidate blob in {@link #sourceCommit}. */
89  	ObjectId sourceBlob;
90  
91  	/** Complete contents of the file in {@link #sourceCommit}. */
92  	RawText sourceText;
93  
94  	/**
95  	 * Chain of regions this candidate may be blamed for.
96  	 * <p>
97  	 * This list is always kept sorted by resultStart order, making it simple to
98  	 * merge-join with the sorted EditList during blame assignment.
99  	 */
100 	Region regionList;
101 
102 	/**
103 	 * Score assigned to the rename to this candidate.
104 	 * <p>
105 	 * Consider the history "A<-B<-C". If the result file S in C was renamed to
106 	 * R in B, the rename score for this rename will be held in this field by
107 	 * the candidate object for B. By storing the score with B, the application
108 	 * can see what the rename score was as it makes the transition from C/S to
109 	 * B/R. This may seem backwards since it was C that performed the rename,
110 	 * but the application doesn't learn about path R until B.
111 	 */
112 	int renameScore;
113 
114 	/** repository used for LFS blob handling */
115 	private Repository sourceRepository;
116 
117 	Candidate(Repository repo, RevCommit commit, PathFilter path) {
118 		sourceRepository = repo;
119 		sourceCommit = commit;
120 		sourcePath = path;
121 	}
122 
123 	void beginResult(RevWalk rw) throws MissingObjectException, IOException {
124 		rw.parseBody(sourceCommit);
125 	}
126 
127 	int getParentCount() {
128 		return sourceCommit.getParentCount();
129 	}
130 
131 	RevCommit getParent(int idx) {
132 		return sourceCommit.getParent(idx);
133 	}
134 
135 	Candidate getNextCandidate(@SuppressWarnings("unused") int idx) {
136 		return null;
137 	}
138 
139 	boolean has(RevFlag flag) {
140 		return sourceCommit.has(flag);
141 	}
142 
143 	void add(RevFlag flag) {
144 		sourceCommit.add(flag);
145 	}
146 
147 	void remove(RevFlag flag) {
148 		sourceCommit.remove(flag);
149 	}
150 
151 	int getTime() {
152 		return sourceCommit.getCommitTime();
153 	}
154 
155 	PersonIdent getAuthor() {
156 		return sourceCommit.getAuthorIdent();
157 	}
158 
159 	Candidate create(Repository repo, RevCommit commit, PathFilter path) {
160 		return new Candidate(repo, commit, path);
161 	}
162 
163 	Candidate copy(RevCommit commit) {
164 		Candidate r = create(sourceRepository, commit, sourcePath);
165 		r.sourceBlob = sourceBlob;
166 		r.sourceText = sourceText;
167 		r.regionList = regionList;
168 		r.renameScore = renameScore;
169 		return r;
170 	}
171 
172 	void loadText(ObjectReader reader) throws IOException {
173 		ObjectLoader ldr = LfsFactory.getInstance().applySmudgeFilter(sourceRepository,
174 				reader.open(sourceBlob, Constants.OBJ_BLOB),
175 				LfsFactory.getAttributesForPath(sourceRepository,
176 						sourcePath.getPath(), sourceCommit)
177 						.get(Constants.ATTR_DIFF));
178 		sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
179 	}
180 
181 	void takeBlame(EditList editList, Candidate child) {
182 		blame(editList, this, child);
183 	}
184 
185 	private static void blame(EditList editList, Candidate a, Candidate b) {
186 		Region r = b.clearRegionList();
187 		Region aTail = null;
188 		Region bTail = null;
189 
190 		for (int eIdx = 0; eIdx < editList.size();) {
191 			// If there are no more regions left, neither side has any
192 			// more responsibility for the result. Remaining edits can
193 			// be safely ignored.
194 			if (r == null)
195 				return;
196 
197 			Edit e = editList.get(eIdx);
198 
199 			// Edit ends before the next candidate region. Skip the edit.
200 			if (e.getEndB() <= r.sourceStart) {
201 				eIdx++;
202 				continue;
203 			}
204 
205 			// Next candidate region starts before the edit. Assign some
206 			// of the blame onto A, but possibly split and also on B.
207 			if (r.sourceStart < e.getBeginB()) {
208 				int d = e.getBeginB() - r.sourceStart;
209 				if (r.length <= d) {
210 					// Pass the blame for this region onto A.
211 					Region next = r.next;
212 					r.sourceStart = e.getBeginA() - d;
213 					aTail = add(aTail, a, r);
214 					r = next;
215 					continue;
216 				}
217 
218 				// Split the region and assign some to A, some to B.
219 				aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d));
220 				r.slideAndShrink(d);
221 			}
222 
223 			// At this point e.getBeginB() <= r.sourceStart.
224 
225 			// An empty edit on the B side isn't relevant to this split,
226 			// as it does not overlap any candidate region.
227 			if (e.getLengthB() == 0) {
228 				eIdx++;
229 				continue;
230 			}
231 
232 			// If the region ends before the edit, blame on B.
233 			int rEnd = r.sourceStart + r.length;
234 			if (rEnd <= e.getEndB()) {
235 				Region next = r.next;
236 				bTail = add(bTail, b, r);
237 				r = next;
238 				if (rEnd == e.getEndB())
239 					eIdx++;
240 				continue;
241 			}
242 
243 			// This region extends beyond the edit. Blame the first
244 			// half of the region on B, and process the rest after.
245 			int len = e.getEndB() - r.sourceStart;
246 			bTail = add(bTail, b, r.splitFirst(r.sourceStart, len));
247 			r.slideAndShrink(len);
248 			eIdx++;
249 		}
250 
251 		if (r == null)
252 			return;
253 
254 		// For any remaining region, pass the blame onto A after shifting
255 		// the source start to account for the difference between the two.
256 		Edit e = editList.get(editList.size() - 1);
257 		int endB = e.getEndB();
258 		int d = endB - e.getEndA();
259 		if (aTail == null)
260 			a.regionList = r;
261 		else
262 			aTail.next = r;
263 		do {
264 			if (endB <= r.sourceStart)
265 				r.sourceStart -= d;
266 			r = r.next;
267 		} while (r != null);
268 	}
269 
270 	private static Region add(Region aTail, Candidate a, Region n) {
271 		// If there is no region on the list, use only this one.
272 		if (aTail == null) {
273 			a.regionList = n;
274 			n.next = null;
275 			return n;
276 		}
277 
278 		// If the prior region ends exactly where the new region begins
279 		// in both the result and the source, combine these together into
280 		// one contiguous region. This occurs when intermediate commits
281 		// have inserted and deleted lines in the middle of a region. Try
282 		// to report this region as a single region to the application,
283 		// rather than in fragments.
284 		if (aTail.resultStart + aTail.length == n.resultStart
285 				&& aTail.sourceStart + aTail.length == n.sourceStart) {
286 			aTail.length += n.length;
287 			return aTail;
288 		}
289 
290 		// Append the region onto the end of the list.
291 		aTail.next = n;
292 		n.next = null;
293 		return n;
294 	}
295 
296 	private Region clearRegionList() {
297 		Region r = regionList;
298 		regionList = null;
299 		return r;
300 	}
301 
302 	boolean canMergeRegions(Candidate other) {
303 		return sourceCommit == other.sourceCommit
304 				&& sourcePath.getPath().equals(other.sourcePath.getPath());
305 	}
306 
307 	void mergeRegions(Candidate other) {
308 		// regionList is always sorted by resultStart. Merge join two
309 		// linked lists, preserving the ordering. Combine neighboring
310 		// regions to reduce the number of results seen by callers.
311 		Region a = clearRegionList();
312 		Region b = other.clearRegionList();
313 		Region t = null;
314 
315 		while (a != null && b != null) {
316 			if (a.resultStart < b.resultStart) {
317 				Region n = a.next;
318 				t = add(t, this, a);
319 				a = n;
320 			} else {
321 				Region n = b.next;
322 				t = add(t, this, b);
323 				b = n;
324 			}
325 		}
326 
327 		if (a != null) {
328 			Region n = a.next;
329 			t = add(t, this, a);
330 			t.next = n;
331 		} else /* b != null */{
332 			Region n = b.next;
333 			t = add(t, this, b);
334 			t.next = n;
335 		}
336 	}
337 
338 	/** {@inheritDoc} */
339 	@SuppressWarnings("nls")
340 	@Override
341 	public String toString() {
342 		StringBuilder r = new StringBuilder();
343 		r.append("Candidate[");
344 		r.append(sourcePath.getPath());
345 		if (sourceCommit != null)
346 			r.append(" @ ").append(sourceCommit.abbreviate(6).name());
347 		if (regionList != null)
348 			r.append(" regions:").append(regionList);
349 		r.append("]");
350 		return r.toString();
351 	}
352 
353 	/**
354 	 * Special candidate type used for reverse blame.
355 	 * <p>
356 	 * Reverse blame inverts the commit history graph to follow from a commit to
357 	 * its descendant children, rather than the normal history direction of
358 	 * child to parent. These types require a {@link ReverseCommit} which keeps
359 	 * children pointers, allowing reverse navigation of history.
360 	 */
361 	static final class ReverseCandidate extends Candidate {
362 		ReverseCandidate(Repository repo, ReverseCommit commit,
363 				PathFilter path) {
364 			super(repo, commit, path);
365 		}
366 
367 		@Override
368 		int getParentCount() {
369 			return ((ReverseCommit) sourceCommit).getChildCount();
370 		}
371 
372 		@Override
373 		RevCommit getParent(int idx) {
374 			return ((ReverseCommit) sourceCommit).getChild(idx);
375 		}
376 
377 		@Override
378 		int getTime() {
379 			// Invert the timestamp so newer dates sort older.
380 			return -sourceCommit.getCommitTime();
381 		}
382 
383 		@Override
384 		Candidate create(Repository repo, RevCommit commit, PathFilter path) {
385 			return new ReverseCandidate(repo, (ReverseCommit) commit, path);
386 		}
387 
388 		@Override
389 		public String toString() {
390 			return "Reverse" + super.toString(); //$NON-NLS-1$
391 		}
392 	}
393 
394 	/**
395 	 * Candidate loaded from a file source, and not a commit.
396 	 * <p>
397 	 * The {@link Candidate#sourceCommit} field is always null on this type of
398 	 * candidate. Instead history traversal follows the single {@link #parent}
399 	 * field to discover the next Candidate. Often this is a normal Candidate
400 	 * type that has a valid sourceCommit.
401 	 */
402 	static final class BlobCandidate extends Candidate {
403 		/**
404 		 * Next candidate to pass blame onto.
405 		 * <p>
406 		 * When computing the differences that this candidate introduced to the
407 		 * file content, the parent's sourceText is used as the base.
408 		 */
409 		Candidate parent;
410 
411 		/** Author name to refer to this blob with. */
412 		String description;
413 
414 		BlobCandidate(Repository repo, String name, PathFilter path) {
415 			super(repo, null, path);
416 			description = name;
417 		}
418 
419 		@Override
420 		void beginResult(RevWalk rw) {
421 			// Blob candidates have nothing to prepare.
422 		}
423 
424 		@Override
425 		int getParentCount() {
426 			return parent != null ? 1 : 0;
427 		}
428 
429 		@Override
430 		RevCommit getParent(int idx) {
431 			return null;
432 		}
433 
434 		@Override
435 		Candidate getNextCandidate(int idx) {
436 			return parent;
437 		}
438 
439 		@Override
440 		boolean has(RevFlag flag) {
441 			return true; // Pretend flag was added; sourceCommit is null.
442 		}
443 
444 		@Override
445 		void add(RevFlag flag) {
446 			// Do nothing, sourceCommit is null.
447 		}
448 
449 		@Override
450 
451 		void remove(RevFlag flag) {
452 			// Do nothing, sourceCommit is null.
453 		}
454 
455 		@Override
456 		int getTime() {
457 			return Integer.MAX_VALUE;
458 		}
459 
460 		@Override
461 		PersonIdent getAuthor() {
462 			return new PersonIdent(description, ""); //$NON-NLS-1$
463 		}
464 	}
465 }