1 /* Copyright (c) 2006-2015 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.
15 #include "tig/refdb.h"
17 #include "tig/options.h"
18 #include "tig/parse.h"
19 #include "tig/display.h"
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
) };
44 struct blame_commit
*commit
;
50 struct blame_commit
*commit
;
51 char author
[SIZEOF_STR
];
54 bool auto_filename_display
;
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
59 struct blame_history_state history_state
;
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
);
71 column
->hidden
= column
->opt
.file_name
.display
== FILENAME_NO
||
72 (column
->opt
.file_name
.display
== FILENAME_AUTO
&&
73 !state
->auto_filename_display
);
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
];
84 if (opt_blame_options
) {
85 for (i
= 0; opt_blame_options
[i
]; i
++) {
86 if (prefixcmp(opt_blame_options
[i
], "-C"))
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");
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
));
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");
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
))
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;
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
;
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
)
156 string_format(view
->vid
, "%s", view
->env
->file
);
157 string_format(view
->ref
, "%s ...", view
->env
->file
);
162 static struct blame_commit
*
163 get_blame_commit(struct view
*view
, const char *id
)
167 for (i
= 0; i
< view
->lines
; i
++) {
168 struct blame
*blame
= view
->line
[i
].data
;
173 if (!strncmp(blame
->commit
->id
, id
, SIZEOF_REV
- 1))
174 return blame
->commit
;
178 struct blame_commit
*commit
= calloc(1, sizeof(*commit
));
181 string_ncopy(commit
->id
, id
, SIZEOF_REV
);
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
;
193 if (!parse_blame_header(&header
, text
, view
->lines
))
196 commit
= get_blame_commit(view
, text
);
200 state
->blamed
+= header
.group
;
201 while (header
.group
--) {
202 struct line
*line
= &view
->line
[header
.lineno
+ header
.group
- 1];
205 blame
->commit
= commit
;
206 blame
->lineno
= header
.orig_lineno
+ header
.group
- 1;
214 blame_read_file(struct view
*view
, struct buffer
*buf
, struct blame_state
*state
)
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");
230 if (view
->env
->goto_lineno
> 0) {
231 select_view_line(view
, view
->env
->goto_lineno
);
232 view
->env
->goto_lineno
= 0;
235 state
->done_reading
= TRUE
;
241 if (!add_line_alloc(view
, &blame
, LINE_DEFAULT
, buf
->size
, FALSE
))
244 blame
->commit
= NULL
;
245 strncpy(blame
->text
, buf
->data
, buf
->size
);
246 blame
->text
[buf
->size
] = 0;
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
);
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);
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
;
277 if (!state
->commit
->filename
)
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
) {
294 if (update_view_columns
)
295 view_column_info_update(view
, line
);
296 update_view_columns
= FALSE
;
300 state
->commit
= NULL
;
307 blame_get_column_data(struct view
*view
, const struct line
*line
, struct view_column_data
*column_data
)
309 struct blame
*blame
= line
->data
;
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
;
325 check_blame_commit(struct blame
*blame
, bool check_null_id
)
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");
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
346 int parent_lineno
= -1;
347 int blamed_lineno
= -1;
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
))
355 while (io_get(&io
, &buf
, '\n', TRUE
)) {
356 char *line
= buf
.data
;
359 char *pos
= strchr(line
, '+');
361 parent_lineno
= atoi(line
+ 4);
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;
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");
392 if (!strcmp(history_state
->id
, id
) && !strcmp(history_state
->filename
, filename
)) {
393 report("The selected commit is already displayed");
397 if (!push_view_history_state(&blame_view_history
, &view
->pos
, history_state
)) {
398 report("Failed to save current view state");
402 string_ncopy(view
->env
->ref
, id
, sizeof(commit
->id
));
403 string_ncopy(view
->env
->file
, filename
, strlen(filename
));
405 setup_blame_parent_line(view
, blame
);
406 view
->env
->goto_lineno
= blame
->lineno
;
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");
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
->goto_lineno
= view
->pos
.lineno
;
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
;
436 if (!check_blame_commit(blame
, request
== REQ_VIEW_BLAME
))
438 blame_go_forward(view
, blame
, request
== REQ_PARENT
);
446 if (!check_blame_commit(blame
, FALSE
))
449 if (view_is_displayed(diff
) &&
450 !strcmp(blame
->commit
->id
, diff
->ref
))
453 if (string_rev_is_null(blame
->commit
->id
)) {
454 const char *diff_parent_argv
[] = {
455 GIT_DIFF_BLAME(encoding_arg
,
458 blame
->commit
->filename
)
460 const char *diff_no_parent_argv
[] = {
461 GIT_DIFF_BLAME_NO_PARENT(encoding_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
);
471 string_copy_rev(diff
->ref
, NULL_ID
);
473 open_diff_view(view
, flags
);
485 blame_select(struct view
*view
, struct line
*line
)
487 struct blame
*blame
= line
->data
;
488 struct blame_commit
*commit
= blame
->commit
;
493 if (string_rev_is_null(commit
->id
))
494 string_ncopy(view
->env
->commit
, "HEAD", 4);
496 string_copy_rev(view
->env
->commit
, commit
->id
);
498 if (commit
->filename
)
499 string_format(view
->env
->file
, "%s", commit
->filename
);
500 view
->env
->lineno
= view
->pos
.lineno
+ 1;
503 static struct view_ops blame_ops
= {
506 VIEW_SEND_CHILD_ENTER
| VIEW_BLAME_LIKE
,
507 sizeof(struct blame_state
),
515 view_column_bit(AUTHOR
) | view_column_bit(DATE
) |
516 view_column_bit(FILE_NAME
) | view_column_bit(ID
) |
517 view_column_bit(LINE_NUMBER
) | view_column_bit(TEXT
),
518 blame_get_column_data
,
523 /* vim: set ts=8 sw=8 noexpandtab: */