From ba2676e98f0192d3b067be87e967202e21882849 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Mon, 27 Apr 2020 23:29:21 +0200 Subject: [PATCH] Blame uncommitted deletions starting from HEAD When starting a blame in the stage view on a deleted line, it seems appropriate to find the commit that added the line. To this end, instead of starting the blame view from the working-tree-state of a file, use the version at HEAD, which still has the deleted line (unless the line was only just staged). Compute the position of the deleted line to start the blame view there. Closes #1008 --- NEWS.adoc | 4 ++ include/tig/status.h | 1 + src/stage.c | 106 +++++++++++++++++++++++++++++++++++++++++-- src/status.c | 2 +- 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 7dbea85eb..584e619cf 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -8,6 +8,10 @@ Bug fixes: - Fix wrapping of lines with multibyte characters. (#988) +Improvements: + + - Start blame of uncommitted deleted lines from HEAD so the line's origin can be traced. (#1008) + tig-2.5.1 --------- diff --git a/include/tig/status.h b/include/tig/status.h index 466e8ee1d..1da515690 100644 --- a/include/tig/status.h +++ b/include/tig/status.h @@ -33,6 +33,7 @@ struct status { bool status_update_file(struct status *status, enum line_type type); bool status_update_files(struct view *view, struct line *line); +bool status_get_diff(struct status *file, const char *buf, size_t bufsize); bool status_revert(struct status *status, enum line_type type, bool has_none); bool status_exists(struct view *view, struct status *status, enum line_type type); diff --git a/src/stage.c b/src/stage.c index 6623cc36b..ca845e1ae 100644 --- a/src/stage.c +++ b/src/stage.c @@ -366,6 +366,97 @@ stage_chunk_is_wrapped(struct view *view, struct line *line) return false; } +const char * +find_deleted_line_in_head(struct view *view, struct line *line, unsigned long *lineno) { + if (line->type != LINE_DIFF_DEL) + return NULL; + + // The file might might be renamed in the index. + const char *file_in_head = view->env->file; + struct io io; + struct buffer buffer; + const char *diff_index_renamed_arg[] = { + "git", "diff-index", "--root", "--cached", "-C", + "--diff-filter=ACR", "-z", "HEAD", NULL + }; + if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff_index_renamed_arg) || io.status) + return NULL; + struct status file_status; + while (io_get(&io, &buffer, 0, true)) { + if (!status_get_diff(&file_status, buffer.data, buffer.size)) + return NULL; + if (file_status.status != 'A') { + if (!io_get(&io, &buffer, 0, true)) + return NULL; + string_ncopy(file_status.old.name, buffer.data, buffer.size); + } + if (!io_get(&io, &buffer, 0, true)) + return NULL; + string_ncopy(file_status.new.name, buffer.data, buffer.size); + if (strcmp(file_status.new.name, view->env->file)) + continue; + // Quit if the file does not exist in HEAD. + if (file_status.status == 'A') { + return NULL; + } + file_in_head = file_status.old.name; + break; + }; + + // Compute the line number in HEAD. The current view is a diff + // of what changed between HEAD, so get the old line number. + *lineno = diff_get_lineno(view, line, true); + assert(*lineno); + + if (stage_line_type == LINE_STAT_STAGED) + return file_in_head; + + // If we are in an unstaged diff, we also need to take into + // account the staged changes to this file, since they happened + // between HEAD and our diff. + char old_file_in_head[sizeof("HEAD:") + SIZEOF_STR]; + sprintf(old_file_in_head, "HEAD:%s", file_in_head); + char new_file_in_index[sizeof(":") + SIZEOF_STR]; + sprintf(new_file_in_index, ":%s", view->env->file); + const char *diff[] = { + "git", "diff", "--root", old_file_in_head, new_file_in_index, NULL + }; + if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff) || io.status) + return NULL; + long bias_by_staged_changes = 0; + unsigned long line_number = 0; + // *lineno is still the line number in the staged version of the + // file. Go through the staged changes up to our line number and + // count the additions and deletions on the way, to compute the + // line number before the staged changes. + while (line_number < *lineno && io_get(&io, &buffer, '\n', true)) { + enum line_type type = get_line_type(buffer.data); + if (type == LINE_DIFF_CHUNK) { + struct chunk_header header; + if (!parse_chunk_header(&header, buffer.data)) + return NULL; + line_number = header.new.position; + continue; + } + if (!line_number) { + continue; + } + if (type == LINE_DIFF_DEL) { + bias_by_staged_changes--; + continue; + } + assert(type == LINE_DIFF_ADD || type == LINE_DEFAULT || + // These are just context lines that start with [-+]. + type == LINE_DIFF_ADD2 || type == LINE_DIFF_DEL2); + if (type == LINE_DIFF_ADD) + bias_by_staged_changes++; + line_number++; + } + + *lineno -= bias_by_staged_changes; + return file_in_head; +} + static enum request stage_request(struct view *view, enum request request, struct line *line) { @@ -445,9 +536,18 @@ stage_request(struct view *view, enum request request, struct line *line) } view->env->ref[0] = 0; - view->env->goto_lineno = diff_get_lineno(view, line, false); - if (view->env->goto_lineno > 0) - view->env->goto_lineno--; + unsigned long lineno = 0; + const char *old_file; + if ((old_file = find_deleted_line_in_head(view, line, &lineno))) { + string_copy(view->env->ref, "HEAD"); + if (old_file != view->env->file) + string_ncopy(view->env->file, old_file, strlen(old_file)); + } else { + lineno = diff_get_lineno(view, line, false); + } + if (lineno > 0) + lineno--; + view->env->goto_lineno = lineno; return request; case REQ_ENTER: diff --git a/src/status.c b/src/status.c index 6212d8973..da312ec2b 100644 --- a/src/status.c +++ b/src/status.c @@ -54,7 +54,7 @@ status_has_none(struct view *view, struct line *line) /* Get fields from the diff line: * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M */ -static inline bool +inline bool status_get_diff(struct status *file, const char *buf, size_t bufsize) { const char *old_mode = buf + 1;