1
2
3
4
5
6
7
8
9
10
11
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
105 private final PersonIdent dummyDate = new PersonIdent("", "");
106
107
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",
121 OBJECT_ID_ABBREV_STRING_LENGTH);
122 }
123 if (!showBlankBoundary) {
124 root = db.getConfig().getBoolean("blame", "blankboundary", false);
125 }
126 if (!root) {
127 root = db.getConfig().getBoolean("blame", "showroot", false);
128 }
129
130 if (showRawTimestamp) {
131 dateFmt = new SimpleDateFormat("ZZZZ");
132 } else {
133 dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ");
134 }
135
136 try (ObjectReader reader = db.newObjectReader();
137 BlameGenerator generator = new BlameGenerator(db, file)) {
138 RevFlag scanned = generator.newFlag("SCANNED");
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}");
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));
202 String numFmt = MessageFormat.format(" %{0}d",
203 valueOf(1 + (int) Math.log10(maxSourceLine + 1)));
204 String lineFmt = MessageFormat.format(" %{0}d) ",
205 valueOf(1 + (int) Math.log10(end + 1)));
206 String authorFmt = MessageFormat.format(" (%-{0}s %{1}s",
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
245
246
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("/")) {
258 int c = rangeString.indexOf("/,", 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";
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("/"))
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("/"))
291 end = findLine(begin, endStr);
292 else if (endStr.startsWith("-"))
293 end = begin + Integer.parseInt(endStr);
294 else if (endStr.startsWith("+"))
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("^"))
303 re = ".*" + re;
304 if (!re.endsWith("$"))
305 re = re + ".*";
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 : "";
318 }
319
320 private String author(int line) {
321 PersonIdent author = blame.getSourceAuthor(line);
322 if (author == null)
323 return "";
324 String name = showAuthorEmail ? author.getEmailAddress() : author
325 .getName();
326 return name != null ? name : "";
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 "";
338
339 dateFmt.setTimeZone(author.getTimeZone());
340 if (!showRawTimestamp)
341 return dateFmt.format(author.getWhen());
342 return String.format("%d %s",
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,
374 OBJECT_ID_STRING_LENGTH - 1);
375 else
376 r = "^" + reader.abbreviate(commit, abbrev).name();
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 }