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.revwalk.RevCommit;
59  import org.eclipse.jgit.revwalk.RevFlag;
60  import org.eclipse.jgit.revwalk.RevWalk;
61  import org.eclipse.jgit.treewalk.filter.PathFilter;
62  
63  /**
64   * A source that may have supplied some (or all) of the result file.
65   * <p>
66   * Candidates are kept in a queue by BlameGenerator, allowing the generator to
67   * perform a parallel search down the parents of any merges that are discovered
68   * during the history traversal. Each candidate retains a {@link #regionList}
69   * describing sections of the result file the candidate has taken responsibility
70   * for either directly or indirectly through its history. Actual blame from this
71   * region list will be assigned to the candidate when its ancestor commit(s) are
72   * themselves converted into Candidate objects and the ancestor's candidate uses
73   * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections
74   * of the result.
75   */
76  class Candidate {
77  	/** Next candidate in the candidate queue. */
78  	Candidate queueNext;
79  
80  	/** Commit being considered (or blamed, depending on state). */
81  	RevCommit sourceCommit;
82  
83  	/** Path of the candidate file in {@link #sourceCommit}. */
84  	PathFilter sourcePath;
85  
86  	/** Unique name of the candidate blob in {@link #sourceCommit}. */
87  	ObjectId sourceBlob;
88  
89  	/** Complete contents of the file in {@link #sourceCommit}. */
90  	RawText sourceText;
91  
92  	/**
93  	 * Chain of regions this candidate may be blamed for.
94  	 * <p>
95  	 * This list is always kept sorted by resultStart order, making it simple to
96  	 * merge-join with the sorted EditList during blame assignment.
97  	 */
98  	Region regionList;
99  
100 	/**
101 	 * Score assigned to the rename to this candidate.
102 	 * <p>
103 	 * Consider the history "A<-B<-C". If the result file S in C was renamed to
104 	 * R in B, the rename score for this rename will be held in this field by
105 	 * the candidate object for B. By storing the score with B, the application
106 	 * can see what the rename score was as it makes the transition from C/S to
107 	 * B/R. This may seem backwards since it was C that performed the rename,
108 	 * but the application doesn't learn about path R until B.
109 	 */
110 	int renameScore;
111 
112 	Candidate(RevCommit commit, PathFilter path) {
113 		sourceCommit = commit;
114 		sourcePath = path;
115 	}
116 
117 	void beginResult(RevWalk rw) throws MissingObjectException, IOException {
118 		rw.parseBody(sourceCommit);
119 	}
120 
121 	int getParentCount() {
122 		return sourceCommit.getParentCount();
123 	}
124 
125 	RevCommit getParent(int idx) {
126 		return sourceCommit.getParent(idx);
127 	}
128 
129 	Candidate getNextCandidate(@SuppressWarnings("unused") int idx) {
130 		return null;
131 	}
132 
133 	boolean has(RevFlag flag) {
134 		return sourceCommit.has(flag);
135 	}
136 
137 	void add(RevFlag flag) {
138 		sourceCommit.add(flag);
139 	}
140 
141 	void remove(RevFlag flag) {
142 		sourceCommit.remove(flag);
143 	}
144 
145 	int getTime() {
146 		return sourceCommit.getCommitTime();
147 	}
148 
149 	PersonIdent getAuthor() {
150 		return sourceCommit.getAuthorIdent();
151 	}
152 
153 	Candidate create(RevCommit commit, PathFilter path) {
154 		return new Candidate(commit, path);
155 	}
156 
157 	Candidate copy(RevCommit commit) {
158 		Candidate r = create(commit, sourcePath);
159 		r.sourceBlob = sourceBlob;
160 		r.sourceText = sourceText;
161 		r.regionList = regionList;
162 		r.renameScore = renameScore;
163 		return r;
164 	}
165 
166 	void loadText(ObjectReader reader) throws IOException {
167 		ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB);
168 		sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
169 	}
170 
171 	void takeBlame(EditList editList, Candidate child) {
172 		blame(editList, this, child);
173 	}
174 
175 	private static void blame(EditList editList, Candidate a, Candidate b) {
176 		Region r = b.clearRegionList();
177 		Region aTail = null;
178 		Region bTail = null;
179 
180 		for (int eIdx = 0; eIdx < editList.size();) {
181 			// If there are no more regions left, neither side has any
182 			// more responsibility for the result. Remaining edits can
183 			// be safely ignored.
184 			if (r == null)
185 				return;
186 
187 			Edit e = editList.get(eIdx);
188 
189 			// Edit ends before the next candidate region. Skip the edit.
190 			if (e.getEndB() <= r.sourceStart) {
191 				eIdx++;
192 				continue;
193 			}
194 
195 			// Next candidate region starts before the edit. Assign some
196 			// of the blame onto A, but possibly split and also on B.
197 			if (r.sourceStart < e.getBeginB()) {
198 				int d = e.getBeginB() - r.sourceStart;
199 				if (r.length <= d) {
200 					// Pass the blame for this region onto A.
201 					Region next = r.next;
202 					r.sourceStart = e.getBeginA() - d;
203 					aTail = add(aTail, a, r);
204 					r = next;
205 					continue;
206 				}
207 
208 				// Split the region and assign some to A, some to B.
209 				aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d));
210 				r.slideAndShrink(d);
211 			}
212 
213 			// At this point e.getBeginB() <= r.sourceStart.
214 
215 			// An empty edit on the B side isn't relevant to this split,
216 			// as it does not overlap any candidate region.
217 			if (e.getLengthB() == 0) {
218 				eIdx++;
219 				continue;
220 			}
221 
222 			// If the region ends before the edit, blame on B.
223 			int rEnd = r.sourceStart + r.length;
224 			if (rEnd <= e.getEndB()) {
225 				Region next = r.next;
226 				bTail = add(bTail, b, r);
227 				r = next;
228 				if (rEnd == e.getEndB())
229 					eIdx++;
230 				continue;
231 			}
232 
233 			// This region extends beyond the edit. Blame the first
234 			// half of the region on B, and process the rest after.
235 			int len = e.getEndB() - r.sourceStart;
236 			bTail = add(bTail, b, r.splitFirst(r.sourceStart, len));
237 			r.slideAndShrink(len);
238 			eIdx++;
239 		}
240 
241 		if (r == null)
242 			return;
243 
244 		// For any remaining region, pass the blame onto A after shifting
245 		// the source start to account for the difference between the two.
246 		Edit e = editList.get(editList.size() - 1);
247 		int endB = e.getEndB();
248 		int d = endB - e.getEndA();
249 		if (aTail == null)
250 			a.regionList = r;
251 		else
252 			aTail.next = r;
253 		do {
254 			if (endB <= r.sourceStart)
255 				r.sourceStart -= d;
256 			r = r.next;
257 		} while (r != null);
258 	}
259 
260 	private static Region add(Region aTail, Candidate a, Region n) {
261 		// If there is no region on the list, use only this one.
262 		if (aTail == null) {
263 			a.regionList = n;
264 			n.next = null;
265 			return n;
266 		}
267 
268 		// If the prior region ends exactly where the new region begins
269 		// in both the result and the source, combine these together into
270 		// one contiguous region. This occurs when intermediate commits
271 		// have inserted and deleted lines in the middle of a region. Try
272 		// to report this region as a single region to the application,
273 		// rather than in fragments.
274 		if (aTail.resultStart + aTail.length == n.resultStart
275 				&& aTail.sourceStart + aTail.length == n.sourceStart) {
276 			aTail.length += n.length;
277 			return aTail;
278 		}
279 
280 		// Append the region onto the end of the list.
281 		aTail.next = n;
282 		n.next = null;
283 		return n;
284 	}
285 
286 	private Region clearRegionList() {
287 		Region r = regionList;
288 		regionList = null;
289 		return r;
290 	}
291 
292 	boolean canMergeRegions(Candidate other) {
293 		return sourceCommit == other.sourceCommit
294 				&& sourcePath.getPath().equals(other.sourcePath.getPath());
295 	}
296 
297 	void mergeRegions(Candidate other) {
298 		// regionList is always sorted by resultStart. Merge join two
299 		// linked lists, preserving the ordering. Combine neighboring
300 		// regions to reduce the number of results seen by callers.
301 		Region a = clearRegionList();
302 		Region b = other.clearRegionList();
303 		Region t = null;
304 
305 		while (a != null && b != null) {
306 			if (a.resultStart < b.resultStart) {
307 				Region n = a.next;
308 				t = add(t, this, a);
309 				a = n;
310 			} else {
311 				Region n = b.next;
312 				t = add(t, this, b);
313 				b = n;
314 			}
315 		}
316 
317 		if (a != null) {
318 			Region n = a.next;
319 			t = add(t, this, a);
320 			t.next = n;
321 		} else /* b != null */{
322 			Region n = b.next;
323 			t = add(t, this, b);
324 			t.next = n;
325 		}
326 	}
327 
328 	@SuppressWarnings("nls")
329 	@Override
330 	public String toString() {
331 		StringBuilder r = new StringBuilder();
332 		r.append("Candidate[");
333 		r.append(sourcePath.getPath());
334 		if (sourceCommit != null)
335 			r.append(" @ ").append(sourceCommit.abbreviate(6).name());
336 		if (regionList != null)
337 			r.append(" regions:").append(regionList);
338 		r.append("]");
339 		return r.toString();
340 	}
341 
342 	/**
343 	 * Special candidate type used for reverse blame.
344 	 * <p>
345 	 * Reverse blame inverts the commit history graph to follow from a commit to
346 	 * its descendant children, rather than the normal history direction of
347 	 * child to parent. These types require a {@link ReverseCommit} which keeps
348 	 * children pointers, allowing reverse navigation of history.
349 	 */
350 	static final class ReverseCandidate extends Candidate {
351 		ReverseCandidate(ReverseCommit commit, PathFilter path) {
352 			super(commit, path);
353 		}
354 
355 		@Override
356 		int getParentCount() {
357 			return ((ReverseCommit) sourceCommit).getChildCount();
358 		}
359 
360 		@Override
361 		RevCommit getParent(int idx) {
362 			return ((ReverseCommit) sourceCommit).getChild(idx);
363 		}
364 
365 		@Override
366 		int getTime() {
367 			// Invert the timestamp so newer dates sort older.
368 			return -sourceCommit.getCommitTime();
369 		}
370 
371 		@Override
372 		Candidate create(RevCommit commit, PathFilter path) {
373 			return new ReverseCandidate((ReverseCommit) commit, path);
374 		}
375 
376 		@Override
377 		public String toString() {
378 			return "Reverse" + super.toString(); //$NON-NLS-1$
379 		}
380 	}
381 
382 	/**
383 	 * Candidate loaded from a file source, and not a commit.
384 	 * <p>
385 	 * The {@link Candidate#sourceCommit} field is always null on this type of
386 	 * candidate. Instead history traversal follows the single {@link #parent}
387 	 * field to discover the next Candidate. Often this is a normal Candidate
388 	 * type that has a valid sourceCommit.
389 	 */
390 	static final class BlobCandidate extends Candidate {
391 		/**
392 		 * Next candidate to pass blame onto.
393 		 * <p>
394 		 * When computing the differences that this candidate introduced to the
395 		 * file content, the parent's sourceText is used as the base.
396 		 */
397 		Candidate parent;
398 
399 		/** Author name to refer to this blob with. */
400 		String description;
401 
402 		BlobCandidate(String name, PathFilter path) {
403 			super(null, path);
404 			description = name;
405 		}
406 
407 		@Override
408 		void beginResult(RevWalk rw) {
409 			// Blob candidates have nothing to prepare.
410 		}
411 
412 		@Override
413 		int getParentCount() {
414 			return parent != null ? 1 : 0;
415 		}
416 
417 		@Override
418 		RevCommit getParent(int idx) {
419 			return null;
420 		}
421 
422 		@Override
423 		Candidate getNextCandidate(int idx) {
424 			return parent;
425 		}
426 
427 		@Override
428 		boolean has(RevFlag flag) {
429 			return true; // Pretend flag was added; sourceCommit is null.
430 		}
431 
432 		@Override
433 		void add(RevFlag flag) {
434 			// Do nothing, sourceCommit is null.
435 		}
436 
437 		@Override
438 
439 		void remove(RevFlag flag) {
440 			// Do nothing, sourceCommit is null.
441 		}
442 
443 		@Override
444 		int getTime() {
445 			return Integer.MAX_VALUE;
446 		}
447 
448 		@Override
449 		PersonIdent getAuthor() {
450 			return new PersonIdent(description, ""); //$NON-NLS-1$
451 		}
452 	}
453 }