From 1925889139f3cd028b1c7d6133c176c27d9f6463 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Tue, 9 Jul 2024 20:14:47 +0100 Subject: [PATCH] cli: add a blame command --- src/cli/cmd.h | 1 + src/cli/cmd_blame.c | 288 ++++++++++++++++++++++++++++++++++++++++++++ src/cli/main.c | 1 + 3 files changed, 290 insertions(+) create mode 100644 src/cli/cmd_blame.c diff --git a/src/cli/cmd.h b/src/cli/cmd.h index bd881223d..5ac67f526 100644 --- a/src/cli/cmd.h +++ b/src/cli/cmd.h @@ -25,6 +25,7 @@ extern const cli_cmd_spec cli_cmds[]; extern const cli_cmd_spec *cli_cmd_spec_byname(const char *name); /* Commands */ +extern int cmd_blame(int argc, char **argv); extern int cmd_cat_file(int argc, char **argv); extern int cmd_clone(int argc, char **argv); extern int cmd_config(int argc, char **argv); diff --git a/src/cli/cmd_blame.c b/src/cli/cmd_blame.c new file mode 100644 index 000000000..3e2f5744c --- /dev/null +++ b/src/cli/cmd_blame.c @@ -0,0 +1,288 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include +#include +#include "common.h" +#include "cmd.h" +#include "error.h" +#include "sighandler.h" +#include "progress.h" + +#include "fs_path.h" +#include "futils.h" +#include "date.h" +#include "hashmap.h" + +#define COMMAND_NAME "blame" + +static char *file; +static int porcelain, line_porcelain; +static int show_help; + +static const cli_opt_spec opts[] = { + CLI_COMMON_OPT, + + { CLI_OPT_TYPE_SWITCH, "porcelain", 'p', &porcelain, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "show machine readable output" }, + { CLI_OPT_TYPE_SWITCH, "line-porcelain", 0, &line_porcelain, 1, + CLI_OPT_USAGE_DEFAULT, NULL, "show individual lines in machine readable output" }, + { CLI_OPT_TYPE_LITERAL }, + { CLI_OPT_TYPE_ARG, "file", 0, &file, 0, + CLI_OPT_USAGE_REQUIRED, "file", "file to blame" }, + + { 0 } +}; + +static void print_help(void) +{ + cli_opt_usage_fprint(stdout, PROGRAM_NAME, COMMAND_NAME, opts); + printf("\n"); + + printf("Show the origin of each line of a file.\n"); + printf("\n"); + + printf("Options:\n"); + + cli_opt_help_fprint(stdout, opts); +} + +static int strintlen(size_t n) +{ + int len = 1; + + while (n > 10) { + n /= 10; + len++; + + if (len == INT_MAX) + break; + } + + return len; +} + +static int fmt_date(git_str *out, git_time_t time, int offset) +{ + time_t t; + struct tm gmt; + + GIT_ASSERT_ARG(out); + + t = (time_t)(time + offset * 60); + + if (p_gmtime_r(&t, &gmt) == NULL) + return -1; + + return git_str_printf(out, "%.4u-%02u-%02u %02u:%02u:%02u %+03d%02d", + gmt.tm_year + 1900, gmt.tm_mon + 1, gmt.tm_mday, + gmt.tm_hour, gmt.tm_min, gmt.tm_sec, + offset / 60, offset % 60); +} + +static int print_standard(git_blame *blame) +{ + size_t max_line_number = 0; + int max_lineno_len, max_line_len, max_author_len = 0, max_path_len = 0; + const char *last_path = NULL; + const git_blame_line *line; + bool paths_differ = false; + git_str date_str = GIT_STR_INIT; + size_t i; + int ret = 0; + + /* Compute the maximum size of things */ + for (i = 0; i < git_blame_hunkcount(blame); i++) { + const git_blame_hunk *hunk = git_blame_hunk_byindex(blame, i); + size_t hunk_author_len = strlen(hunk->orig_signature->name); + size_t hunk_path_len = strlen(hunk->orig_path); + size_t hunk_max_line_number = + hunk->orig_start_line_number + hunk->lines_in_hunk; + + if (hunk_max_line_number > max_line_number) + max_line_number = hunk_max_line_number; + + if (hunk_author_len > INT_MAX) + max_author_len = INT_MAX; + else if ((int)hunk_author_len > max_author_len) + max_author_len = (int)hunk_author_len; + + if (hunk_path_len > INT_MAX) + hunk_path_len = INT_MAX; + else if ((int)hunk_path_len > max_path_len) + max_path_len = (int)hunk_path_len; + + if (!paths_differ && last_path != NULL && + strcmp(last_path, hunk->orig_path) != 0) { + paths_differ = true; + } + + last_path = hunk->orig_path; + } + + max_lineno_len = strintlen(max_line_number); + + max_author_len--; + + for (i = 1; i < git_blame_linecount(blame); i++) { + const git_blame_hunk *hunk = git_blame_hunk_byline(blame, i); + int oid_abbrev; + + if (!hunk) + break; + + oid_abbrev = hunk->boundary ? 7 : 8; + printf("%s%.*s ", hunk->boundary ? "^" : "", + oid_abbrev, git_oid_tostr_s(&hunk->orig_commit_id)); + + if (paths_differ) + printf("%-*.*s ", max_path_len, max_path_len, hunk->orig_path); + + git_str_clear(&date_str); + if (fmt_date(&date_str, + hunk->orig_signature->when.time, + hunk->orig_signature->when.offset) < 0) { + ret = cli_error_git(); + goto done; + } + + if ((line = git_blame_line_byindex(blame, i)) == NULL) { + ret = cli_error_git(); + goto done; + } + + max_line_len = (int)min(line->len, INT_MAX); + + printf("(%-*.*s %s %*" PRIuZ ") %.*s" , + max_author_len, max_author_len, hunk->orig_signature->name, + date_str.ptr, + max_lineno_len, i, + max_line_len, line->ptr); + printf("\n"); + } + +done: + git_str_dispose(&date_str); + return ret; +} + +GIT_INLINE(uint32_t) oid_hashcode(const git_oid *oid) +{ + uint32_t hash; + memcpy(&hash, oid->id, sizeof(uint32_t)); + return hash; +} + +GIT_HASHSET_SETUP(git_blame_commitmap, const git_oid *, oid_hashcode, git_oid_equal); + +static int print_porcelain(git_blame *blame) +{ + git_blame_commitmap seen_ids = GIT_HASHSET_INIT; + size_t i, j; + + for (i = 0; i < git_blame_hunkcount(blame); i++) { + const git_blame_line *line; + const git_blame_hunk *hunk = git_blame_hunk_byindex(blame, i); + + for (j = 0; j < hunk->lines_in_hunk; j++) { + size_t line_number = hunk->final_start_line_number + j; + bool seen = git_blame_commitmap_contains(&seen_ids, &hunk->orig_commit_id); + + printf("%s %" PRIuZ " %" PRIuZ, + git_oid_tostr_s(&hunk->orig_commit_id), + hunk->orig_start_line_number + j, + hunk->final_start_line_number + j); + + if (!j) + printf(" %" PRIuZ, hunk->lines_in_hunk); + + printf("\n"); + + if ((!j && !seen) || line_porcelain) { + printf("author %s\n", hunk->orig_signature->name); + printf("author-mail <%s>\n", hunk->orig_signature->email); + printf("author-time %" PRId64 "\n", hunk->orig_signature->when.time); + printf("author-tz %+03d%02d\n", + hunk->orig_signature->when.offset / 60, + hunk->orig_signature->when.offset % 60); + + printf("committer %s\n", hunk->orig_committer->name); + printf("committer-mail <%s>\n", hunk->orig_committer->email); + printf("committer-time %" PRId64 "\n", hunk->orig_committer->when.time); + printf("committer-tz %+03d%02d\n", + hunk->orig_committer->when.offset / 60, + hunk->orig_committer->when.offset % 60); + + printf("summary %s\n", hunk->summary); + + /* TODO: previous */ + + printf("filename %s\n", hunk->orig_path); + } + + if ((line = git_blame_line_byindex(blame, line_number)) == NULL) + return cli_error_git(); + + printf("\t%.*s\n", (int)min(line->len, INT_MAX), + line->ptr); + + if (!seen) + git_blame_commitmap_add(&seen_ids, &hunk->orig_commit_id); + } + } + + git_blame_commitmap_dispose(&seen_ids); + return 0; +} + +int cmd_blame(int argc, char **argv) +{ + cli_repository_open_options open_opts = { argv + 1, argc - 1 }; + git_blame_options blame_opts = GIT_BLAME_OPTIONS_INIT; + git_repository *repo = NULL; + git_str workdir_path = GIT_STR_INIT; + git_blame *blame = NULL; + cli_opt invalid_opt; + int ret = 0; + + blame_opts.flags |= GIT_BLAME_USE_MAILMAP; + + if (cli_opt_parse(&invalid_opt, opts, argv + 1, argc - 1, CLI_OPT_PARSE_GNU)) + return cli_opt_usage_error(COMMAND_NAME, opts, &invalid_opt); + + if (show_help) { + print_help(); + return 0; + } + + if (!file) { + ret = cli_error_usage("you must specify a file to blame"); + goto done; + } + + if (cli_repository_open(&repo, &open_opts) < 0) + return cli_error_git(); + + if ((ret = cli_resolve_path(&workdir_path, repo, file)) != 0) + goto done; + + if (git_blame_file(&blame, repo, workdir_path.ptr, &blame_opts) < 0) { + ret = cli_error_git(); + goto done; + } + + if (porcelain || line_porcelain) + ret = print_porcelain(blame); + else + ret = print_standard(blame); + +done: + git_str_dispose(&workdir_path); + git_blame_free(blame); + git_repository_free(repo); + return ret; +} diff --git a/src/cli/main.c b/src/cli/main.c index c7a6fcfce..715feab89 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -32,6 +32,7 @@ const cli_opt_spec cli_common_opts[] = { }; const cli_cmd_spec cli_cmds[] = { + { "blame", cmd_blame, "Show the origin of each line of a file" }, { "cat-file", cmd_cat_file, "Display an object in the repository" }, { "clone", cmd_clone, "Clone a repository into a new directory" }, { "config", cmd_config, "View or set configuration values " },