View Javadoc
1   /*
2    * Copyright (C) 2011, Google Inc.
3    * Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
4    * Copyright (C) 2009, Johannes E. Schindelin
5    * Copyright (C) 2009, Johannes Schindelin <johannes.schindelin@gmx.de> and others
6    *
7    * This program and the accompanying materials are made available under the
8    * terms of the Eclipse Distribution License v. 1.0 which is available at
9    * https://www.eclipse.org/org/documents/edl-v10.php.
10   *
11   * SPDX-License-Identifier: BSD-3-Clause
12   */
13  
14  package org.eclipse.jgit.pgm;
15  
16  import static java.lang.Integer.valueOf;
17  import static java.lang.Long.valueOf;
18  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;
19  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
20  
21  import java.io.IOException;
22  import java.text.MessageFormat;
23  import java.text.SimpleDateFormat;
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.regex.Pattern;
29  
30  import org.eclipse.jgit.api.errors.NoHeadException;
31  import org.eclipse.jgit.blame.BlameGenerator;
32  import org.eclipse.jgit.blame.BlameResult;
33  import org.eclipse.jgit.diff.RawText;
34  import org.eclipse.jgit.diff.RawTextComparator;
35  import org.eclipse.jgit.errors.NoWorkTreeException;
36  import org.eclipse.jgit.lib.Constants;
37  import org.eclipse.jgit.lib.ObjectId;
38  import org.eclipse.jgit.lib.ObjectReader;
39  import org.eclipse.jgit.lib.PersonIdent;
40  import org.eclipse.jgit.pgm.internal.CLIText;
41  import org.eclipse.jgit.revwalk.RevCommit;
42  import org.eclipse.jgit.revwalk.RevFlag;
43  import org.kohsuke.args4j.Argument;
44  import org.kohsuke.args4j.Option;
45  
46  @Command(common = false, usage = "usage_Blame")
47  class Blame extends TextBuiltin {
48  	private RawTextComparator comparator = RawTextComparator.DEFAULT;
49  
50  	@Option(name = "-w", usage = "usage_ignoreWhitespace")
51  	void ignoreAllSpace(@SuppressWarnings("unused") boolean on) {
52  		comparator = RawTextComparator.WS_IGNORE_ALL;
53  	}
54  
55  	@Option(name = "--abbrev", metaVar = "metaVar_n", usage = "usage_abbrevCommits")
56  	private int abbrev;
57  
58  	@Option(name = "-l", usage = "usage_blameLongRevision")
59  	private boolean showLongRevision;
60  
61  	@Option(name = "-t", usage = "usage_blameRawTimestamp")
62  	private boolean showRawTimestamp;
63  
64  	@Option(name = "-b", usage = "usage_blameShowBlankBoundary")
65  	private boolean showBlankBoundary;
66  
67  	@Option(name = "-s", usage = "usage_blameSuppressAuthor")
68  	private boolean noAuthor;
69  
70  	@Option(name = "--show-email", aliases = { "-e" }, usage = "usage_blameShowEmail")
71  	private boolean showAuthorEmail;
72  
73  	@Option(name = "--show-name", aliases = { "-f" }, usage = "usage_blameShowSourcePath")
74  	private boolean showSourcePath;
75  
76  	@Option(name = "--show-number", aliases = { "-n" }, usage = "usage_blameShowSourceLine")
77  	private boolean showSourceLine;
78  
79  	@Option(name = "--root", usage = "usage_blameShowRoot")
80  	private boolean root;
81  
82  	@Option(name = "-L", metaVar = "metaVar_blameL", usage = "usage_blameRange")
83  	private String rangeString;
84  
85  	@Option(name = "--reverse", metaVar = "metaVar_blameReverse", usage = "usage_blameReverse")
86  	private List<RevCommit> reverseRange = new ArrayList<>(2);
87  
88  	@Argument(index = 0, required = false, metaVar = "metaVar_revision")
89  	private String revision;
90  
91  	@Argument(index = 1, required = false, metaVar = "metaVar_file")
92  	private String file;
93  
94  	private final Map<RevCommit, String> abbreviatedCommits = new HashMap<>();
95  
96  	private SimpleDateFormat dateFmt;
97  
98  	private int begin;
99  
100 	private int end;
101 
102 	private BlameResult blame;
103 
104 	/** Used to get a current time stamp for lines without commit. */
105 	private final PersonIdent dummyDate = new PersonIdent("", ""); //$NON-NLS-1$ //$NON-NLS-2$
106 
107 	/** {@inheritDoc} */
108 	@Override
109 	protected void run() {
110 		if (file == null) {
111 			if (revision == null) {
112 				throw die(CLIText.get().fileIsRequired);
113 			}
114 			file = revision;
115 			revision = null;
116 		}
117 
118 		boolean autoAbbrev = abbrev == 0;
119 		if (abbrev == 0) {
120 			abbrev = db.getConfig().getInt("core", "abbrev", //$NON-NLS-1$ //$NON-NLS-2$
121 					OBJECT_ID_ABBREV_STRING_LENGTH);
122 		}
123 		if (!showBlankBoundary) {
124 			root = db.getConfig().getBoolean("blame", "blankboundary", false); //$NON-NLS-1$ //$NON-NLS-2$
125 		}
126 		if (!root) {
127 			root = db.getConfig().getBoolean("blame", "showroot", false); //$NON-NLS-1$ //$NON-NLS-2$
128 		}
129 
130 		if (showRawTimestamp) {
131 			dateFmt = new SimpleDateFormat("ZZZZ"); //$NON-NLS-1$
132 		} else {
133 			dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ"); //$NON-NLS-1$
134 		}
135 
136 		try (ObjectReader reader = db.newObjectReader();
137 				BlameGenerator generator = new BlameGenerator(db, file)) {
138 			RevFlag scanned = generator.newFlag("SCANNED"); //$NON-NLS-1$
139 			generator.setTextComparator(comparator);
140 
141 			if (!reverseRange.isEmpty()) {
142 				RevCommit rangeStart = null;
143 				List<RevCommit> rangeEnd = new ArrayList<>(2);
144 				for (RevCommit c : reverseRange) {
145 					if (c.has(RevFlag.UNINTERESTING)) {
146 						rangeStart = c;
147 					} else {
148 						rangeEnd.add(c);
149 					}
150 				}
151 				generator.reverse(rangeStart, rangeEnd);
152 			} else if (revision != null) {
153 				ObjectId rev = db.resolve(revision + "^{commit}"); //$NON-NLS-1$
154 				if (rev == null) {
155 					throw die(MessageFormat.format(CLIText.get().noSuchRef,
156 							revision));
157 				}
158 				generator.push(null, rev);
159 			} else {
160 				generator.prepareHead();
161 			}
162 
163 			blame = BlameResult.create(generator);
164 			if (blame == null) {
165 				throw die(MessageFormat.format(CLIText.get().noSuchPathInRef,
166 						file, revision != null ? revision : Constants.HEAD));
167 			}
168 			begin = 0;
169 			end = blame.getResultContents().size();
170 			if (rangeString != null) {
171 				parseLineRangeOption();
172 			}
173 			blame.computeRange(begin, end);
174 
175 			int authorWidth = 8;
176 			int dateWidth = 8;
177 			int pathWidth = 1;
178 			int maxSourceLine = 1;
179 			for (int line = begin; line < end; line++) {
180 				RevCommit c = blame.getSourceCommit(line);
181 				if (c != null && !c.has(scanned)) {
182 					c.add(scanned);
183 					if (autoAbbrev) {
184 						abbrev = Math.max(abbrev, uniqueAbbrevLen(reader, c));
185 					}
186 					authorWidth = Math.max(authorWidth, author(line).length());
187 					dateWidth = Math.max(dateWidth, date(line).length());
188 					pathWidth = Math.max(pathWidth, path(line).length());
189 				} else if (c == null) {
190 					authorWidth = Math.max(authorWidth, author(line).length());
191 					dateWidth = Math.max(dateWidth, date(line).length());
192 					pathWidth = Math.max(pathWidth, path(line).length());
193 				}
194 				while (line + 1 < end
195 						&& sameCommit(blame.getSourceCommit(line + 1), c)) {
196 					line++;
197 				}
198 				maxSourceLine = Math.max(maxSourceLine, blame.getSourceLine(line));
199 			}
200 
201 			String pathFmt = MessageFormat.format(" %{0}s", valueOf(pathWidth)); //$NON-NLS-1$
202 			String numFmt = MessageFormat.format(" %{0}d", //$NON-NLS-1$
203 					valueOf(1 + (int) Math.log10(maxSourceLine + 1)));
204 			String lineFmt = MessageFormat.format(" %{0}d) ", //$NON-NLS-1$
205 					valueOf(1 + (int) Math.log10(end + 1)));
206 			String authorFmt = MessageFormat.format(" (%-{0}s %{1}s", //$NON-NLS-1$
207 					valueOf(authorWidth), valueOf(dateWidth));
208 
209 			for (int line = begin; line < end;) {
210 				RevCommit c = blame.getSourceCommit(line);
211 				String commit = abbreviate(reader, c);
212 				String author = null;
213 				String date = null;
214 				if (!noAuthor) {
215 					author = author(line);
216 					date = date(line);
217 				}
218 				do {
219 					outw.print(commit);
220 					if (showSourcePath) {
221 						outw.format(pathFmt, path(line));
222 					}
223 					if (showSourceLine) {
224 						outw.format(numFmt, valueOf(blame.getSourceLine(line) + 1));
225 					}
226 					if (!noAuthor) {
227 						outw.format(authorFmt, author, date);
228 					}
229 					outw.format(lineFmt, valueOf(line + 1));
230 					outw.flush();
231 					blame.getResultContents().writeLine(outs, line);
232 					outs.flush();
233 					outw.print('\n');
234 				} while (++line < end
235 						&& sameCommit(blame.getSourceCommit(line), c));
236 			}
237 		} catch (NoWorkTreeException | NoHeadException | IOException e) {
238 			throw die(e.getMessage(), e);
239 		}
240 	}
241 
242 	@SuppressWarnings("ReferenceEquality")
243 	private static boolean sameCommit(RevCommit a, RevCommit b) {
244 		// Reference comparison is intentional; BlameGenerator uses a single
245 		// RevWalk which caches the RevCommit objects, and if a given commit
246 		// is cached the RevWalk returns the same instance.
247 		return a == b;
248 	}
249 
250 	private int uniqueAbbrevLen(ObjectReader reader, RevCommit commit)
251 			throws IOException {
252 		return reader.abbreviate(commit, abbrev).length();
253 	}
254 
255 	private void parseLineRangeOption() {
256 		String beginStr, endStr;
257 		if (rangeString.startsWith("/")) { //$NON-NLS-1$
258 			int c = rangeString.indexOf("/,", 1); //$NON-NLS-1$
259 			if (c < 0) {
260 				beginStr = rangeString;
261 				endStr = String.valueOf(end);
262 			} else {
263 				beginStr = rangeString.substring(0, c);
264 				endStr = rangeString.substring(c + 2);
265 			}
266 
267 		} else {
268 			int c = rangeString.indexOf(',');
269 			if (c < 0) {
270 				beginStr = rangeString;
271 				endStr = String.valueOf(end);
272 			} else if (c == 0) {
273 				beginStr = "0"; //$NON-NLS-1$
274 				endStr = rangeString.substring(1);
275 			} else {
276 				beginStr = rangeString.substring(0, c);
277 				endStr = rangeString.substring(c + 1);
278 			}
279 		}
280 
281 		if (beginStr.isEmpty())
282 			begin = 0;
283 		else if (beginStr.startsWith("/")) //$NON-NLS-1$
284 			begin = findLine(0, beginStr);
285 		else
286 			begin = Math.max(0, Integer.parseInt(beginStr) - 1);
287 
288 		if (endStr.isEmpty())
289 			end = blame.getResultContents().size();
290 		else if (endStr.startsWith("/")) //$NON-NLS-1$
291 			end = findLine(begin, endStr);
292 		else if (endStr.startsWith("-")) //$NON-NLS-1$
293 			end = begin + Integer.parseInt(endStr);
294 		else if (endStr.startsWith("+")) //$NON-NLS-1$
295 			end = begin + Integer.parseInt(endStr.substring(1));
296 		else
297 			end = Math.max(0, Integer.parseInt(endStr) - 1);
298 	}
299 
300 	private int findLine(int b, String regex) {
301 		String re = regex.substring(1, regex.length() - 1);
302 		if (!re.startsWith("^")) //$NON-NLS-1$
303 			re = ".*" + re; //$NON-NLS-1$
304 		if (!re.endsWith("$")) //$NON-NLS-1$
305 			re = re + ".*"; //$NON-NLS-1$
306 		Pattern p = Pattern.compile(re);
307 		RawText text = blame.getResultContents();
308 		for (int line = b; line < text.size(); line++) {
309 			if (p.matcher(text.getString(line)).matches())
310 				return line;
311 		}
312 		return b;
313 	}
314 
315 	private String path(int line) {
316 		String p = blame.getSourcePath(line);
317 		return p != null ? p : ""; //$NON-NLS-1$
318 	}
319 
320 	private String author(int line) {
321 		PersonIdent author = blame.getSourceAuthor(line);
322 		if (author == null)
323 			return ""; //$NON-NLS-1$
324 		String name = showAuthorEmail ? author.getEmailAddress() : author
325 				.getName();
326 		return name != null ? name : ""; //$NON-NLS-1$
327 	}
328 
329 	private String date(int line) {
330 		PersonIdent author;
331 		if (blame.getSourceCommit(line) == null) {
332 			author = dummyDate;
333 		} else {
334 			author = blame.getSourceAuthor(line);
335 		}
336 		if (author == null)
337 			return ""; //$NON-NLS-1$
338 
339 		dateFmt.setTimeZone(author.getTimeZone());
340 		if (!showRawTimestamp)
341 			return dateFmt.format(author.getWhen());
342 		return String.format("%d %s", //$NON-NLS-1$
343 				valueOf(author.getWhen().getTime() / 1000L),
344 				dateFmt.format(author.getWhen()));
345 	}
346 
347 	private String abbreviate(ObjectReader reader, RevCommit commit)
348 			throws IOException {
349 		String r = abbreviatedCommits.get(commit);
350 		if (r != null)
351 			return r;
352 
353 		if (commit == null) {
354 			if (showLongRevision) {
355 				r = ObjectId.zeroId().name();
356 			} else {
357 				r = ObjectId.zeroId().abbreviate(abbrev + 1).name();
358 			}
359 		} else {
360 			if (showBlankBoundary && commit.getParentCount() == 0)
361 				commit = null;
362 
363 			if (commit == null) {
364 				int len = showLongRevision ? OBJECT_ID_STRING_LENGTH
365 						: (abbrev + 1);
366 				StringBuilder b = new StringBuilder(len);
367 				for (int i = 0; i < len; i++)
368 					b.append(' ');
369 				r = b.toString();
370 
371 			} else if (!root && commit.getParentCount() == 0) {
372 				if (showLongRevision)
373 					r = "^" + commit.name().substring(0, //$NON-NLS-1$
374 							OBJECT_ID_STRING_LENGTH - 1);
375 				else
376 					r = "^" + reader.abbreviate(commit, abbrev).name(); //$NON-NLS-1$
377 			} else {
378 				if (showLongRevision)
379 					r = commit.name();
380 				else
381 					r = reader.abbreviate(commit, abbrev + 1).name();
382 			}
383 		}
384 		abbreviatedCommits.put(commit, r);
385 		return r;
386 	}
387 }