Add option to install Tig with Homebrew
[tig.git] / src / blame.c
blob9f3a824019cb6094979b6089244840039dab533e
1 /* Copyright (c) 2006-2014 Jonas Fonseca <jonas.fonseca@gmail.com>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
14 #include "tig/io.h"
15 #include "tig/refdb.h"
16 #include "tig/repo.h"
17 #include "tig/options.h"
18 #include "tig/parse.h"
19 #include "tig/display.h"
20 #include "tig/view.h"
21 #include "tig/draw.h"
22 #include "tig/git.h"
23 #include "tig/diff.h"
26 * Blame backend
28 * Loading the blame view is a two phase job:
30 * 1. File content is read either using argv_env.file from the
31 * filesystem or using git-cat-file.
32 * 2. Then blame information is incrementally added by
33 * reading output from git-blame.
36 struct blame_history_state {
37 char id[SIZEOF_REV]; /* SHA1 ID. */
38 const char *filename; /* Name of file. */
41 static struct view_history blame_view_history = { sizeof(struct blame_history_state) };
43 struct blame {
44 struct blame_commit *commit;
45 unsigned long lineno;
46 char text[1];
49 struct blame_state {
50 struct blame_commit *commit;
51 char author[SIZEOF_STR];
52 int blamed;
53 bool done_reading;
54 bool auto_filename_display;
55 const char *filename;
56 /* The history state for the current view is cached in the view
57 * state so it always matches what was used to load the current blame
58 * view. */
59 struct blame_history_state history_state;
62 static void
63 blame_update_file_name_visibility(struct view *view)
65 struct blame_state *state = view->private;
66 struct view_column *column = get_view_column(view, VIEW_COLUMN_FILE_NAME);
68 if (!column)
69 return;
71 column->hidden = column->opt.file_name.display == FILENAME_NO ||
72 (column->opt.file_name.display == FILENAME_AUTO &&
73 !state->auto_filename_display);
76 static bool
77 blame_open(struct view *view, enum open_flags flags)
79 struct blame_state *state = view->private;
80 const char *file_argv[] = { repo.cdup, view->env->file , NULL };
81 char path[SIZEOF_STR];
82 size_t i;
84 if (opt_blame_options) {
85 for (i = 0; opt_blame_options[i]; i++) {
86 if (prefixcmp(opt_blame_options[i], "-C"))
87 continue;
88 state->auto_filename_display = TRUE;
92 blame_update_file_name_visibility(view);
94 if (is_initial_view(view)) {
95 /* Finish validating and setting up blame options */
96 if (!opt_file_args || opt_file_args[1] || (opt_rev_args && opt_rev_args[1]))
97 usage("Invalid number of options to blame");
99 if (opt_rev_args) {
100 string_ncopy(view->env->ref, opt_rev_args[0], strlen(opt_rev_args[0]));
103 string_ncopy(view->env->file, opt_file_args[0], strlen(opt_file_args[0]));
105 opt_blame_options = opt_cmdline_args;
106 opt_cmdline_args = NULL;
109 if (!view->env->file[0]) {
110 report("No file chosen, press %s to open tree view",
111 get_view_key(view, REQ_VIEW_TREE));
112 return FALSE;
115 if (!view->prev && *repo.prefix && !(flags & (OPEN_RELOAD | OPEN_REFRESH))) {
116 string_copy(path, view->env->file);
117 if (!string_format(view->env->file, "%s%s", repo.prefix, path)) {
118 report("Failed to setup the blame view");
119 return FALSE;
123 if (*view->env->ref || !begin_update(view, repo.cdup, file_argv, flags)) {
124 const char *blame_cat_file_argv[] = {
125 "git", "cat-file", "blob", "%(ref):%(file)", NULL
128 if (!begin_update(view, repo.cdup, blame_cat_file_argv, flags))
129 return FALSE;
132 /* First pass: remove multiple references to the same commit. */
133 for (i = 0; i < view->lines; i++) {
134 struct blame *blame = view->line[i].data;
136 if (blame->commit && blame->commit->id[0])
137 blame->commit->id[0] = 0;
138 else
139 blame->commit = NULL;
142 /* Second pass: free existing references. */
143 for (i = 0; i < view->lines; i++) {
144 struct blame *blame = view->line[i].data;
146 if (blame->commit)
147 free(blame->commit);
150 if (!(flags & OPEN_RELOAD))
151 reset_view_history(&blame_view_history);
152 string_copy_rev(state->history_state.id, view->env->ref);
153 state->history_state.filename = get_path(view->env->file);
154 if (!state->history_state.filename)
155 return FALSE;
156 string_format(view->vid, "%s", view->env->file);
157 string_format(view->ref, "%s ...", view->env->file);
159 return TRUE;
162 static struct blame_commit *
163 get_blame_commit(struct view *view, const char *id)
165 size_t i;
167 for (i = 0; i < view->lines; i++) {
168 struct blame *blame = view->line[i].data;
170 if (!blame->commit)
171 continue;
173 if (!strncmp(blame->commit->id, id, SIZEOF_REV - 1))
174 return blame->commit;
178 struct blame_commit *commit = calloc(1, sizeof(*commit));
180 if (commit)
181 string_ncopy(commit->id, id, SIZEOF_REV);
182 return commit;
186 static struct blame_commit *
187 read_blame_commit(struct view *view, const char *text, struct blame_state *state)
189 struct blame_header header;
190 struct blame_commit *commit;
191 struct blame *blame;
193 if (!parse_blame_header(&header, text, view->lines))
194 return NULL;
196 commit = get_blame_commit(view, text);
197 if (!commit)
198 return NULL;
200 state->blamed += header.group;
201 while (header.group--) {
202 struct line *line = &view->line[header.lineno + header.group - 1];
204 blame = line->data;
205 blame->commit = commit;
206 blame->lineno = header.orig_lineno + header.group - 1;
207 line->dirty = 1;
210 return commit;
213 static bool
214 blame_read_file(struct view *view, struct buffer *buf, struct blame_state *state)
216 if (!buf) {
217 const char *blame_argv[] = {
218 "git", "blame", encoding_arg, "%(blameargs)", "--incremental",
219 *view->env->ref ? view->env->ref : "--incremental", "--", view->env->file, NULL
222 if (failed_to_load_initial_view(view))
223 die("No blame exist for %s", view->vid);
225 if (view->lines == 0 || !begin_update(view, repo.cdup, blame_argv, OPEN_EXTRA)) {
226 report("Failed to load blame data");
227 return TRUE;
230 if (view->env->lineno > 0) {
231 select_view_line(view, view->env->lineno);
232 view->env->lineno = 0;
235 state->done_reading = TRUE;
236 return FALSE;
238 } else {
239 struct blame *blame;
241 if (!add_line_alloc(view, &blame, LINE_DEFAULT, buf->size, FALSE))
242 return FALSE;
244 blame->commit = NULL;
245 strncpy(blame->text, buf->data, buf->size);
246 blame->text[buf->size] = 0;
247 return TRUE;
251 static bool
252 blame_read(struct view *view, struct buffer *buf)
254 struct blame_state *state = view->private;
256 if (!state->done_reading)
257 return blame_read_file(view, buf, state);
259 if (!buf) {
260 string_format(view->ref, "%s", view->vid);
261 if (view_is_displayed(view)) {
262 update_view_title(view);
263 redraw_view_from(view, 0);
265 return TRUE;
268 if (!state->commit) {
269 state->commit = read_blame_commit(view, buf->data, state);
270 string_format(view->ref, "%s %2zd%%", view->vid,
271 view->lines ? state->blamed * 100 / view->lines : 0);
273 } else if (parse_blame_info(state->commit, state->author, buf->data)) {
274 bool update_view_columns = TRUE;
275 int i;
277 if (!state->commit->filename)
278 return FALSE;
280 if (!state->filename) {
281 state->filename = state->commit->filename;
282 } else if (strcmp(state->filename, state->commit->filename)) {
283 state->auto_filename_display = TRUE;
284 view->force_redraw = TRUE;
285 blame_update_file_name_visibility(view);
288 for (i = 0; i < view->lines; i++) {
289 struct line *line = &view->line[i];
290 struct blame *blame = line->data;
292 if (blame && blame->commit == state->commit) {
293 line->dirty = 1;
294 if (update_view_columns)
295 view_column_info_update(view, line);
296 update_view_columns = FALSE;
300 state->commit = NULL;
303 return TRUE;
306 bool
307 blame_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
309 struct blame *blame = line->data;
311 if (blame->commit) {
312 column_data->id = blame->commit->id;
313 column_data->author = blame->commit->author;
314 column_data->file_name = blame->commit->filename;
315 column_data->date = &blame->commit->time;
316 column_data->commit_title = blame->commit->title;
319 column_data->text = blame->text;
321 return TRUE;
324 static bool
325 check_blame_commit(struct blame *blame, bool check_null_id)
327 if (!blame->commit)
328 report("Commit data not loaded yet");
329 else if (check_null_id && string_rev_is_null(blame->commit->id))
330 report("No commit exist for the selected line");
331 else
332 return TRUE;
333 return FALSE;
336 static void
337 setup_blame_parent_line(struct view *view, struct blame *blame)
339 char from[SIZEOF_REF + SIZEOF_STR];
340 char to[SIZEOF_REF + SIZEOF_STR];
341 const char *diff_tree_argv[] = {
342 "git", "diff", encoding_arg, "--no-textconv", "--no-ext-diff",
343 "--no-color", "-U0", from, to, "--", NULL
345 struct io io;
346 int parent_lineno = -1;
347 int blamed_lineno = -1;
348 struct buffer buf;
350 if (!string_format(from, "%s:%s", view->env->ref, view->env->file) ||
351 !string_format(to, "%s:%s", blame->commit->id, blame->commit->filename) ||
352 !io_run(&io, IO_RD, NULL, opt_env, diff_tree_argv))
353 return;
355 while (io_get(&io, &buf, '\n', TRUE)) {
356 char *line = buf.data;
358 if (*line == '@') {
359 char *pos = strchr(line, '+');
361 parent_lineno = atoi(line + 4);
362 if (pos)
363 blamed_lineno = atoi(pos + 1);
365 } else if (*line == '+' && parent_lineno != -1) {
366 if (blame->lineno == blamed_lineno - 1 &&
367 !strcmp(blame->text, line + 1)) {
368 view->pos.lineno = parent_lineno ? parent_lineno - 1 : 0;
369 break;
371 blamed_lineno++;
375 io_done(&io);
378 static void
379 blame_go_forward(struct view *view, struct blame *blame, bool parent)
381 struct blame_state *state = view->private;
382 struct blame_history_state *history_state = &state->history_state;
383 struct blame_commit *commit = blame->commit;
384 const char *id = parent ? commit->parent_id : commit->id;
385 const char *filename = parent ? commit->parent_filename : commit->filename;
387 if (!*id && parent) {
388 report("The selected commit has no parents");
389 return;
392 if (!strcmp(history_state->id, id) && !strcmp(history_state->filename, filename)) {
393 report("The selected commit is already displayed");
394 return;
397 if (!push_view_history_state(&blame_view_history, &view->pos, history_state)) {
398 report("Failed to save current view state");
399 return;
402 string_ncopy(view->env->ref, id, sizeof(commit->id));
403 string_ncopy(view->env->file, filename, strlen(filename));
404 if (parent)
405 setup_blame_parent_line(view, blame);
406 view->env->lineno = blame->lineno;
407 reload_view(view);
410 static void
411 blame_go_back(struct view *view)
413 struct blame_history_state history_state;
415 if (!pop_view_history_state(&blame_view_history, &view->pos, &history_state)) {
416 report("Already at start of history");
417 return;
420 string_copy(view->env->ref, history_state.id);
421 string_ncopy(view->env->file, history_state.filename, strlen(history_state.filename));
422 view->env->lineno = view->pos.lineno;
423 reload_view(view);
426 static enum request
427 blame_request(struct view *view, enum request request, struct line *line)
429 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
430 struct blame *blame = line->data;
431 struct view *diff = &diff_view;
433 switch (request) {
434 case REQ_VIEW_BLAME:
435 case REQ_PARENT:
436 if (!check_blame_commit(blame, request == REQ_VIEW_BLAME))
437 break;
438 blame_go_forward(view, blame, request == REQ_PARENT);
439 break;
441 case REQ_BACK:
442 blame_go_back(view);
443 break;
445 case REQ_ENTER:
446 if (!check_blame_commit(blame, FALSE))
447 break;
449 if (view_is_displayed(diff) &&
450 !strcmp(blame->commit->id, diff->ref))
451 break;
453 if (string_rev_is_null(blame->commit->id)) {
454 const char *diff_parent_argv[] = {
455 GIT_DIFF_BLAME(encoding_arg,
456 diff_context_arg(),
457 ignore_space_arg(),
458 blame->commit->filename)
460 const char *diff_no_parent_argv[] = {
461 GIT_DIFF_BLAME_NO_PARENT(encoding_arg,
462 diff_context_arg(),
463 ignore_space_arg(),
464 blame->commit->filename)
466 const char **diff_index_argv = *blame->commit->parent_id
467 ? diff_parent_argv : diff_no_parent_argv;
469 open_argv(view, diff, diff_index_argv, NULL, flags);
470 if (diff->pipe)
471 string_copy_rev(diff->ref, NULL_ID);
472 } else {
473 open_diff_view(view, flags);
475 break;
477 default:
478 return request;
481 return REQ_NONE;
484 static void
485 blame_select(struct view *view, struct line *line)
487 struct blame *blame = line->data;
488 struct blame_commit *commit = blame->commit;
490 if (!commit)
491 return;
493 if (string_rev_is_null(commit->id))
494 string_ncopy(view->env->commit, "HEAD", 4);
495 else
496 string_copy_rev(view->env->commit, commit->id);
499 static struct view_ops blame_ops = {
500 "line",
501 argv_env.commit,
502 VIEW_SEND_CHILD_ENTER | VIEW_BLAME_LIKE,
503 sizeof(struct blame_state),
504 blame_open,
505 blame_read,
506 view_column_draw,
507 blame_request,
508 view_column_grep,
509 blame_select,
510 NULL,
511 view_column_bit(AUTHOR) | view_column_bit(DATE) |
512 view_column_bit(FILE_NAME) | view_column_bit(ID) |
513 view_column_bit(LINE_NUMBER) | view_column_bit(TEXT),
514 blame_get_column_data,
517 DEFINE_VIEW(blame);
519 /* vim: set ts=8 sw=8 noexpandtab: */