Skip to content

Commit

Permalink
Blame uncommitted deletions starting from HEAD (#1009)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
krobelus authored Dec 13, 2020
1 parent 9d7b5cd commit c5b1f0c
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 12 deletions.
1 change: 1 addition & 0 deletions NEWS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ master
Improvements:

- Jump from blame to commit. (#355)
- Start blame of an uncommitted deleted line from HEAD so the line's origin can be traced. (#1008)

Bug fixes:

Expand Down
2 changes: 1 addition & 1 deletion include/tig/diff.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ void diff_restore_line(struct view *view, struct diff_state *state);
enum status_code diff_init_highlight(struct view *view, struct diff_state *state);
bool diff_done_highlight(struct diff_state *state);

unsigned int diff_get_lineno(struct view *view, struct line *line);
unsigned int diff_get_lineno(struct view *view, struct line *line, bool old);
const char *diff_get_pathname(struct view *view, struct line *line);

extern struct view diff_view;
Expand Down
1 change: 1 addition & 0 deletions include/tig/status.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 8 additions & 8 deletions src/diff.c
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ diff_save_line(struct view *view, struct diff_state *state, enum open_flags flag

if (file) {
state->file = get_path(file);
state->lineno = diff_get_lineno(view, line);
state->lineno = diff_get_lineno(view, line, false);
state->pos = view->pos;
}
}
Expand Down Expand Up @@ -479,7 +479,7 @@ diff_restore_line(struct view *view, struct diff_state *state)
return;

while ((line = find_next_line_by_type(view, line, LINE_DIFF_CHUNK))) {
unsigned int lineno = diff_get_lineno(view, line);
unsigned int lineno = diff_get_lineno(view, line, false);

for (line++; view_has_line(view, line) && line->type != LINE_DIFF_CHUNK; line++) {
if (lineno == state->lineno) {
Expand Down Expand Up @@ -605,7 +605,7 @@ diff_blame_line(const char *ref, const char *file, unsigned long lineno,
}

unsigned int
diff_get_lineno(struct view *view, struct line *line)
diff_get_lineno(struct view *view, struct line *line, bool old)
{
const struct line *header, *chunk;
unsigned int lineno;
Expand All @@ -625,11 +625,11 @@ diff_get_lineno(struct view *view, struct line *line)
if (!parse_chunk_header(&chunk_header, box_text(chunk)))
return 0;

lineno = chunk_header.new.position;
lineno = old ? chunk_header.old.position : chunk_header.new.position;

for (chunk++; chunk < line; chunk++)
if (chunk->type != LINE_DIFF_DEL &&
chunk->type != LINE_DIFF_DEL2)
if (old ? chunk->type != LINE_DIFF_ADD && chunk->type != LINE_DIFF_ADD2
: chunk->type != LINE_DIFF_DEL && chunk->type != LINE_DIFF_DEL2)
lineno++;

return lineno;
Expand Down Expand Up @@ -761,7 +761,7 @@ diff_common_edit(struct view *view, enum request request, struct line *line)
lineno = view->env->lineno;
} else {
file = diff_get_pathname(view, line);
lineno = diff_get_lineno(view, line);
lineno = diff_get_lineno(view, line, false);
}

if (!file) {
Expand Down Expand Up @@ -827,7 +827,7 @@ diff_common_select(struct view *view, struct line *line, const char *changes_msg
if (changes_msg)
string_format(view->ref, "%s to '%s'", changes_msg, file);
string_format(view->env->file, "%s", file);
view->env->lineno = view->env->goto_lineno = diff_get_lineno(view, line);
view->env->lineno = view->env->goto_lineno = diff_get_lineno(view, line, false);
view->env->blob[0] = 0;
} else {
string_ncopy(view->ref, view->ops->id, strlen(view->ops->id));
Expand Down
119 changes: 117 additions & 2 deletions src/stage.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
struct io io;
struct buffer buffer;
unsigned long line_number_in_head, line_number = 0;
long bias_by_staged_changes = 0;
char buf[SIZEOF_STR] = "";
char file_in_head_pathspec[sizeof("HEAD:") + SIZEOF_STR],
file_in_index_pathspec[sizeof(":") + SIZEOF_STR];
const char *file_in_head = NULL;
const char *ls_tree_argv[] = {
"git", "ls-tree", "-z", "HEAD", view->env->file, NULL
};
const char *diff_argv[] = {
"git", "diff", "--root", file_in_head_pathspec, file_in_index_pathspec,
"--no-color", NULL
};

if (line->type != LINE_DIFF_DEL)
return false;

// Check if the file exists in HEAD.
io_run_buf(ls_tree_argv, buf, sizeof(buf), repo.exec_dir, false);
if (buf[0]) {
file_in_head = view->env->file;
} else { // The file might might be renamed in the index. Find its old name.
struct status file_status;
const char *diff_index_argv[] = {
"git", "diff-index", "--root", "--cached", "-C",
"--diff-filter=ACR", "-z", "HEAD", NULL
};
if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff_index_argv) || io.status)
return false;
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.
line_number_in_head = diff_get_lineno(view, line, /*old=*/true);
assert(line_number_in_head);

// 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.
sprintf(file_in_head_pathspec, "HEAD:%s", file_in_head);
sprintf(file_in_index_pathspec, ":%s", view->env->file);
if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff_argv) || io.status)
return false;
// line_number_in_head 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 < line_number_in_head && 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++;
}

line_number_in_head -= 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 = line_number_in_head;
return true;
}

static enum request
stage_request(struct view *view, enum request request, struct line *line)
{
Expand Down Expand Up @@ -420,7 +532,7 @@ stage_request(struct view *view, enum request request, struct line *line)
if (stage_line_type == LINE_STAT_UNTRACKED) {
open_editor(stage_status.new.name, (line - view->line) + 1);
} else {
open_editor(stage_status.new.name, diff_get_lineno(view, line));
open_editor(stage_status.new.name, diff_get_lineno(view, line, false));
}
break;

Expand All @@ -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);
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;
Expand Down
2 changes: 1 addition & 1 deletion src/status.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c5b1f0c

Please sign in to comment.