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/display.h"
19 #include "tig/parse.h"
20 #include "tig/pager.h"
25 diff_open(struct view
*view
, enum open_flags flags
)
27 const char *diff_argv
[] = {
28 "git", "show", encoding_arg
, "--pretty=fuller", "--root",
29 "--patch-with-stat", use_mailmap_arg(),
30 show_notes_arg(), diff_context_arg(), ignore_space_arg(),
31 "%(diffargs)", "%(cmdlineargs)", "--no-color", "%(commit)",
32 "--", "%(fileargs)", NULL
35 diff_save_line(view
, view
->private, flags
);
37 return begin_update(view
, NULL
, diff_argv
, flags
);
40 struct diff_stat_context
{
44 struct box_cell cell
[10];
48 diff_common_add_cell(struct diff_stat_context
*context
, size_t length
)
50 assert(ARRAY_SIZE(context
->cell
) > context
->cells
);
53 context
->cell
[context
->cells
].length
= length
;
54 context
->cell
[context
->cells
].type
= context
->type
;
59 diff_common_read_diff_stat_part(struct diff_stat_context
*context
, char c
, enum line_type next_type
)
61 const char *sep
= c
== '|' ? strrchr(context
->text
, c
) : strchr(context
->text
, c
);
66 diff_common_add_cell(context
, sep
- context
->text
);
68 context
->type
= next_type
;
74 diff_common_read_diff_stat(struct view
*view
, const char *text
)
76 struct diff_stat_context context
= { text
, LINE_DIFF_STAT
};
80 diff_common_read_diff_stat_part(&context
, '|', LINE_DEFAULT
);
81 if (diff_common_read_diff_stat_part(&context
, 'B', LINE_DEFAULT
)) {
82 /* Handle binary diffstat: Bin <deleted> -> <added> bytes */
83 diff_common_read_diff_stat_part(&context
, ' ', LINE_DIFF_DEL
);
84 diff_common_read_diff_stat_part(&context
, '-', LINE_DEFAULT
);
85 diff_common_read_diff_stat_part(&context
, ' ', LINE_DIFF_ADD
);
86 diff_common_read_diff_stat_part(&context
, 'b', LINE_DEFAULT
);
89 diff_common_read_diff_stat_part(&context
, '+', LINE_DIFF_ADD
);
90 diff_common_read_diff_stat_part(&context
, '-', LINE_DIFF_DEL
);
92 diff_common_add_cell(&context
, strlen(context
.text
));
94 line
= add_line_text_at(view
, view
->lines
, text
, LINE_DIFF_STAT
, context
.cells
);
100 memcpy(box
->cell
, context
.cell
, sizeof(struct box_cell
) * context
.cells
);
101 box
->cells
= context
.cells
;
106 diff_common_add_diff_stat(struct view
*view
, const char *text
, size_t offset
)
108 const char *start
= text
+ offset
;
109 const char *data
= start
+ strspn(start
, " ");
110 size_t len
= strlen(data
);
111 char *pipe
= strchr(data
, '|');
113 /* Ensure that '|' is present and the file name part contains
114 * non-space characters. */
115 if (!pipe
|| pipe
== data
|| strcspn(data
, " ") == 0)
118 /* Detect remaining part of a diff stat line:
120 * added | 40 +++++++++++
121 * remove | 124 --------------------------
123 * rename.from => rename.to | 0
124 * .../truncated file name | 11 ++---
125 * binary add | Bin 0 -> 1234 bytes
126 * binary update | Bin 1234 -> 2345 bytes
127 * unmerged | Unmerged
129 if ((data
[len
- 1] == '-' || data
[len
- 1] == '+') ||
130 strstr(pipe
, " 0") ||
131 (strstr(pipe
, "Bin") && strstr(pipe
, "->")) ||
132 strstr(pipe
, "Unmerged") ||
133 (data
[len
- 1] == '0' && (strstr(data
, "=>") || !prefixcmp(data
, "..."))))
134 return diff_common_read_diff_stat(view
, text
);
139 diff_common_read(struct view
*view
, const char *data
, struct diff_state
*state
)
141 enum line_type type
= get_line_type(data
);
143 if (!view
->lines
&& type
!= LINE_COMMIT
)
144 state
->reading_diff_stat
= true;
146 if (state
->combined_diff
&& !state
->after_diff
&& data
[0] == ' ' && data
[1] != ' ')
147 state
->reading_diff_stat
= true;
149 if (state
->reading_diff_stat
) {
150 if (diff_common_add_diff_stat(view
, data
, 0))
152 state
->reading_diff_stat
= false;
154 } else if (!strcmp(data
, "---")) {
155 state
->reading_diff_stat
= true;
158 if (!state
->after_commit_title
&& !prefixcmp(data
, " ")) {
159 struct line
*line
= add_line_text(view
, data
, LINE_DEFAULT
);
162 line
->commit_title
= 1;
163 state
->after_commit_title
= true;
167 if (type
== LINE_DIFF_HEADER
) {
168 const int len
= STRING_SIZE("diff --");
170 state
->after_diff
= true;
171 if (!strncmp(data
+ len
, "combined ", strlen("combined ")) ||
172 !strncmp(data
+ len
, "cc ", strlen("cc ")))
173 state
->combined_diff
= true;
175 } else if (type
== LINE_DIFF_CHUNK
) {
176 const char *context
= strstr(data
+ STRING_SIZE("@@"), "@@");
178 context
? add_line_text_at(view
, view
->lines
, data
, LINE_DIFF_CHUNK
, 2)
186 box
->cell
[0].length
= (context
+ 2) - data
;
187 box
->cell
[1].length
= strlen(context
+ 2);
188 box
->cell
[box
->cells
++].type
= LINE_DIFF_STAT
;
191 } else if (type
== LINE_PP_MERGE
) {
192 state
->combined_diff
= true;
195 /* ADD2 and DEL2 are only valid in combined diff hunks */
196 if (!state
->combined_diff
&& (type
== LINE_DIFF_ADD2
|| type
== LINE_DIFF_DEL2
))
199 return pager_common_read(view
, data
, type
, NULL
);
203 diff_find_stat_entry(struct view
*view
, struct line
*line
, enum line_type type
)
205 struct line
*marker
= find_next_line_by_type(view
, line
, type
);
208 line
== find_prev_line_by_type(view
, marker
, LINE_DIFF_HEADER
);
212 diff_find_header_from_stat(struct view
*view
, struct line
*line
)
214 if (line
->type
== LINE_DIFF_STAT
) {
217 while (view_has_line(view
, line
) && line
->type
== LINE_DIFF_STAT
) {
222 for (line
= view
->line
; view_has_line(view
, line
); line
++) {
223 line
= find_next_line_by_type(view
, line
, LINE_DIFF_HEADER
);
227 if (diff_find_stat_entry(view
, line
, LINE_DIFF_INDEX
)
228 || diff_find_stat_entry(view
, line
, LINE_DIFF_SIMILARITY
)) {
229 if (file_number
== 1) {
243 diff_common_enter(struct view
*view
, enum request request
, struct line
*line
)
245 if (line
->type
== LINE_DIFF_STAT
) {
246 line
= diff_find_header_from_stat(view
, line
);
248 report("Failed to find file diff");
252 select_view_line(view
, line
- view
->line
);
257 return pager_request(view
, request
, line
);
262 diff_save_line(struct view
*view
, struct diff_state
*state
, enum open_flags flags
)
264 if (flags
& OPEN_RELOAD
) {
265 struct line
*line
= &view
->line
[view
->pos
.lineno
];
266 const char *file
= view_has_line(view
, line
) ? diff_get_pathname(view
, line
) : NULL
;
269 state
->file
= get_path(file
);
270 state
->lineno
= diff_get_lineno(view
, line
);
271 state
->pos
= view
->pos
;
277 diff_restore_line(struct view
*view
, struct diff_state
*state
)
279 struct line
*line
= &view
->line
[view
->lines
- 1];
284 while ((line
= find_prev_line_by_type(view
, line
, LINE_DIFF_HEADER
))) {
285 const char *file
= diff_get_pathname(view
, line
);
287 if (file
&& !strcmp(file
, state
->file
))
297 while ((line
= find_next_line_by_type(view
, line
, LINE_DIFF_CHUNK
))) {
298 unsigned int lineno
= diff_get_lineno(view
, line
);
300 for (line
++; view_has_line(view
, line
) && line
->type
!= LINE_DIFF_CHUNK
; line
++) {
301 if (lineno
== state
->lineno
) {
302 unsigned long lineno
= line
- view
->line
;
303 unsigned long offset
= lineno
- (state
->pos
.lineno
- state
->pos
.offset
);
305 goto_view_line(view
, offset
, lineno
);
309 if (line
->type
!= LINE_DIFF_DEL
&&
310 line
->type
!= LINE_DIFF_DEL2
)
317 diff_read_describe(struct view
*view
, struct buffer
*buffer
, struct diff_state
*state
)
319 struct line
*line
= find_next_line_by_type(view
, view
->line
, LINE_PP_REFS
);
321 if (line
&& buffer
) {
322 const char *ref
= chomp_string(buffer
->data
);
323 const char *sep
= !strcmp("Refs: ", box_text(line
)) ? "" : ", ";
325 if (*ref
&& !append_line_format(view
, line
, "%s%s", sep
, ref
))
333 diff_read(struct view
*view
, struct buffer
*buf
, bool force_stop
)
335 struct diff_state
*state
= view
->private;
337 if (state
->adding_describe_ref
)
338 return diff_read_describe(view
, buf
, state
);
341 /* Fall back to retry if no diff will be shown. */
342 if (view
->lines
== 0 && opt_file_args
) {
343 int pos
= argv_size(view
->argv
)
344 - argv_size(opt_file_args
) - 1;
346 if (pos
> 0 && !strcmp(view
->argv
[pos
], "--")) {
347 for (; view
->argv
[pos
]; pos
++) {
348 free((void *) view
->argv
[pos
]);
349 view
->argv
[pos
] = NULL
;
354 if (view_exec(view
, 0))
359 diff_restore_line(view
, state
);
361 if (!state
->adding_describe_ref
&& !ref_list_contains_tag(view
->vid
)) {
362 const char *describe_argv
[] = { "git", "describe", view
->vid
, NULL
};
364 if (!begin_update(view
, NULL
, describe_argv
, OPEN_EXTRA
)) {
365 report("Failed to load describe data");
369 state
->adding_describe_ref
= true;
376 return diff_common_read(view
, buf
->data
, state
);
380 diff_blame_line(const char *ref
, const char *file
, unsigned long lineno
,
381 struct blame_header
*header
, struct blame_commit
*commit
)
383 char author
[SIZEOF_STR
] = "";
384 char line_arg
[SIZEOF_STR
];
385 const char *blame_argv
[] = {
386 "git", "blame", encoding_arg
, "-p", line_arg
, ref
, "--", file
, NULL
392 if (!string_format(line_arg
, "-L%ld,+1", lineno
))
395 if (!io_run(&io
, IO_RD
, repo
.cdup
, NULL
, blame_argv
))
398 while (io_get(&io
, &buf
, '\n', true)) {
400 if (!parse_blame_header(header
, buf
.data
, 9999999))
404 } else if (parse_blame_info(commit
, author
, buf
.data
)) {
405 ok
= commit
->filename
!= NULL
;
418 diff_get_lineno(struct view
*view
, struct line
*line
)
420 const struct line
*header
, *chunk
;
422 struct chunk_header chunk_header
;
424 /* Verify that we are after a diff header and one of its chunks */
425 header
= find_prev_line_by_type(view
, line
, LINE_DIFF_HEADER
);
426 chunk
= find_prev_line_by_type(view
, line
, LINE_DIFF_CHUNK
);
427 if (!header
|| !chunk
|| chunk
< header
)
431 * In a chunk header, the number after the '+' sign is the number of its
432 * following line, in the new version of the file. We increment this
433 * number for each non-deletion line, until the given line position.
435 if (!parse_chunk_header(&chunk_header
, box_text(chunk
)))
438 lineno
= chunk_header
.new.position
;
440 for (chunk
++; chunk
< line
; chunk
++)
441 if (chunk
->type
!= LINE_DIFF_DEL
&&
442 chunk
->type
!= LINE_DIFF_DEL2
)
449 diff_trace_origin(struct view
*view
, struct line
*line
)
451 struct line
*diff
= find_prev_line_by_type(view
, line
, LINE_DIFF_HEADER
);
452 struct line
*chunk
= find_prev_line_by_type(view
, line
, LINE_DIFF_CHUNK
);
453 const char *chunk_data
;
454 int chunk_marker
= line
->type
== LINE_DIFF_DEL
? '-' : '+';
455 unsigned long lineno
= 0;
456 const char *file
= NULL
;
457 char ref
[SIZEOF_REF
];
458 struct blame_header header
;
459 struct blame_commit commit
;
461 if (!diff
|| !chunk
|| chunk
== line
) {
462 report("The line to trace must be inside a diff chunk");
466 for (; diff
< line
&& !file
; diff
++) {
467 const char *data
= box_text(diff
);
469 if (!prefixcmp(data
, "--- a/")) {
470 file
= data
+ STRING_SIZE("--- a/");
475 if (diff
== line
|| !file
) {
476 report("Failed to read the file name");
480 chunk_data
= box_text(chunk
);
482 if (!parse_chunk_lineno(&lineno
, chunk_data
, chunk_marker
)) {
483 report("Failed to read the line number");
488 report("This is the origin of the line");
492 for (chunk
+= 1; chunk
< line
; chunk
++) {
493 if (chunk
->type
== LINE_DIFF_ADD
) {
494 lineno
+= chunk_marker
== '+';
495 } else if (chunk
->type
== LINE_DIFF_DEL
) {
496 lineno
+= chunk_marker
== '-';
502 if (chunk_marker
== '+')
503 string_copy(ref
, view
->vid
);
505 string_format(ref
, "%s^", view
->vid
);
507 if (string_rev_is_null(ref
)) {
508 string_ncopy(view
->env
->file
, file
, strlen(file
));
509 string_copy(view
->env
->ref
, "");
510 view
->env
->goto_lineno
= lineno
- 1;
513 if (!diff_blame_line(ref
, file
, lineno
, &header
, &commit
)) {
514 report("Failed to read blame data");
518 string_ncopy(view
->env
->file
, commit
.filename
, strlen(commit
.filename
));
519 string_copy(view
->env
->ref
, header
.id
);
520 view
->env
->goto_lineno
= header
.orig_lineno
- 1;
523 return REQ_VIEW_BLAME
;
527 diff_get_pathname(struct view
*view
, struct line
*line
)
529 const struct line
*header
;
530 const char *dst
= NULL
;
531 const char *prefixes
[] = { " b/", "diff --cc ", "diff --combined " };
534 if (opt_diff_noprefix
) {
535 header
= find_prev_line_by_type(view
, line
, LINE_DIFF_ADD_FILE
);
539 dst
= strstr(box_text(header
), "+++ ");
540 return dst
? dst
+ 4 : NULL
;
543 header
= find_prev_line_by_type(view
, line
, LINE_DIFF_HEADER
);
547 for (i
= 0; i
< ARRAY_SIZE(prefixes
) && !dst
; i
++)
548 dst
= strstr(box_text(header
), prefixes
[i
]);
550 return dst
? dst
+ strlen(prefixes
[--i
]) : NULL
;
554 diff_common_edit(struct view
*view
, enum request request
, struct line
*line
)
557 char path
[SIZEOF_STR
];
560 if (line
->type
== LINE_DIFF_STAT
) {
561 file
= view
->env
->file
;
562 lineno
= view
->env
->lineno
;
564 file
= diff_get_pathname(view
, line
);
565 lineno
= diff_get_lineno(view
, line
);
568 if (file
&& string_format(path
, "%s%s", repo
.cdup
, file
) && access(path
, R_OK
)) {
569 report("Failed to open file: %s", file
);
573 open_editor(file
, lineno
);
578 diff_request(struct view
*view
, enum request request
, struct line
*line
)
582 return diff_trace_origin(view
, line
);
585 return diff_common_edit(view
, request
, line
);
588 return diff_common_enter(view
, request
, line
);
591 if (string_rev_is_null(view
->vid
))
598 return pager_request(view
, request
, line
);
603 diff_common_select(struct view
*view
, struct line
*line
, const char *changes_msg
)
605 if (line
->type
== LINE_DIFF_STAT
) {
606 struct line
*header
= diff_find_header_from_stat(view
, line
);
608 const char *file
= diff_get_pathname(view
, header
);
611 string_format(view
->env
->file
, "%s", file
);
612 view
->env
->lineno
= view
->env
->goto_lineno
= 0;
613 view
->env
->blob
[0] = 0;
617 string_format(view
->ref
, "Press '%s' to jump to file diff",
618 get_view_key(view
, REQ_ENTER
));
620 const char *file
= diff_get_pathname(view
, line
);
624 string_format(view
->ref
, "%s to '%s'", changes_msg
, file
);
625 string_format(view
->env
->file
, "%s", file
);
626 view
->env
->lineno
= view
->env
->goto_lineno
= diff_get_lineno(view
, line
);
627 view
->env
->blob
[0] = 0;
629 string_ncopy(view
->ref
, view
->ops
->id
, strlen(view
->ops
->id
));
630 pager_select(view
, line
);
636 diff_select(struct view
*view
, struct line
*line
)
638 diff_common_select(view
, line
, "Changes");
641 static struct view_ops diff_ops
= {
644 VIEW_DIFF_LIKE
| VIEW_ADD_DESCRIBE_REF
| VIEW_ADD_PAGER_REFS
| VIEW_FILE_FILTER
| VIEW_REFRESH
| VIEW_FLEX_WIDTH
,
645 sizeof(struct diff_state
),
653 view_column_bit(LINE_NUMBER
) | view_column_bit(TEXT
),
654 pager_get_column_data
,
659 /* vim: set ts=8 sw=8 noexpandtab: */