Skip to content

Commit 1925889

Browse files
committed
cli: add a blame command
1 parent 1ee2c33 commit 1925889

File tree

3 files changed

+290
-0
lines changed

3 files changed

+290
-0
lines changed

src/cli/cmd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extern const cli_cmd_spec cli_cmds[];
2525
extern const cli_cmd_spec *cli_cmd_spec_byname(const char *name);
2626

2727
/* Commands */
28+
extern int cmd_blame(int argc, char **argv);
2829
extern int cmd_cat_file(int argc, char **argv);
2930
extern int cmd_clone(int argc, char **argv);
3031
extern int cmd_config(int argc, char **argv);

src/cli/cmd_blame.c

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
* Copyright (C) the libgit2 contributors. All rights reserved.
3+
*
4+
* This file is part of libgit2, distributed under the GNU GPL v2 with
5+
* a Linking Exception. For full terms see the included COPYING file.
6+
*/
7+
8+
#include <stdio.h>
9+
#include <git2.h>
10+
#include "common.h"
11+
#include "cmd.h"
12+
#include "error.h"
13+
#include "sighandler.h"
14+
#include "progress.h"
15+
16+
#include "fs_path.h"
17+
#include "futils.h"
18+
#include "date.h"
19+
#include "hashmap.h"
20+
21+
#define COMMAND_NAME "blame"
22+
23+
static char *file;
24+
static int porcelain, line_porcelain;
25+
static int show_help;
26+
27+
static const cli_opt_spec opts[] = {
28+
CLI_COMMON_OPT,
29+
30+
{ CLI_OPT_TYPE_SWITCH, "porcelain", 'p', &porcelain, 1,
31+
CLI_OPT_USAGE_DEFAULT, NULL, "show machine readable output" },
32+
{ CLI_OPT_TYPE_SWITCH, "line-porcelain", 0, &line_porcelain, 1,
33+
CLI_OPT_USAGE_DEFAULT, NULL, "show individual lines in machine readable output" },
34+
{ CLI_OPT_TYPE_LITERAL },
35+
{ CLI_OPT_TYPE_ARG, "file", 0, &file, 0,
36+
CLI_OPT_USAGE_REQUIRED, "file", "file to blame" },
37+
38+
{ 0 }
39+
};
40+
41+
static void print_help(void)
42+
{
43+
cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts);
44+
printf("\n");
45+
46+
printf("Show the origin of each line of a file.\n");
47+
printf("\n");
48+
49+
printf("Options:\n");
50+
51+
cli_opt_help_fprint(stdout, opts);
52+
}
53+
54+
static int strintlen(size_t n)
55+
{
56+
int len = 1;
57+
58+
while (n > 10) {
59+
n /= 10;
60+
len++;
61+
62+
if (len == INT_MAX)
63+
break;
64+
}
65+
66+
return len;
67+
}
68+
69+
static int fmt_date(git_str *out, git_time_t time, int offset)
70+
{
71+
time_t t;
72+
struct tm gmt;
73+
74+
GIT_ASSERT_ARG(out);
75+
76+
t = (time_t)(time + offset * 60);
77+
78+
if (p_gmtime_r(&t, &gmt) == NULL)
79+
return -1;
80+
81+
return git_str_printf(out, "%.4u-%02u-%02u %02u:%02u:%02u %+03d%02d",
82+
gmt.tm_year + 1900, gmt.tm_mon + 1, gmt.tm_mday,
83+
gmt.tm_hour, gmt.tm_min, gmt.tm_sec,
84+
offset / 60, offset % 60);
85+
}
86+
87+
static int print_standard(git_blame *blame)
88+
{
89+
size_t max_line_number = 0;
90+
int max_lineno_len, max_line_len, max_author_len = 0, max_path_len = 0;
91+
const char *last_path = NULL;
92+
const git_blame_line *line;
93+
bool paths_differ = false;
94+
git_str date_str = GIT_STR_INIT;
95+
size_t i;
96+
int ret = 0;
97+
98+
/* Compute the maximum size of things */
99+
for (i = 0; i < git_blame_hunkcount(blame); i++) {
100+
const git_blame_hunk *hunk = git_blame_hunk_byindex(blame, i);
101+
size_t hunk_author_len = strlen(hunk->orig_signature->name);
102+
size_t hunk_path_len = strlen(hunk->orig_path);
103+
size_t hunk_max_line_number =
104+
hunk->orig_start_line_number + hunk->lines_in_hunk;
105+
106+
if (hunk_max_line_number > max_line_number)
107+
max_line_number = hunk_max_line_number;
108+
109+
if (hunk_author_len > INT_MAX)
110+
max_author_len = INT_MAX;
111+
else if ((int)hunk_author_len > max_author_len)
112+
max_author_len = (int)hunk_author_len;
113+
114+
if (hunk_path_len > INT_MAX)
115+
hunk_path_len = INT_MAX;
116+
else if ((int)hunk_path_len > max_path_len)
117+
max_path_len = (int)hunk_path_len;
118+
119+
if (!paths_differ && last_path != NULL &&
120+
strcmp(last_path, hunk->orig_path) != 0) {
121+
paths_differ = true;
122+
}
123+
124+
last_path = hunk->orig_path;
125+
}
126+
127+
max_lineno_len = strintlen(max_line_number);
128+
129+
max_author_len--;
130+
131+
for (i = 1; i < git_blame_linecount(blame); i++) {
132+
const git_blame_hunk *hunk = git_blame_hunk_byline(blame, i);
133+
int oid_abbrev;
134+
135+
if (!hunk)
136+
break;
137+
138+
oid_abbrev = hunk->boundary ? 7 : 8;
139+
printf("%s%.*s ", hunk->boundary ? "^" : "",
140+
oid_abbrev, git_oid_tostr_s(&hunk->orig_commit_id));
141+
142+
if (paths_differ)
143+
printf("%-*.*s ", max_path_len, max_path_len, hunk->orig_path);
144+
145+
git_str_clear(&date_str);
146+
if (fmt_date(&date_str,
147+
hunk->orig_signature->when.time,
148+
hunk->orig_signature->when.offset) < 0) {
149+
ret = cli_error_git();
150+
goto done;
151+
}
152+
153+
if ((line = git_blame_line_byindex(blame, i)) == NULL) {
154+
ret = cli_error_git();
155+
goto done;
156+
}
157+
158+
max_line_len = (int)min(line->len, INT_MAX);
159+
160+
printf("(%-*.*s %s %*" PRIuZ ") %.*s" ,
161+
max_author_len, max_author_len, hunk->orig_signature->name,
162+
date_str.ptr,
163+
max_lineno_len, i,
164+
max_line_len, line->ptr);
165+
printf("\n");
166+
}
167+
168+
done:
169+
git_str_dispose(&date_str);
170+
return ret;
171+
}
172+
173+
GIT_INLINE(uint32_t) oid_hashcode(const git_oid *oid)
174+
{
175+
uint32_t hash;
176+
memcpy(&hash, oid->id, sizeof(uint32_t));
177+
return hash;
178+
}
179+
180+
GIT_HASHSET_SETUP(git_blame_commitmap, const git_oid *, oid_hashcode, git_oid_equal);
181+
182+
static int print_porcelain(git_blame *blame)
183+
{
184+
git_blame_commitmap seen_ids = GIT_HASHSET_INIT;
185+
size_t i, j;
186+
187+
for (i = 0; i < git_blame_hunkcount(blame); i++) {
188+
const git_blame_line *line;
189+
const git_blame_hunk *hunk = git_blame_hunk_byindex(blame, i);
190+
191+
for (j = 0; j < hunk->lines_in_hunk; j++) {
192+
size_t line_number = hunk->final_start_line_number + j;
193+
bool seen = git_blame_commitmap_contains(&seen_ids, &hunk->orig_commit_id);
194+
195+
printf("%s %" PRIuZ " %" PRIuZ,
196+
git_oid_tostr_s(&hunk->orig_commit_id),
197+
hunk->orig_start_line_number + j,
198+
hunk->final_start_line_number + j);
199+
200+
if (!j)
201+
printf(" %" PRIuZ, hunk->lines_in_hunk);
202+
203+
printf("\n");
204+
205+
if ((!j && !seen) || line_porcelain) {
206+
printf("author %s\n", hunk->orig_signature->name);
207+
printf("author-mail <%s>\n", hunk->orig_signature->email);
208+
printf("author-time %" PRId64 "\n", hunk->orig_signature->when.time);
209+
printf("author-tz %+03d%02d\n",
210+
hunk->orig_signature->when.offset / 60,
211+
hunk->orig_signature->when.offset % 60);
212+
213+
printf("committer %s\n", hunk->orig_committer->name);
214+
printf("committer-mail <%s>\n", hunk->orig_committer->email);
215+
printf("committer-time %" PRId64 "\n", hunk->orig_committer->when.time);
216+
printf("committer-tz %+03d%02d\n",
217+
hunk->orig_committer->when.offset / 60,
218+
hunk->orig_committer->when.offset % 60);
219+
220+
printf("summary %s\n", hunk->summary);
221+
222+
/* TODO: previous */
223+
224+
printf("filename %s\n", hunk->orig_path);
225+
}
226+
227+
if ((line = git_blame_line_byindex(blame, line_number)) == NULL)
228+
return cli_error_git();
229+
230+
printf("\t%.*s\n", (int)min(line->len, INT_MAX),
231+
line->ptr);
232+
233+
if (!seen)
234+
git_blame_commitmap_add(&seen_ids, &hunk->orig_commit_id);
235+
}
236+
}
237+
238+
git_blame_commitmap_dispose(&seen_ids);
239+
return 0;
240+
}
241+
242+
int cmd_blame(int argc, char **argv)
243+
{
244+
cli_repository_open_options open_opts = { argv + 1, argc - 1 };
245+
git_blame_options blame_opts = GIT_BLAME_OPTIONS_INIT;
246+
git_repository *repo = NULL;
247+
git_str workdir_path = GIT_STR_INIT;
248+
git_blame *blame = NULL;
249+
cli_opt invalid_opt;
250+
int ret = 0;
251+
252+
blame_opts.flags |= GIT_BLAME_USE_MAILMAP;
253+
254+
if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU))
255+
return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt);
256+
257+
if (show_help) {
258+
print_help();
259+
return 0;
260+
}
261+
262+
if (!file) {
263+
ret = cli_error_usage("you must specify a file to blame");
264+
goto done;
265+
}
266+
267+
if (cli_repository_open(&repo, &open_opts) < 0)
268+
return cli_error_git();
269+
270+
if ((ret = cli_resolve_path(&workdir_path, repo, file)) != 0)
271+
goto done;
272+
273+
if (git_blame_file(&blame, repo, workdir_path.ptr, &blame_opts) < 0) {
274+
ret = cli_error_git();
275+
goto done;
276+
}
277+
278+
if (porcelain || line_porcelain)
279+
ret = print_porcelain(blame);
280+
else
281+
ret = print_standard(blame);
282+
283+
done:
284+
git_str_dispose(&workdir_path);
285+
git_blame_free(blame);
286+
git_repository_free(repo);
287+
return ret;
288+
}

src/cli/main.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const cli_opt_spec cli_common_opts[] = {
3232
};
3333

3434
const cli_cmd_spec cli_cmds[] = {
35+
{ "blame", cmd_blame, "Show the origin of each line of a file" },
3536
{ "cat-file", cmd_cat_file, "Display an object in the repository" },
3637
{ "clone", cmd_clone, "Clone a repository into a new directory" },
3738
{ "config", cmd_config, "View or set configuration values " },

0 commit comments

Comments
 (0)