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>
6    * and other copyright owners as documented in the project's IP log.
7    *
8    * This program and the accompanying materials are made available
9    * under the terms of the Eclipse Distribution License v1.0 which
10   * accompanies this distribution, is reproduced below, and is
11   * available at http://www.eclipse.org/org/documents/edl-v10.php
12   *
13   * All rights reserved.
14   *
15   * Redistribution and use in source and binary forms, with or
16   * without modification, are permitted provided that the following
17   * conditions are met:
18   *
19   * - Redistributions of source code must retain the above copyright
20   *   notice, this list of conditions and the following disclaimer.
21   *
22   * - Redistributions in binary form must reproduce the above
23   *   copyright notice, this list of conditions and the following
24   *   disclaimer in the documentation and/or other materials provided
25   *   with the distribution.
26   *
27   * - Neither the name of the Eclipse Foundation, Inc. nor the
28   *   names of its contributors may be used to endorse or promote
29   *   products derived from this software without specific prior
30   *   written permission.
31   *
32   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
33   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
34   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
36   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
37   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
39   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
40   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
41   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
42   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
43   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45   */
46  
47  package org.eclipse.jgit.pgm;
48  
49  import static java.lang.Integer.valueOf;
50  import static java.lang.Long.valueOf;
51  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
52  
53  import java.io.File;
54  import java.io.IOException;
55  import java.text.MessageFormat;
56  import java.text.SimpleDateFormat;
57  import java.util.ArrayList;
58  import java.util.HashMap;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.regex.Pattern;
62  
63  import org.eclipse.jgit.blame.BlameGenerator;
64  import org.eclipse.jgit.blame.BlameResult;
65  import org.eclipse.jgit.diff.RawText;
66  import org.eclipse.jgit.diff.RawTextComparator;
67  import org.eclipse.jgit.dircache.DirCache;
68  import org.eclipse.jgit.lib.Constants;
69  import org.eclipse.jgit.lib.ObjectReader;
70  import org.eclipse.jgit.lib.PersonIdent;
71  import org.eclipse.jgit.pgm.internal.CLIText;
72  import org.eclipse.jgit.revwalk.RevCommit;
73  import org.eclipse.jgit.revwalk.RevFlag;
74  import org.kohsuke.args4j.Argument;
75  import org.kohsuke.args4j.Option;
76  
77  @Command(common = false, usage = "usage_Blame")
78  class Blame extends TextBuiltin {
79  	private RawTextComparator comparator = RawTextComparator.DEFAULT;
80  
81  	@Option(name = "-w", usage = "usage_ignoreWhitespace")
82  	void ignoreAllSpace(@SuppressWarnings("unused") boolean on) {
83  		comparator = RawTextComparator.WS_IGNORE_ALL;
84  	}
85  
86  	@Option(name = "--abbrev", metaVar = "metaVar_n", usage = "usage_abbrevCommits")
87  	private int abbrev;
88  
89  	@Option(name = "-l", usage = "usage_blameLongRevision")
90  	private boolean showLongRevision;
91  
92  	@Option(name = "-t", usage = "usage_blameRawTimestamp")
93  	private boolean showRawTimestamp;
94  
95  	@Option(name = "-b", usage = "usage_blameShowBlankBoundary")
96  	private boolean showBlankBoundary;
97  
98  	@Option(name = "-s", usage = "usage_blameSuppressAuthor")
99  	private boolean noAuthor;
100 
101 	@Option(name = "--show-email", aliases = { "-e" }, usage = "usage_blameShowEmail")
102 	private boolean showAuthorEmail;
103 
104 	@Option(name = "--show-name", aliases = { "-f" }, usage = "usage_blameShowSourcePath")
105 	private boolean showSourcePath;
106 
107 	@Option(name = "--show-number", aliases = { "-n" }, usage = "usage_blameShowSourceLine")
108 	private boolean showSourceLine;
109 
110 	@Option(name = "--root", usage = "usage_blameShowRoot")
111 	private boolean root;
112 
113 	@Option(name = "-L", metaVar = "metaVar_blameL", usage = "usage_blameRange")
114 	private String rangeString;
115 
116 	@Option(name = "--reverse", metaVar = "metaVar_blameReverse", usage = "usage_blameReverse")
117 	private List<RevCommit> reverseRange = new ArrayList<>(2);
118 
119 	@Argument(index = 0, required = false, metaVar = "metaVar_revision")
120 	private String revision;
121 
122 	@Argument(index = 1, required = false, metaVar = "metaVar_file")
123 	private String file;
124 
125 	private ObjectReader reader;
126 
127 	private final Map<RevCommit, String> abbreviatedCommits = new HashMap<>();
128 
129 	private SimpleDateFormat dateFmt;
130 
131 	private int begin;
132 
133 	private int end;
134 
135 	private BlameResult blame;
136 
137 	/** {@inheritDoc} */
138 	@Override
139 	protected void run() throws Exception {
140 		if (file == null) {
141 			if (revision == null)
142 				throw die(CLIText.get().fileIsRequired);
143 			file = revision;
144 			revision = null;
145 		}
146 
147 		boolean autoAbbrev = abbrev == 0;
148 		if (abbrev == 0)
149 			abbrev = db.getConfig().getInt("core", "abbrev", 7); //$NON-NLS-1$ //$NON-NLS-2$
150 		if (!showBlankBoundary)
151 			root = db.getConfig().getBoolean("blame", "blankboundary", false); //$NON-NLS-1$ //$NON-NLS-2$
152 		if (!root)
153 			root = db.getConfig().getBoolean("blame", "showroot", false); //$NON-NLS-1$ //$NON-NLS-2$
154 
155 		if (showRawTimestamp)
156 			dateFmt = new SimpleDateFormat("ZZZZ"); //$NON-NLS-1$
157 		else
158 			dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ"); //$NON-NLS-1$
159 
160 		reader = db.newObjectReader();
161 		try (BlameGenerator generator = new BlameGenerator(db, file)) {
162 			RevFlag scanned = generator.newFlag("SCANNED"); //$NON-NLS-1$
163 			generator.setTextComparator(comparator);
164 
165 			if (!reverseRange.isEmpty()) {
166 				RevCommit rangeStart = null;
167 				List<RevCommit> rangeEnd = new ArrayList<>(2);
168 				for (RevCommit c : reverseRange) {
169 					if (c.has(RevFlag.UNINTERESTING))
170 						rangeStart = c;
171 					else
172 						rangeEnd.add(c);
173 				}
174 				generator.reverse(rangeStart, rangeEnd);
175 			} else if (revision != null) {
176 				generator.push(null, db.resolve(revision + "^{commit}")); //$NON-NLS-1$
177 			} else {
178 				generator.push(null, db.resolve(Constants.HEAD));
179 				if (!db.isBare()) {
180 					DirCache dc = db.readDirCache();
181 					int entry = dc.findEntry(file);
182 					if (0 <= entry)
183 						generator.push(null, dc.getEntry(entry).getObjectId());
184 
185 					File inTree = new File(db.getWorkTree(), file);
186 					if (db.getFS().isFile(inTree))
187 						generator.push(null, new RawText(inTree));
188 				}
189 			}
190 
191 			blame = BlameResult.create(generator);
192 			begin = 0;
193 			end = blame.getResultContents().size();
194 			if (rangeString != null)
195 				parseLineRangeOption();
196 			blame.computeRange(begin, end);
197 
198 			int authorWidth = 8;
199 			int dateWidth = 8;
200 			int pathWidth = 1;
201 			int maxSourceLine = 1;
202 			for (int line = begin; line < end; line++) {
203 				RevCommit c = blame.getSourceCommit(line);
204 				if (c != null && !c.has(scanned)) {
205 					c.add(scanned);
206 					if (autoAbbrev)
207 						abbrev = Math.max(abbrev, uniqueAbbrevLen(c));
208 					authorWidth = Math.max(authorWidth, author(line).length());
209 					dateWidth = Math.max(dateWidth, date(line).length());
210 					pathWidth = Math.max(pathWidth, path(line).length());
211 				}
212 				while (line + 1 < end && blame.getSourceCommit(line + 1) == c)
213 					line++;
214 				maxSourceLine = Math.max(maxSourceLine, blame.getSourceLine(line));
215 			}
216 
217 			String pathFmt = MessageFormat.format(" %{0}s", valueOf(pathWidth)); //$NON-NLS-1$
218 			String numFmt = MessageFormat.format(" %{0}d", //$NON-NLS-1$
219 					valueOf(1 + (int) Math.log10(maxSourceLine + 1)));
220 			String lineFmt = MessageFormat.format(" %{0}d) ", //$NON-NLS-1$
221 					valueOf(1 + (int) Math.log10(end + 1)));
222 			String authorFmt = MessageFormat.format(" (%-{0}s %{1}s", //$NON-NLS-1$
223 					valueOf(authorWidth), valueOf(dateWidth));
224 
225 			for (int line = begin; line < end;) {
226 				RevCommit c = blame.getSourceCommit(line);
227 				String commit = abbreviate(c);
228 				String author = null;
229 				String date = null;
230 				if (!noAuthor) {
231 					author = author(line);
232 					date = date(line);
233 				}
234 				do {
235 					outw.print(commit);
236 					if (showSourcePath)
237 						outw.format(pathFmt, path(line));
238 					if (showSourceLine)
239 						outw.format(numFmt, valueOf(blame.getSourceLine(line) + 1));
240 					if (!noAuthor)
241 						outw.format(authorFmt, author, date);
242 					outw.format(lineFmt, valueOf(line + 1));
243 					outw.flush();
244 					blame.getResultContents().writeLine(outs, line);
245 					outs.flush();
246 					outw.print('\n');
247 				} while (++line < end && blame.getSourceCommit(line) == c);
248 			}
249 		} finally {
250 			reader.close();
251 		}
252 	}
253 
254 	private int uniqueAbbrevLen(RevCommit commit) throws IOException {
255 		return reader.abbreviate(commit, abbrev).length();
256 	}
257 
258 	private void parseLineRangeOption() {
259 		String beginStr, endStr;
260 		if (rangeString.startsWith("/")) { //$NON-NLS-1$
261 			int c = rangeString.indexOf("/,", 1); //$NON-NLS-1$
262 			if (c < 0) {
263 				beginStr = rangeString;
264 				endStr = String.valueOf(end);
265 			} else {
266 				beginStr = rangeString.substring(0, c);
267 				endStr = rangeString.substring(c + 2);
268 			}
269 
270 		} else {
271 			int c = rangeString.indexOf(',');
272 			if (c < 0) {
273 				beginStr = rangeString;
274 				endStr = String.valueOf(end);
275 			} else if (c == 0) {
276 				beginStr = "0"; //$NON-NLS-1$
277 				endStr = rangeString.substring(1);
278 			} else {
279 				beginStr = rangeString.substring(0, c);
280 				endStr = rangeString.substring(c + 1);
281 			}
282 		}
283 
284 		if (beginStr.equals("")) //$NON-NLS-1$
285 			begin = 0;
286 		else if (beginStr.startsWith("/")) //$NON-NLS-1$
287 			begin = findLine(0, beginStr);
288 		else
289 			begin = Math.max(0, Integer.parseInt(beginStr) - 1);
290 
291 		if (endStr.equals("")) //$NON-NLS-1$
292 			end = blame.getResultContents().size();
293 		else if (endStr.startsWith("/")) //$NON-NLS-1$
294 			end = findLine(begin, endStr);
295 		else if (endStr.startsWith("-")) //$NON-NLS-1$
296 			end = begin + Integer.parseInt(endStr);
297 		else if (endStr.startsWith("+")) //$NON-NLS-1$
298 			end = begin + Integer.parseInt(endStr.substring(1));
299 		else
300 			end = Math.max(0, Integer.parseInt(endStr) - 1);
301 	}
302 
303 	private int findLine(int b, String regex) {
304 		String re = regex.substring(1, regex.length() - 1);
305 		if (!re.startsWith("^")) //$NON-NLS-1$
306 			re = ".*" + re; //$NON-NLS-1$
307 		if (!re.endsWith("$")) //$NON-NLS-1$
308 			re = re + ".*"; //$NON-NLS-1$
309 		Pattern p = Pattern.compile(re);
310 		RawText text = blame.getResultContents();
311 		for (int line = b; line < text.size(); line++) {
312 			if (p.matcher(text.getString(line)).matches())
313 				return line;
314 		}
315 		return b;
316 	}
317 
318 	private String path(int line) {
319 		String p = blame.getSourcePath(line);
320 		return p != null ? p : ""; //$NON-NLS-1$
321 	}
322 
323 	private String author(int line) {
324 		PersonIdent author = blame.getSourceAuthor(line);
325 		if (author == null)
326 			return ""; //$NON-NLS-1$
327 		String name = showAuthorEmail ? author.getEmailAddress() : author
328 				.getName();
329 		return name != null ? name : ""; //$NON-NLS-1$
330 	}
331 
332 	private String date(int line) {
333 		if (blame.getSourceCommit(line) == null)
334 			return ""; //$NON-NLS-1$
335 
336 		PersonIdent author = blame.getSourceAuthor(line);
337 		if (author == null)
338 			return ""; //$NON-NLS-1$
339 
340 		dateFmt.setTimeZone(author.getTimeZone());
341 		if (!showRawTimestamp)
342 			return dateFmt.format(author.getWhen());
343 		return String.format("%d %s", //$NON-NLS-1$
344 				valueOf(author.getWhen().getTime() / 1000L),
345 				dateFmt.format(author.getWhen()));
346 	}
347 
348 	private String abbreviate(RevCommit commit) throws IOException {
349 		String r = abbreviatedCommits.get(commit);
350 		if (r != null)
351 			return r;
352 
353 		if (showBlankBoundary && commit.getParentCount() == 0)
354 			commit = null;
355 
356 		if (commit == null) {
357 			int len = showLongRevision ? OBJECT_ID_STRING_LENGTH : (abbrev + 1);
358 			StringBuilder b = new StringBuilder(len);
359 			for (int i = 0; i < len; i++)
360 				b.append(' ');
361 			r = b.toString();
362 
363 		} else if (!root && commit.getParentCount() == 0) {
364 			if (showLongRevision)
365 				r = "^" + commit.name().substring(0, OBJECT_ID_STRING_LENGTH - 1); //$NON-NLS-1$
366 			else
367 				r = "^" + reader.abbreviate(commit, abbrev).name(); //$NON-NLS-1$
368 		} else {
369 			if (showLongRevision)
370 				r = commit.name();
371 			else
372 				r = reader.abbreviate(commit, abbrev + 1).name();
373 		}
374 
375 		abbreviatedCommits.put(commit, r);
376 		return r;
377 	}
378 }