Read git diff.context option
[tig.git] / src / diff.c
blobdc78a7451d218e9791a5814cb547a2f4d4eeee95
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 *data = text + offset;
42 size_t len = strlen(data);
43 char *pipe = strchr(data, '|');
44 bool has_histogram = data[len - 1] == '-' || data[len - 1] == '+';
45 bool has_bin_diff = pipe && strstr(pipe, "Bin") && strstr(pipe, "->");
46 bool has_rename = data[len - 1] == '0' && (strstr(data, "=>") || !strncmp(data, " ...", 4));
47 bool has_no_change = pipe && strstr(pipe, " 0");
49 if (pipe && (has_histogram || has_bin_diff || has_rename || has_no_change))
50 return add_line_text(view, text, LINE_DIFF_STAT);
51 return NULL;
54 bool
55 diff_common_read(struct view *view, const char *data, struct diff_state *state)
57 enum line_type type = get_line_type(data);
59 if (!view->lines && type != LINE_COMMIT)
60 state->reading_diff_stat = TRUE;
62 if (state->combined_diff && !state->after_diff && data[0] == ' ' && data[1] != ' ')
63 state->reading_diff_stat = TRUE;
65 if (state->reading_diff_stat) {
66 if (diff_common_add_diff_stat(view, data, 0))
67 return TRUE;
68 state->reading_diff_stat = FALSE;
70 } else if (!strcmp(data, "---")) {
71 state->reading_diff_stat = TRUE;
74 if (!state->after_commit_title && !prefixcmp(data, " ")) {
75 struct line *line = add_line_text(view, data, LINE_DEFAULT);
77 if (line)
78 line->commit_title = 1;
79 state->after_commit_title = TRUE;
80 return line != NULL;
83 if (type == LINE_DIFF_HEADER) {
84 const int len = STRING_SIZE("diff --");
86 state->after_diff = TRUE;
87 if (!strncmp(data + len, "combined ", strlen("combined ")) ||
88 !strncmp(data + len, "cc ", strlen("cc ")))
89 state->combined_diff = TRUE;
91 } else if (type == LINE_PP_MERGE) {
92 state->combined_diff = TRUE;
95 /* ADD2 and DEL2 are only valid in combined diff hunks */
96 if (!state->combined_diff && (type == LINE_DIFF_ADD2 || type == LINE_DIFF_DEL2))
97 type = LINE_DEFAULT;
99 return pager_common_read(view, data, type, NULL);
102 static bool
103 diff_find_stat_entry(struct view *view, struct line *line, enum line_type type)
105 struct line *marker = find_next_line_by_type(view, line, type);
107 return marker &&
108 line == find_prev_line_by_type(view, marker, LINE_DIFF_HEADER);
111 enum request
112 diff_common_enter(struct view *view, enum request request, struct line *line)
114 if (line->type == LINE_DIFF_STAT) {
115 int file_number = 0;
117 while (view_has_line(view, line) && line->type == LINE_DIFF_STAT) {
118 file_number++;
119 line--;
122 for (line = view->line; view_has_line(view, line); line++) {
123 line = find_next_line_by_type(view, line, LINE_DIFF_HEADER);
124 if (!line)
125 break;
127 if (diff_find_stat_entry(view, line, LINE_DIFF_INDEX)
128 || diff_find_stat_entry(view, line, LINE_DIFF_SIMILARITY)) {
129 if (file_number == 1) {
130 break;
132 file_number--;
136 if (!line) {
137 report("Failed to find file diff");
138 return REQ_NONE;
141 select_view_line(view, line - view->line);
142 report_clear();
143 return REQ_NONE;
145 } else {
146 return pager_request(view, request, line);
150 static bool
151 diff_read(struct view *view, char *data)
153 struct diff_state *state = view->private;
155 if (!data) {
156 /* Fall back to retry if no diff will be shown. */
157 if (view->lines == 0 && opt_file_argv) {
158 int pos = argv_size(view->argv)
159 - argv_size(opt_file_argv) - 1;
161 if (pos > 0 && !strcmp(view->argv[pos], "--")) {
162 for (; view->argv[pos]; pos++) {
163 free((void *) view->argv[pos]);
164 view->argv[pos] = NULL;
167 if (view->pipe)
168 io_done(view->pipe);
169 if (io_run(&view->io, IO_RD, view->dir, opt_env, view->argv))
170 return FALSE;
173 return TRUE;
176 return diff_common_read(view, data, state);
179 static bool
180 diff_blame_line(const char *ref, const char *file, unsigned long lineno,
181 struct blame_header *header, struct blame_commit *commit)
183 char author[SIZEOF_STR] = "";
184 char line_arg[SIZEOF_STR];
185 const char *blame_argv[] = {
186 "git", "blame", encoding_arg, "-p", line_arg, ref, "--", file, NULL
188 struct io io;
189 bool ok = FALSE;
190 char *buf;
192 if (!string_format(line_arg, "-L%ld,+1", lineno))
193 return FALSE;
195 if (!io_run(&io, IO_RD, repo.cdup, opt_env, blame_argv))
196 return FALSE;
198 while ((buf = io_get(&io, '\n', TRUE))) {
199 if (header) {
200 if (!parse_blame_header(header, buf, 9999999))
201 break;
202 header = NULL;
204 } else if (parse_blame_info(commit, author, buf)) {
205 ok = commit->filename != NULL;
206 break;
210 if (io_error(&io))
211 ok = FALSE;
213 io_done(&io);
214 return ok;
217 unsigned int
218 diff_get_lineno(struct view *view, struct line *line)
220 const struct line *header, *chunk;
221 unsigned int lineno;
222 struct chunk_header chunk_header;
224 /* Verify that we are after a diff header and one of its chunks */
225 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
226 chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
227 if (!header || !chunk || chunk < header)
228 return 0;
231 * In a chunk header, the number after the '+' sign is the number of its
232 * following line, in the new version of the file. We increment this
233 * number for each non-deletion line, until the given line position.
235 if (!parse_chunk_header(&chunk_header, chunk->data))
236 return 0;
238 lineno = chunk_header.new.position;
240 for (chunk++; chunk < line; chunk++)
241 if (chunk->type != LINE_DIFF_DEL &&
242 chunk->type != LINE_DIFF_DEL2)
243 lineno++;
245 return lineno;
248 static enum request
249 diff_trace_origin(struct view *view, struct line *line)
251 struct line *diff = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
252 struct line *chunk = find_prev_line_by_type(view, line, LINE_DIFF_CHUNK);
253 const char *chunk_data;
254 int chunk_marker = line->type == LINE_DIFF_DEL ? '-' : '+';
255 unsigned long lineno = 0;
256 const char *file = NULL;
257 char ref[SIZEOF_REF];
258 struct blame_header header;
259 struct blame_commit commit;
261 if (!diff || !chunk || chunk == line) {
262 report("The line to trace must be inside a diff chunk");
263 return REQ_NONE;
266 for (; diff < line && !file; diff++) {
267 const char *data = diff->data;
269 if (!prefixcmp(data, "--- a/")) {
270 file = data + STRING_SIZE("--- a/");
271 break;
275 if (diff == line || !file) {
276 report("Failed to read the file name");
277 return REQ_NONE;
280 chunk_data = chunk->data;
282 if (!parse_chunk_lineno(&lineno, chunk_data, chunk_marker)) {
283 report("Failed to read the line number");
284 return REQ_NONE;
287 if (lineno == 0) {
288 report("This is the origin of the line");
289 return REQ_NONE;
292 for (chunk += 1; chunk < line; chunk++) {
293 if (chunk->type == LINE_DIFF_ADD) {
294 lineno += chunk_marker == '+';
295 } else if (chunk->type == LINE_DIFF_DEL) {
296 lineno += chunk_marker == '-';
297 } else {
298 lineno++;
302 if (chunk_marker == '+')
303 string_copy(ref, view->vid);
304 else
305 string_format(ref, "%s^", view->vid);
307 if (string_rev_is_null(ref)) {
308 string_ncopy(view->env->file, file, strlen(file));
309 string_copy(view->env->ref, "");
310 view->env->lineno = lineno - 1;
312 } else {
313 if (!diff_blame_line(ref, file, lineno, &header, &commit)) {
314 report("Failed to read blame data");
315 return REQ_NONE;
318 string_ncopy(view->env->file, commit.filename, strlen(commit.filename));
319 string_copy(view->env->ref, header.id);
320 view->env->lineno = header.orig_lineno - 1;
323 return REQ_VIEW_BLAME;
326 const char *
327 diff_get_pathname(struct view *view, struct line *line)
329 const struct line *header;
330 const char *dst = NULL;
331 const char *prefixes[] = { " b/", "cc ", "combined " };
332 int i;
334 header = find_prev_line_by_type(view, line, LINE_DIFF_HEADER);
335 if (!header)
336 return NULL;
338 for (i = 0; i < ARRAY_SIZE(prefixes) && !dst; i++)
339 dst = strstr(header->data, prefixes[i]);
341 return dst ? dst + strlen(prefixes[--i]) : NULL;
344 enum request
345 diff_common_edit(struct view *view, enum request request, struct line *line)
347 const char *file = diff_get_pathname(view, line);
348 char path[SIZEOF_STR];
349 bool has_path = file && string_format(path, "%s%s", repo.cdup, file);
351 if (has_path && access(path, R_OK)) {
352 report("Failed to open file: %s", file);
353 return REQ_NONE;
356 open_editor(file, diff_get_lineno(view, line));
357 return REQ_NONE;
360 static enum request
361 diff_request(struct view *view, enum request request, struct line *line)
363 switch (request) {
364 case REQ_VIEW_BLAME:
365 return diff_trace_origin(view, line);
367 case REQ_EDIT:
368 return diff_common_edit(view, request, line);
370 case REQ_ENTER:
371 return diff_common_enter(view, request, line);
373 case REQ_REFRESH:
374 if (string_rev_is_null(view->vid))
375 refresh_view(view);
376 else
377 reload_view(view);
378 return REQ_NONE;
380 default:
381 return pager_request(view, request, line);
385 static void
386 diff_select(struct view *view, struct line *line)
388 if (line->type == LINE_DIFF_STAT) {
389 string_format(view->ref, "Press '%s' to jump to file diff",
390 get_view_key(view, REQ_ENTER));
391 } else {
392 const char *file = diff_get_pathname(view, line);
394 if (file) {
395 string_format(view->ref, "Changes to '%s'", file);
396 string_format(view->env->file, "%s", file);
397 view->env->blob[0] = 0;
398 } else {
399 string_ncopy(view->ref, view->ops->id, strlen(view->ops->id));
400 pager_select(view, line);
405 static struct view_ops diff_ops = {
406 "line",
407 argv_env.commit,
408 VIEW_DIFF_LIKE | VIEW_ADD_DESCRIBE_REF | VIEW_ADD_PAGER_REFS | VIEW_FILE_FILTER | VIEW_REFRESH,
409 sizeof(struct diff_state),
410 diff_open,
411 diff_read,
412 view_column_draw,
413 diff_request,
414 view_column_grep,
415 diff_select,
416 NULL,
417 view_column_bit(LINE_NUMBER) | view_column_bit(TEXT),
418 pager_get_column_data,
421 DEFINE_VIEW(diff);
423 /* vim: set ts=8 sw=8 noexpandtab: */