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;