Simplify view config checking
[tig.git] / src / diff.c
blobfa9d6130962846473891361daf5b98622a3d6099
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/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",
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 line *
41 diff_common_add_diff_stat(struct view *view, const char *text, size_t offset)
43 const char *start = text + offset;
44 const char *data = start + strspn(start, " ");
45 size_t len = strlen(data);
46 char *pipe = strchr(data, '|');
48 /* Ensure that '|' is present and the file name part contains
49 * non-space characters. */
50 if (!pipe || pipe == data || strcspn(data, " ") == 0)
51 return NULL;
53 /* Detect remaining part of a diff stat line:
55 * added | 40 +++++++++++
56 * remove | 124 --------------------------
57 * updated | 14 +----
58 * rename.from => rename.to | 0
59 * .../truncated file name | 11 ++---
60 * binary add | Bin 0 -> 1234 bytes
61 * binary update | Bin 1234 -> 2345 bytes
62 * unmerged | Unmerged
64 if ((data[len - 1] == '-' || data[len - 1] == '+') ||
65 strstr(pipe, " 0") ||
66 (strstr(pipe, "Bin") && strstr(pipe, "->")) ||
67 strstr(pipe, "Unmerged") ||
68 (data[len - 1] == '0' && (strstr(data, "=>") || !prefixcmp(data, "..."))))
69 return add_line_text(view, text, LINE_DIFF_STAT);
70 return NULL;
73 bool
74 diff_common_read(struct view *view, const char *data, struct diff_state *state)
76 enum line_type type = get_line_type(data);
78 if (!view->lines && type != LINE_COMMIT)
79 state->reading_diff_stat = TRUE;
81 if (state->combined_diff && !state->after_diff && data[0] == ' ' && data[1] != ' ')
82 state->reading_diff_stat = TRUE;
84 if (state->reading_diff_stat) {
85 if (diff_common_add_diff_stat(view, data, 0))
86 return TRUE;
87 state->reading_diff_stat = FALSE;
89 } else if (!strcmp(data, "---")) {
90 state->reading_diff_stat = TRUE;
93 if (!state->after_commit_title && !prefixcmp(data, " ")) {
94 struct line *line = add_line_text(view, data, LINE_DEFAULT);
96 if (line)
97 line->commit_title = 1;
98 state->after_commit_title = TRUE;
99 return line != NULL;
102 if (type == LINE_DIFF_HEADER) {
103 const int len = STRING_SIZE("diff --");
105 state->after_diff = TRUE;
106 if (!strncmp(data + len, "combined ", strlen("combined ")) ||
107 !strncmp(data + len, "cc ", strlen("cc ")))
108 state->combined_diff = TRUE;
110 } else if (type == LINE_PP_MERGE) {
111 state->combined_diff = TRUE;
114 /* ADD2 and DEL2 are only valid in combined diff hunks */
115 if (!state->combined_diff && (type == LINE_DIFF_ADD2 || type == LINE_DIFF_DEL2))
116 type = LINE_DEFAULT;
118 return pager_common_read(view, data, type, NULL);
121 static bool
122 diff_find_stat_entry(struct view *view, struct line *line, enum line_type type)
124 struct line *marker = find_next_line_by_type(view, line, type);
126 return marker &&
127 line == find_prev_line_by_type(view, marker, LINE_DIFF_HEADER);
130 enum request
131 diff_common_enter(struct view *view, enum request request, struct line *line)
133 if (line->type == LINE_DIFF_STAT) {
134 int file_number = 0;
136 while (view_has_line(view, line) && line->type == LINE_DIFF_STAT) {
137 file_number++;
138 line--;
141 for (line = view->line; view_has_line(view, line); line++) {
142 line = find_next_line_by_type(view, line, LINE_DIFF_HEADER);
143 if (!line)
144 break;
146 if (diff_find_stat_entry(view, line, LINE_DIFF_INDEX)
147 || diff_find_stat_entry(view, line, LINE_DIFF_SIMILARITY)) {
148 if (file_number == 1) {
149 break;
151 file_number--;
155 if (!line) {
156 report("Failed to find file diff");
157 return REQ_NONE;
160 select_view_line(view, line - view->line);
161 report_clear();
162 return REQ_NONE;
164 } else {
165 return pager_request(view, request, line);
169 void
170 diff_save_line(struct view *view, struct diff_state *state, enum open_flags flags)
172 if (flags & OPEN_RELOAD) {
173 struct line *line = &view->line[view->pos.lineno];
174 const char *file = view_has_line(view, line) ? diff_get_pathname(view, line) : NULL;
176 if (file) {
177 state->file = get_path(file);
178 state->lineno = diff_get_lineno(view, line);
179 state->pos = view->pos;
184 void
185 diff_restore_line(struct view *view, struct diff_state *state)
187 struct line *line = &view->line[view->lines - 1];
189 if (!state->file)
190 return;
192 while ((line = find_prev_line_by_type(view, line, LINE_DIFF_HEADER))) {
193 const char *file = diff_get_pathname(view, line);
195 if (file && !strcmp(file, state->file))
196 break;
197 line--;
200 state->file = NULL;
202 if (!line)
203 return;
205 while ((line = find_next_line_by_type(view, line, LINE_DIFF_CHUNK))) {
206 unsigned int lineno = diff_get_lineno(view, line);
208 for (line++; view_has_line(view, line) && line->type != LINE_DIFF_CHUNK; line++) {
209 if (lineno == state->lineno) {
210 unsigned long lineno = line - view->line;
211 unsigned long offset = lineno - (state->pos.lineno - state->pos.offset);
213 goto_view_line(view, offset, lineno);
214 redraw_view(view);
215 return;
217 if (line->type != LINE_DIFF_DEL &&
218 line->type != LINE_DIFF_DEL2)
219 lineno++;
224 static bool
225 diff_read(struct view *view, struct buffer *buf)
227 struct diff_state *state = view->private;
229 if (!buf) {
230 /* Fall back to retry if no diff will be shown. */
231 if (view->lines == 0 && opt_file_args) {
232 int pos = argv_size(view->argv)
233 - argv_size(opt_file_args) - 1;
235 if (pos > 0 && !strcmp(view->argv[pos], "--")) {
236 for (; view->argv[pos]; pos++) {
237 free((void *) view->argv[pos]);
238 view->argv[pos] = NULL;
241 if (view->pipe)
242 io_done(view->pipe);
243 if (io_run(&view->io, IO_RD, view->dir, opt_env, view->argv))
244 return FALSE;
248 diff_restore_line(view, state);
250 return TRUE;
253 return diff_common_read(view, buf->data, state);
256 static bool
257 diff_blame_line(const char *ref, const char *file, unsigned long lineno,
258 struct blame_header *header, struct blame_commit *commit)
260 char author[SIZEOF_STR] = "";
261 char line_arg[SIZEOF_STR];
262 const char *blame_argv[] = {
263 "git", "blame", encoding_arg, "-p", line_arg, ref, "--", file, NULL
265 struct io io;
266 bool ok = FALSE;
267 struct buffer buf;
269 if (!string_format(line_arg, "-L%ld,+1", lineno))
270 return FALSE;
272 if (!io_run(&io, IO_RD, repo.cdup, opt_env, blame_argv))
273 return FALSE;
275 while (io_get(&io, &buf, '\n', TRUE)) {
276 if (header) {
277 if (!parse_blame_header(header, buf.data, 9999999))
278 break;
279 header = NULL;
281 } else if (parse_blame_info(commit, author, buf.data)) {
282 ok = commit->filename != NULL;
283 break;
287 if (io_error(&io))
288 ok = FALSE;
290 io_done(&io);
291 return ok;
294 unsigned int
295 diff_get_lineno(struct view *view, struct line *line)
297 const struct line *header, *chunk;
298 unsigned int lineno;
299 struct chunk_header chunk_header;
301 /* Verify that we are after a diff header and one of its chunks */
302 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
303 chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
304 if (!header || !chunk || chunk < header)
305 return 0;
308 * In a chunk header, the number after the '+' sign is the number of its
309 * following line, in the new version of the file. We increment this
310 * number for each non-deletion line, until the given line position.
312 if (!parse_chunk_header(&chunk_header, chunk->data))
313 return 0;
315 lineno = chunk_header.new.position;
317 for (chunk++; chunk < line; chunk++)
318 if (chunk->type != LINE_DIFF_DEL &&
319 chunk->type != LINE_DIFF_DEL2)
320 lineno++;
322 return lineno;
325 static enum request
326 diff_trace_origin(struct view *view, struct line *line)
328 struct line *diff = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
329 struct line *chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
330 const char *chunk_data;
331 int chunk_marker = line->type == LINE_DIFF_DEL ? '-' : '+';
332 unsigned long lineno = 0;
333 const char *file = NULL;
334 char ref[SIZEOF_REF];
335 struct blame_header header;
336 struct blame_commit commit;
338 if (!diff || !chunk || chunk == line) {
339 report("The line to trace must be inside a diff chunk");
340 return REQ_NONE;
343 for (; diff < line && !file; diff++) {
344 const char *data = diff->data;
346 if (!prefixcmp(data, "--- a/")) {
347 file = data + STRING_SIZE("--- a/");
348 break;
352 if (diff == line || !file) {
353 report("Failed to read the file name");
354 return REQ_NONE;
357 chunk_data = chunk->data;
359 if (!parse_chunk_lineno(&lineno, chunk_data, chunk_marker)) {
360 report("Failed to read the line number");
361 return REQ_NONE;
364 if (lineno == 0) {
365 report("This is the origin of the line");
366 return REQ_NONE;
369 for (chunk += 1; chunk < line; chunk++) {
370 if (chunk->type == LINE_DIFF_ADD) {
371 lineno += chunk_marker == '+';
372 } else if (chunk->type == LINE_DIFF_DEL) {
373 lineno += chunk_marker == '-';
374 } else {
375 lineno++;
379 if (chunk_marker == '+')
380 string_copy(ref, view->vid);
381 else
382 string_format(ref, "%s^", view->vid);
384 if (string_rev_is_null(ref)) {
385 string_ncopy(view->env->file, file, strlen(file));
386 string_copy(view->env->ref, "");
387 view->env->lineno = lineno - 1;
389 } else {
390 if (!diff_blame_line(ref, file, lineno, &header, &commit)) {
391 report("Failed to read blame data");
392 return REQ_NONE;
395 string_ncopy(view->env->file, commit.filename, strlen(commit.filename));
396 string_copy(view->env->ref, header.id);
397 view->env->lineno = header.orig_lineno - 1;
400 return REQ_VIEW_BLAME;
403 const char *
404 diff_get_pathname(struct view *view, struct line *line)
406 const struct line *header;
407 const char *dst = NULL;
408 const char *prefixes[] = { " b/", "cc ", "combined " };
409 int i;
411 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
412 if (!header)
413 return NULL;
415 for (i = 0; i < ARRAY_SIZE(prefixes) && !dst; i++)
416 dst = strstr(header->data, prefixes[i]);
418 return dst ? dst + strlen(prefixes[--i]) : NULL;
421 enum request
422 diff_common_edit(struct view *view, enum request request, struct line *line)
424 const char *file = diff_get_pathname(view, line);
425 char path[SIZEOF_STR];
426 bool has_path = file && string_format(path, "%s%s", repo.cdup, file);
428 if (has_path && access(path, R_OK)) {
429 report("Failed to open file: %s", file);
430 return REQ_NONE;
433 open_editor(file, diff_get_lineno(view, line));
434 return REQ_NONE;
437 static enum request
438 diff_request(struct view *view, enum request request, struct line *line)
440 switch (request) {
441 case REQ_VIEW_BLAME:
442 return diff_trace_origin(view, line);
444 case REQ_EDIT:
445 return diff_common_edit(view, request, line);
447 case REQ_ENTER:
448 return diff_common_enter(view, request, line);
450 case REQ_REFRESH:
451 if (string_rev_is_null(view->vid))
452 refresh_view(view);
453 else
454 reload_view(view);
455 return REQ_NONE;
457 default:
458 return pager_request(view, request, line);
462 static void
463 diff_select(struct view *view, struct line *line)
465 if (line->type == LINE_DIFF_STAT) {
466 string_format(view->ref, "Press '%s' to jump to file diff",
467 get_view_key(view, REQ_ENTER));
468 } else {
469 const char *file = diff_get_pathname(view, line);
471 if (file) {
472 string_format(view->ref, "Changes to '%s'", file);
473 string_format(view->env->file, "%s", file);
474 view->env->lineno = diff_get_lineno(view, line);
475 view->env->blob[0] = 0;
476 } else {
477 string_ncopy(view->ref, view->ops->id, strlen(view->ops->id));
478 pager_select(view, line);
483 static struct view_ops diff_ops = {
484 "line",
485 argv_env.commit,
486 VIEW_DIFF_LIKE | VIEW_ADD_DESCRIBE_REF | VIEW_ADD_PAGER_REFS | VIEW_FILE_FILTER | VIEW_REFRESH,
487 sizeof(struct diff_state),
488 diff_open,
489 diff_read,
490 view_column_draw,
491 diff_request,
492 view_column_grep,
493 diff_select,
494 NULL,
495 view_column_bit(LINE_NUMBER) | view_column_bit(TEXT),
496 pager_get_column_data,
499 DEFINE_VIEW(diff);
501 /* vim: set ts=8 sw=8 noexpandtab: */