diff --git a/NEWS.adoc b/NEWS.adoc index e13e1c199..2ef5bfd56 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -13,6 +13,10 @@ Bug fixes: - Fix wrapping of lines with multibyte characters. (#988) - Improve highlighting of search with $ regex. (#1000) +Improvements: + + - Start blame of an uncommitted deleted line 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..c97920c3a 100644 --- a/src/stage.c +++ b/src/stage.c @@ -366,6 +366,118 @@ stage_chunk_is_wrapped(struct view *view, struct line *line) return false; } +bool +find_deleted_line_in_head(struct view *view, struct line *line) { + if (line->type != LINE_DIFF_DEL) + return false; + + // Check if the file exists in HEAD. + const char *file_in_head = NULL; + const char *ls_tree_argv[] = { + "git", "ls-tree", "-z", "HEAD", view->env->file, NULL + }; + char buf[SIZEOF_STR] = ""; + io_run_buf(ls_tree_argv, buf, sizeof(buf), repo.exec_dir, false); + if (buf[0]) + file_in_head = view->env->file; + + struct io io; + struct buffer buffer; + // The file might might be renamed in the index. Find its old name. + if (!file_in_head) { + 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 false; + struct status file_status; + while (io_get(&io, &buffer, 0, true)) { + if (!status_get_diff(&file_status, buffer.data, buffer.size)) + return false; + if (file_status.status != 'A') { + if (!io_get(&io, &buffer, 0, true)) + return false; + string_ncopy(file_status.old.name, buffer.data, buffer.size); + } + if (!io_get(&io, &buffer, 0, true)) + return false; + 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 false; + } + file_in_head = file_status.old.name; + break; + } + } + + if (!file_in_head) + return false; + + // We want to compute the line number in HEAD. The current view is a diff + // of (un)staged changes on top of HEAD. + unsigned long lineno = diff_get_lineno(view, line, /*old=*/true); + assert(lineno); + + // When looking at staged changes, we already have the correct + // line number in HEAD. + if (stage_line_type == LINE_STAT_STAGED) + goto found_line; + + // 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 false; + 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 false; + 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 happen to 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; + +found_line: + if (file_in_head != view->env->file) + string_ncopy(view->env->file, file_in_head, strlen(file_in_head)); + view->env->goto_lineno = lineno; + return true; +} + static enum request stage_request(struct view *view, enum request request, struct line *line) { @@ -445,7 +557,10 @@ 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 (find_deleted_line_in_head(view, line)) + string_copy(view->env->ref, "HEAD"); + else + view->env->goto_lineno = diff_get_lineno(view, line, false); if (view->env->goto_lineno > 0) view->env->goto_lineno--; return request; 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;