Merge pull request #457 from vivien/text-variable
[tig.git] / src / diff.c
blob591a33a7ae586a98c869f2f529d140befb3e3146
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.
14 #include "tig/argv.h"
15 #include "tig/refdb.h"
16 #include "tig/repo.h"
17 #include "tig/options.h"
18 #include "tig/display.h"
19 #include "tig/parse.h"
20 #include "tig/pager.h"
21 #include "tig/diff.h"
22 #include "tig/draw.h"
24 static bool
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 {
41 const char *text;
42 enum line_type type;
43 size_t cells;
44 struct box_cell cell[10];
47 static void
48 diff_common_add_cell(struct diff_stat_context *context, size_t length)
50 assert(ARRAY_SIZE(context->cell) > context->cells);
51 if (length == 0)
52 return;
53 context->cell[context->cells].length = length;
54 context->cell[context->cells].type = context->type;
55 context->cells++;
58 static bool
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);
63 if (sep == NULL)
64 return false;
66 diff_common_add_cell(context, sep - context->text);
67 context->text = sep;
68 context->type = next_type;
70 return true;
73 static struct line *
74 diff_common_read_diff_stat(struct view *view, const char *text)
76 struct diff_stat_context context = { text, LINE_DIFF_STAT };
77 struct line *line;
78 struct box *box;
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);
88 } else {
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);
95 if (!line)
96 return NULL;
98 box = line->data;
99 if (context.cells)
100 memcpy(box->cell, context.cell, sizeof(struct box_cell) * context.cells);
101 box->cells = context.cells;
102 return line;
105 struct line *
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)
116 return NULL;
118 /* Detect remaining part of a diff stat line:
120 * added | 40 +++++++++++
121 * remove | 124 --------------------------
122 * updated | 14 +----
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);
135 return NULL;
138 bool
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))
151 return true;
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);
161 if (line)
162 line->commit_title = 1;
163 state->after_commit_title = true;
164 return line != NULL;
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("@@"), "@@");
177 struct line *line =
178 context ? add_line_text_at(view, view->lines, data, LINE_DIFF_CHUNK, 2)
179 : NULL;
180 struct box *box;
182 if (!line)
183 return false;
185 box = line->data;
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;
189 return true;
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))
197 type = LINE_DEFAULT;
199 return pager_common_read(view, data, type, NULL);
202 static bool
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);
207 return marker &&
208 line == find_prev_line_by_type(view, marker, LINE_DIFF_HEADER);
211 static struct line *
212 diff_find_header_from_stat(struct view *view, struct line *line)
214 if (line->type == LINE_DIFF_STAT) {
215 int file_number = 0;
217 while (view_has_line(view, line) && line->type == LINE_DIFF_STAT) {
218 file_number++;
219 line--;
222 for (line = view->line; view_has_line(view, line); line++) {
223 line = find_next_line_by_type(view, line, LINE_DIFF_HEADER);
224 if (!line)
225 break;
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) {
230 break;
232 file_number--;
236 return line;
239 return NULL;
242 enum request
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);
247 if (!line) {
248 report("Failed to find file diff");
249 return REQ_NONE;
252 select_view_line(view, line - view->line);
253 report_clear();
254 return REQ_NONE;
256 } else {
257 return pager_request(view, request, line);
261 void
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;
268 if (file) {
269 state->file = get_path(file);
270 state->lineno = diff_get_lineno(view, line);
271 state->pos = view->pos;
276 void
277 diff_restore_line(struct view *view, struct diff_state *state)
279 struct line *line = &view->line[view->lines - 1];
281 if (!state->file)
282 return;
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))
288 break;
289 line--;
292 state->file = NULL;
294 if (!line)
295 return;
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);
306 redraw_view(view);
307 return;
309 if (line->type != LINE_DIFF_DEL &&
310 line->type != LINE_DIFF_DEL2)
311 lineno++;
316 static bool
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))
326 return false;
329 return true;
332 static bool
333 diff_read(struct view *view, struct buffer *buf)
335 struct diff_state *state = view->private;
337 if (state->adding_describe_ref)
338 return diff_read_describe(view, buf, state);
340 if (!buf) {
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;
352 if (view->pipe)
353 io_done(view->pipe);
354 if (view_exec(view, 0))
355 return false;
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");
366 return true;
369 state->adding_describe_ref = true;
370 return false;
373 return true;
376 return diff_common_read(view, buf->data, state);
379 static bool
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
388 struct io io;
389 bool ok = false;
390 struct buffer buf;
392 if (!string_format(line_arg, "-L%ld,+1", lineno))
393 return false;
395 if (!io_run(&io, IO_RD, repo.cdup, NULL, blame_argv))
396 return false;
398 while (io_get(&io, &buf, '\n', true)) {
399 if (header) {
400 if (!parse_blame_header(header, buf.data, 9999999))
401 break;
402 header = NULL;
404 } else if (parse_blame_info(commit, author, buf.data)) {
405 ok = commit->filename != NULL;
406 break;
410 if (io_error(&io))
411 ok = false;
413 io_done(&io);
414 return ok;
417 unsigned int
418 diff_get_lineno(struct view *view, struct line *line)
420 const struct line *header, *chunk;
421 unsigned int lineno;
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)
428 return 0;
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)))
436 return 0;
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)
443 lineno++;
445 return lineno;
448 static enum request
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");
463 return REQ_NONE;
466 for (; diff < line && !file; diff++) {
467 const char *data = box_text(diff);
469 if (!prefixcmp(data, "--- a/")) {
470 file = data + STRING_SIZE("--- a/");
471 break;
475 if (diff == line || !file) {
476 report("Failed to read the file name");
477 return REQ_NONE;
480 chunk_data = box_text(chunk);
482 if (!parse_chunk_lineno(&lineno, chunk_data, chunk_marker)) {
483 report("Failed to read the line number");
484 return REQ_NONE;
487 if (lineno == 0) {
488 report("This is the origin of the line");
489 return REQ_NONE;
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 == '-';
497 } else {
498 lineno++;
502 if (chunk_marker == '+')
503 string_copy(ref, view->vid);
504 else
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;
512 } else {
513 if (!diff_blame_line(ref, file, lineno, &header, &commit)) {
514 report("Failed to read blame data");
515 return REQ_NONE;
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;
526 const char *
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/", "cc ", "combined " };
532 int i;
534 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
535 if (!header)
536 return NULL;
538 for (i = 0; i < ARRAY_SIZE(prefixes) && !dst; i++)
539 dst = strstr(box_text(header), prefixes[i]);
541 return dst ? dst + strlen(prefixes[--i]) : NULL;
544 enum request
545 diff_common_edit(struct view *view, enum request request, struct line *line)
547 const char *file;
548 char path[SIZEOF_STR];
549 unsigned int lineno;
551 if (line->type == LINE_DIFF_STAT) {
552 file = view->env->file;
553 lineno = view->env->lineno;
554 } else {
555 file = diff_get_pathname(view, line);
556 lineno = diff_get_lineno(view, line);
559 if (file && string_format(path, "%s%s", repo.cdup, file) && access(path, R_OK)) {
560 report("Failed to open file: %s", file);
561 return REQ_NONE;
564 open_editor(file, lineno);
565 return REQ_NONE;
568 static enum request
569 diff_request(struct view *view, enum request request, struct line *line)
571 switch (request) {
572 case REQ_VIEW_BLAME:
573 return diff_trace_origin(view, line);
575 case REQ_EDIT:
576 return diff_common_edit(view, request, line);
578 case REQ_ENTER:
579 return diff_common_enter(view, request, line);
581 case REQ_REFRESH:
582 if (string_rev_is_null(view->vid))
583 refresh_view(view);
584 else
585 reload_view(view);
586 return REQ_NONE;
588 default:
589 return pager_request(view, request, line);
593 void
594 diff_common_select(struct view *view, struct line *line, const char *changes_msg)
596 if (line->type == LINE_DIFF_STAT) {
597 struct line *header = diff_find_header_from_stat(view, line);
598 if (header) {
599 const char *file = diff_get_pathname(view, header);
601 if (file) {
602 string_format(view->env->file, "%s", file);
603 view->env->lineno = view->env->goto_lineno = 0;
604 view->env->blob[0] = 0;
608 string_format(view->ref, "Press '%s' to jump to file diff",
609 get_view_key(view, REQ_ENTER));
610 } else {
611 const char *file = diff_get_pathname(view, line);
613 if (file) {
614 if (changes_msg)
615 string_format(view->ref, "%s to '%s'", changes_msg, file);
616 string_format(view->env->file, "%s", file);
617 view->env->lineno = view->env->goto_lineno = diff_get_lineno(view, line);
618 view->env->blob[0] = 0;
619 } else {
620 string_ncopy(view->ref, view->ops->id, strlen(view->ops->id));
621 pager_select(view, line);
626 static void
627 diff_select(struct view *view, struct line *line)
629 diff_common_select(view, line, "Changes");
632 static struct view_ops diff_ops = {
633 "line",
634 argv_env.commit,
635 VIEW_DIFF_LIKE | VIEW_ADD_DESCRIBE_REF | VIEW_ADD_PAGER_REFS | VIEW_FILE_FILTER | VIEW_REFRESH | VIEW_FLEX_WIDTH,
636 sizeof(struct diff_state),
637 diff_open,
638 diff_read,
639 view_column_draw,
640 diff_request,
641 view_column_grep,
642 diff_select,
643 NULL,
644 view_column_bit(LINE_NUMBER) | view_column_bit(TEXT),
645 pager_get_column_data,
648 DEFINE_VIEW(diff);
650 /* vim: set ts=8 sw=8 noexpandtab: */