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