1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
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);
150 if (!showBlankBoundary)
151 root = db.getConfig().getBoolean("blame", "blankboundary", false);
152 if (!root)
153 root = db.getConfig().getBoolean("blame", "showroot", false);
154
155 if (showRawTimestamp)
156 dateFmt = new SimpleDateFormat("ZZZZ");
157 else
158 dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ");
159
160 reader = db.newObjectReader();
161 try (BlameGenerator generator = new BlameGenerator(db, file)) {
162 RevFlag scanned = generator.newFlag("SCANNED");
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}"));
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));
218 String numFmt = MessageFormat.format(" %{0}d",
219 valueOf(1 + (int) Math.log10(maxSourceLine + 1)));
220 String lineFmt = MessageFormat.format(" %{0}d) ",
221 valueOf(1 + (int) Math.log10(end + 1)));
222 String authorFmt = MessageFormat.format(" (%-{0}s %{1}s",
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("/")) {
261 int c = rangeString.indexOf("/,", 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";
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(""))
285 begin = 0;
286 else if (beginStr.startsWith("/"))
287 begin = findLine(0, beginStr);
288 else
289 begin = Math.max(0, Integer.parseInt(beginStr) - 1);
290
291 if (endStr.equals(""))
292 end = blame.getResultContents().size();
293 else if (endStr.startsWith("/"))
294 end = findLine(begin, endStr);
295 else if (endStr.startsWith("-"))
296 end = begin + Integer.parseInt(endStr);
297 else if (endStr.startsWith("+"))
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("^"))
306 re = ".*" + re;
307 if (!re.endsWith("$"))
308 re = re + ".*";
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 : "";
321 }
322
323 private String author(int line) {
324 PersonIdent author = blame.getSourceAuthor(line);
325 if (author == null)
326 return "";
327 String name = showAuthorEmail ? author.getEmailAddress() : author
328 .getName();
329 return name != null ? name : "";
330 }
331
332 private String date(int line) {
333 if (blame.getSourceCommit(line) == null)
334 return "";
335
336 PersonIdent author = blame.getSourceAuthor(line);
337 if (author == null)
338 return "";
339
340 dateFmt.setTimeZone(author.getTimeZone());
341 if (!showRawTimestamp)
342 return dateFmt.format(author.getWhen());
343 return String.format("%d %s",
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);
366 else
367 r = "^" + reader.abbreviate(commit, abbrev).name();
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 }