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.
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
.show
== FILENAME_NO
||
72 (column
->opt
.file_name
.show
== 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_argv
|| opt_file_argv
[1] || (opt_rev_argv
&& opt_rev_argv
[1]))
97 usage("Invalid number of options to blame");
100 string_ncopy(view
->env
->ref
, opt_rev_argv
[0], strlen(opt_rev_argv
[0]));
103 string_ncopy(view
->env
->file
, opt_file_argv
[0], strlen(opt_file_argv
[0]));
105 opt_blame_options
= opt_cmdline_argv
;
106 opt_cmdline_argv
= 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
, const char *text
, 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
->lineno
> 0) {
231 select_view_line(view
, view
->env
->lineno
);
232 view
->env
->lineno
= 0;
235 state
->done_reading
= TRUE
;
239 size_t textlen
= strlen(text
);
242 if (!add_line_alloc(view
, &blame
, LINE_DEFAULT
, textlen
, FALSE
))
245 blame
->commit
= NULL
;
246 strncpy(blame
->text
, text
, textlen
);
247 blame
->text
[textlen
] = 0;
253 blame_read(struct view
*view
, char *line
)
255 struct blame_state
*state
= view
->private;
257 if (!state
->done_reading
)
258 return blame_read_file(view
, line
, state
);
261 string_format(view
->ref
, "%s", view
->vid
);
262 if (view_is_displayed(view
)) {
263 update_view_title(view
);
264 redraw_view_from(view
, 0);
269 if (!state
->commit
) {
270 state
->commit
= read_blame_commit(view
, line
, state
);
271 string_format(view
->ref
, "%s %2zd%%", view
->vid
,
272 view
->lines
? state
->blamed
* 100 / view
->lines
: 0);
274 } else if (parse_blame_info(state
->commit
, state
->author
, line
)) {
275 bool update_view_columns
= TRUE
;
278 if (!state
->commit
->filename
)
281 if (!state
->filename
) {
282 state
->filename
= state
->commit
->filename
;
283 } else if (strcmp(state
->filename
, state
->commit
->filename
)) {
284 state
->auto_filename_display
= TRUE
;
285 view
->force_redraw
= TRUE
;
286 blame_update_file_name_visibility(view
);
289 for (i
= 0; i
< view
->lines
; i
++) {
290 struct line
*line
= &view
->line
[i
];
291 struct blame
*blame
= line
->data
;
293 if (blame
&& blame
->commit
== state
->commit
) {
295 if (update_view_columns
)
296 view_column_info_update(view
, line
);
297 update_view_columns
= FALSE
;
301 state
->commit
= NULL
;
308 blame_get_column_data(struct view
*view
, const struct line
*line
, struct view_column_data
*column_data
)
310 struct blame
*blame
= line
->data
;
313 column_data
->id
= blame
->commit
->id
;
314 column_data
->author
= blame
->commit
->author
;
315 column_data
->file_name
= blame
->commit
->filename
;
316 column_data
->date
= &blame
->commit
->time
;
317 column_data
->commit_title
= blame
->commit
->title
;
320 column_data
->text
= blame
->text
;
326 check_blame_commit(struct blame
*blame
, bool check_null_id
)
329 report("Commit data not loaded yet");
330 else if (check_null_id
&& string_rev_is_null(blame
->commit
->id
))
331 report("No commit exist for the selected line");
338 setup_blame_parent_line(struct view
*view
, struct blame
*blame
)
340 char from
[SIZEOF_REF
+ SIZEOF_STR
];
341 char to
[SIZEOF_REF
+ SIZEOF_STR
];
342 const char *diff_tree_argv
[] = {
343 "git", "diff", encoding_arg
, "--no-textconv", "--no-extdiff",
344 "--no-color", "-U0", from
, to
, "--", NULL
347 int parent_lineno
= -1;
348 int blamed_lineno
= -1;
351 if (!string_format(from
, "%s:%s", view
->env
->ref
, view
->env
->file
) ||
352 !string_format(to
, "%s:%s", blame
->commit
->id
, blame
->commit
->filename
) ||
353 !io_run(&io
, IO_RD
, NULL
, opt_env
, diff_tree_argv
))
356 while ((line
= io_get(&io
, '\n', TRUE
))) {
358 char *pos
= strchr(line
, '+');
360 parent_lineno
= atoi(line
+ 4);
362 blamed_lineno
= atoi(pos
+ 1);
364 } else if (*line
== '+' && parent_lineno
!= -1) {
365 if (blame
->lineno
== blamed_lineno
- 1 &&
366 !strcmp(blame
->text
, line
+ 1)) {
367 view
->pos
.lineno
= parent_lineno
? parent_lineno
- 1 : 0;
378 blame_go_forward(struct view
*view
, struct blame
*blame
, bool parent
)
380 struct blame_state
*state
= view
->private;
381 struct blame_history_state
*history_state
= &state
->history_state
;
382 struct blame_commit
*commit
= blame
->commit
;
383 const char *id
= parent
? commit
->parent_id
: commit
->id
;
384 const char *filename
= parent
? commit
->parent_filename
: commit
->filename
;
386 if (!*id
&& parent
) {
387 report("The selected commit has no parents");
391 if (!strcmp(history_state
->id
, id
) && !strcmp(history_state
->filename
, filename
)) {
392 report("The selected commit is already displayed");
396 if (!push_view_history_state(&blame_view_history
, &view
->pos
, history_state
)) {
397 report("Failed to save current view state");
401 string_ncopy(view
->env
->ref
, id
, sizeof(commit
->id
));
402 string_ncopy(view
->env
->file
, filename
, strlen(filename
));
404 setup_blame_parent_line(view
, blame
);
405 view
->env
->lineno
= blame
->lineno
;
410 blame_go_back(struct view
*view
)
412 struct blame_history_state history_state
;
414 if (!pop_view_history_state(&blame_view_history
, &view
->pos
, &history_state
)) {
415 report("Already at start of history");
419 string_copy(view
->env
->ref
, history_state
.id
);
420 string_ncopy(view
->env
->file
, history_state
.filename
, strlen(history_state
.filename
));
421 view
->env
->lineno
= view
->pos
.lineno
;
426 blame_request(struct view
*view
, enum request request
, struct line
*line
)
428 enum open_flags flags
= view_is_displayed(view
) ? OPEN_SPLIT
: OPEN_DEFAULT
;
429 struct blame
*blame
= line
->data
;
430 struct view
*diff
= &diff_view
;
435 if (!check_blame_commit(blame
, TRUE
))
437 blame_go_forward(view
, blame
, request
== REQ_PARENT
);
445 if (!check_blame_commit(blame
, FALSE
))
448 if (view_is_displayed(diff
) &&
449 !strcmp(blame
->commit
->id
, diff
->ref
))
452 if (string_rev_is_null(blame
->commit
->id
)) {
453 const char *diff_parent_argv
[] = {
454 GIT_DIFF_BLAME(encoding_arg
,
456 ignore_space_arg(), view
->vid
)
458 const char *diff_no_parent_argv
[] = {
459 GIT_DIFF_BLAME_NO_PARENT(encoding_arg
,
461 ignore_space_arg(), view
->vid
)
463 const char **diff_index_argv
= *blame
->commit
->parent_id
464 ? diff_parent_argv
: diff_no_parent_argv
;
466 open_argv(view
, diff
, diff_index_argv
, NULL
, flags
);
468 string_copy_rev(diff
->ref
, NULL_ID
);
470 open_diff_view(view
, flags
);
482 blame_select(struct view
*view
, struct line
*line
)
484 struct blame
*blame
= line
->data
;
485 struct blame_commit
*commit
= blame
->commit
;
490 if (string_rev_is_null(commit
->id
))
491 string_ncopy(view
->env
->commit
, "HEAD", 4);
493 string_copy_rev(view
->env
->commit
, commit
->id
);
496 static struct view_ops blame_ops
= {
499 VIEW_SEND_CHILD_ENTER
| VIEW_BLAME_LIKE
,
500 sizeof(struct blame_state
),
508 view_column_bit(AUTHOR
) | view_column_bit(DATE
) |
509 view_column_bit(FILE_NAME
) | view_column_bit(ID
) |
510 view_column_bit(LINE_NUMBER
) | view_column_bit(TEXT
),
511 blame_get_column_data
,
516 /* vim: set ts=8 sw=8 noexpandtab: */