Partly restore old refresh behavior
[tig.git] / src / diff.c
blob42cd35e147bcda143c0626eb90baf4e3b4d0b45d
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 return begin_update(view, NULL, diff_argv, flags);
38 struct line *
39 diff_common_add_diff_stat(struct view *view, const char *text, size_t offset)
41 const char *start = text + offset;
42 const char *data = start + strspn(start, " ");
43 size_t len = strlen(data);
44 char *pipe = strchr(data, '|');
46 /* Ensure that '|' is present and the file name part contains
47 * non-space characters. */
48 if (!pipe || pipe == data || strcspn(data, " ") == 0)
49 return NULL;
51 /* Detect remaining part of a diff stat line:
53 * added | 40 +++++++++++
54 * remove | 124 --------------------------
55 * updated | 14 +----
56 * rename.from => rename.to | 0
57 * .../truncated file name | 11 ++---
58 * binary add | Bin 0 -> 1234 bytes
59 * binary update | Bin 1234 -> 2345 bytes
60 * unmerged | Unmerged
62 if ((data[len - 1] == '-' || data[len - 1] == '+') ||
63 strstr(pipe, " 0") ||
64 (strstr(pipe, "Bin") && strstr(pipe, "->")) ||
65 strstr(pipe, "Unmerged") ||
66 (data[len - 1] == '0' && (strstr(data, "=>") || !prefixcmp(data, "..."))))
67 return add_line_text(view, text, LINE_DIFF_STAT);
68 return NULL;
71 bool
72 diff_common_read(struct view *view, const char *data, struct diff_state *state)
74 enum line_type type = get_line_type(data);
76 if (!view->lines && type != LINE_COMMIT)
77 state->reading_diff_stat = TRUE;
79 if (state->combined_diff && !state->after_diff && data[0] == ' ' && data[1] != ' ')
80 state->reading_diff_stat = TRUE;
82 if (state->reading_diff_stat) {
83 if (diff_common_add_diff_stat(view, data, 0))
84 return TRUE;
85 state->reading_diff_stat = FALSE;
87 } else if (!strcmp(data, "---")) {
88 state->reading_diff_stat = TRUE;
91 if (!state->after_commit_title && !prefixcmp(data, " ")) {
92 struct line *line = add_line_text(view, data, LINE_DEFAULT);
94 if (line)
95 line->commit_title = 1;
96 state->after_commit_title = TRUE;
97 return line != NULL;
100 if (type == LINE_DIFF_HEADER) {
101 const int len = STRING_SIZE("diff --");
103 state->after_diff = TRUE;
104 if (!strncmp(data + len, "combined ", strlen("combined ")) ||
105 !strncmp(data + len, "cc ", strlen("cc ")))
106 state->combined_diff = TRUE;
108 } else if (type == LINE_PP_MERGE) {
109 state->combined_diff = TRUE;
112 /* ADD2 and DEL2 are only valid in combined diff hunks */
113 if (!state->combined_diff && (type == LINE_DIFF_ADD2 || type == LINE_DIFF_DEL2))
114 type = LINE_DEFAULT;
116 return pager_common_read(view, data, type, NULL);
119 static bool
120 diff_find_stat_entry(struct view *view, struct line *line, enum line_type type)
122 struct line *marker = find_next_line_by_type(view, line, type);
124 return marker &&
125 line == find_prev_line_by_type(view, marker, LINE_DIFF_HEADER);
128 enum request
129 diff_common_enter(struct view *view, enum request request, struct line *line)
131 if (line->type == LINE_DIFF_STAT) {
132 int file_number = 0;
134 while (view_has_line(view, line) && line->type == LINE_DIFF_STAT) {
135 file_number++;
136 line--;
139 for (line = view->line; view_has_line(view, line); line++) {
140 line = find_next_line_by_type(view, line, LINE_DIFF_HEADER);
141 if (!line)
142 break;
144 if (diff_find_stat_entry(view, line, LINE_DIFF_INDEX)
145 || diff_find_stat_entry(view, line, LINE_DIFF_SIMILARITY)) {
146 if (file_number == 1) {
147 break;
149 file_number--;
153 if (!line) {
154 report("Failed to find file diff");
155 return REQ_NONE;
158 select_view_line(view, line - view->line);
159 report_clear();
160 return REQ_NONE;
162 } else {
163 return pager_request(view, request, line);
167 static bool
168 diff_read(struct view *view, struct buffer *buf)
170 struct diff_state *state = view->private;
172 if (!buf) {
173 /* Fall back to retry if no diff will be shown. */
174 if (view->lines == 0 && opt_file_argv) {
175 int pos = argv_size(view->argv)
176 - argv_size(opt_file_argv) - 1;
178 if (pos > 0 && !strcmp(view->argv[pos], "--")) {
179 for (; view->argv[pos]; pos++) {
180 free((void *) view->argv[pos]);
181 view->argv[pos] = NULL;
184 if (view->pipe)
185 io_done(view->pipe);
186 if (io_run(&view->io, IO_RD, view->dir, opt_env, view->argv))
187 return FALSE;
190 return TRUE;
193 return diff_common_read(view, buf->data, state);
196 static bool
197 diff_blame_line(const char *ref, const char *file, unsigned long lineno,
198 struct blame_header *header, struct blame_commit *commit)
200 char author[SIZEOF_STR] = "";
201 char line_arg[SIZEOF_STR];
202 const char *blame_argv[] = {
203 "git", "blame", encoding_arg, "-p", line_arg, ref, "--", file, NULL
205 struct io io;
206 bool ok = FALSE;
207 struct buffer buf;
209 if (!string_format(line_arg, "-L%ld,+1", lineno))
210 return FALSE;
212 if (!io_run(&io, IO_RD, repo.cdup, opt_env, blame_argv))
213 return FALSE;
215 while (io_get(&io, &buf, '\n', TRUE)) {
216 if (header) {
217 if (!parse_blame_header(header, buf.data, 9999999))
218 break;
219 header = NULL;
221 } else if (parse_blame_info(commit, author, buf.data)) {
222 ok = commit->filename != NULL;
223 break;
227 if (io_error(&io))
228 ok = FALSE;
230 io_done(&io);
231 return ok;
234 unsigned int
235 diff_get_lineno(struct view *view, struct line *line)
237 const struct line *header, *chunk;
238 unsigned int lineno;
239 struct chunk_header chunk_header;
241 /* Verify that we are after a diff header and one of its chunks */
242 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
243 chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
244 if (!header || !chunk || chunk < header)
245 return 0;
248 * In a chunk header, the number after the '+' sign is the number of its
249 * following line, in the new version of the file. We increment this
250 * number for each non-deletion line, until the given line position.
252 if (!parse_chunk_header(&chunk_header, chunk->data))
253 return 0;
255 lineno = chunk_header.new.position;
257 for (chunk++; chunk < line; chunk++)
258 if (chunk->type != LINE_DIFF_DEL &&
259 chunk->type != LINE_DIFF_DEL2)
260 lineno++;
262 return lineno;
265 static enum request
266 diff_trace_origin(struct view *view, struct line *line)
268 struct line *diff = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
269 struct line *chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
270 const char *chunk_data;
271 int chunk_marker = line->type == LINE_DIFF_DEL ? '-' : '+';
272 unsigned long lineno = 0;
273 const char *file = NULL;
274 char ref[SIZEOF_REF];
275 struct blame_header header;
276 struct blame_commit commit;
278 if (!diff || !chunk || chunk == line) {
279 report("The line to trace must be inside a diff chunk");
280 return REQ_NONE;
283 for (; diff < line && !file; diff++) {
284 const char *data = diff->data;
286 if (!prefixcmp(data, "--- a/")) {
287 file = data + STRING_SIZE("--- a/");
288 break;
292 if (diff == line || !file) {
293 report("Failed to read the file name");
294 return REQ_NONE;
297 chunk_data = chunk->data;
299 if (!parse_chunk_lineno(&lineno, chunk_data, chunk_marker)) {
300 report("Failed to read the line number");
301 return REQ_NONE;
304 if (lineno == 0) {
305 report("This is the origin of the line");
306 return REQ_NONE;
309 for (chunk += 1; chunk < line; chunk++) {
310 if (chunk->type == LINE_DIFF_ADD) {
311 lineno += chunk_marker == '+';
312 } else if (chunk->type == LINE_DIFF_DEL) {
313 lineno += chunk_marker == '-';
314 } else {
315 lineno++;
319 if (chunk_marker == '+')
320 string_copy(ref, view->vid);
321 else
322 string_format(ref, "%s^", view->vid);
324 if (string_rev_is_null(ref)) {
325 string_ncopy(view->env->file, file, strlen(file));
326 string_copy(view->env->ref, "");
327 view->env->lineno = lineno - 1;
329 } else {
330 if (!diff_blame_line(ref, file, lineno, &header, &commit)) {
331 report("Failed to read blame data");
332 return REQ_NONE;
335 string_ncopy(view->env->file, commit.filename, strlen(commit.filename));
336 string_copy(view->env->ref, header.id);
337 view->env->lineno = header.orig_lineno - 1;
340 return REQ_VIEW_BLAME;
343 const char *
344 diff_get_pathname(struct view *view, struct line *line)
346 const struct line *header;
347 const char *dst = NULL;
348 const char *prefixes[] = { " b/", "cc ", "combined " };
349 int i;
351 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
352 if (!header)
353 return NULL;
355 for (i = 0; i < ARRAY_SIZE(prefixes) && !dst; i++)
356 dst = strstr(header->data, prefixes[i]);
358 return dst ? dst + strlen(prefixes[--i]) : NULL;
361 enum request
362 diff_common_edit(struct view *view, enum request request, struct line *line)
364 const char *file = diff_get_pathname(view, line);
365 char path[SIZEOF_STR];
366 bool has_path = file && string_format(path, "%s%s", repo.cdup, file);
368 if (has_path && access(path, R_OK)) {
369 report("Failed to open file: %s", file);
370 return REQ_NONE;
373 open_editor(file, diff_get_lineno(view, line));
374 return REQ_NONE;
377 static enum request
378 diff_request(struct view *view, enum request request, struct line *line)
380 switch (request) {
381 case REQ_VIEW_BLAME:
382 return diff_trace_origin(view, line);
384 case REQ_EDIT:
385 return diff_common_edit(view, request, line);
387 case REQ_ENTER:
388 return diff_common_enter(view, request, line);
390 case REQ_REFRESH:
391 if (string_rev_is_null(view->vid))
392 refresh_view(view);
393 else
394 reload_view(view);
395 return REQ_NONE;
397 default:
398 return pager_request(view, request, line);
402 static void
403 diff_select(struct view *view, struct line *line)
405 if (line->type == LINE_DIFF_STAT) {
406 string_format(view->ref, "Press '%s' to jump to file diff",
407 get_view_key(view, REQ_ENTER));
408 } else {
409 const char *file = diff_get_pathname(view, line);
411 if (file) {
412 string_format(view->ref, "Changes to '%s'", file);
413 string_format(view->env->file, "%s", file);
414 view->env->blob[0] = 0;
415 } else {
416 string_ncopy(view->ref, view->ops->id, strlen(view->ops->id));
417 pager_select(view, line);
422 static struct view_ops diff_ops = {
423 "line",
424 argv_env.commit,
425 VIEW_DIFF_LIKE | VIEW_ADD_DESCRIBE_REF | VIEW_ADD_PAGER_REFS | VIEW_FILE_FILTER | VIEW_REFRESH,
426 sizeof(struct diff_state),
427 diff_open,
428 diff_read,
429 view_column_draw,
430 diff_request,
431 view_column_grep,
432 diff_select,
433 NULL,
434 view_column_bit(LINE_NUMBER) | view_column_bit(TEXT),
435 pager_get_column_data,
438 DEFINE_VIEW(diff);
440 /* vim: set ts=8 sw=8 noexpandtab: */