]> git.ipfire.org Git - thirdparty/git.git/blob - add-patch.c
built-in add -p: handle deleted empty files
[thirdparty/git.git] / add-patch.c
1 #include "cache.h"
2 #include "add-interactive.h"
3 #include "strbuf.h"
4 #include "run-command.h"
5 #include "argv-array.h"
6 #include "pathspec.h"
7 #include "color.h"
8 #include "diff.h"
9
10 struct hunk_header {
11 unsigned long old_offset, old_count, new_offset, new_count;
12 /*
13 * Start/end offsets to the extra text after the second `@@` in the
14 * hunk header, e.g. the function signature. This is expected to
15 * include the newline.
16 */
17 size_t extra_start, extra_end, colored_extra_start, colored_extra_end;
18 };
19
20 struct hunk {
21 size_t start, end, colored_start, colored_end;
22 enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
23 struct hunk_header header;
24 };
25
26 struct add_p_state {
27 struct add_i_state s;
28 struct strbuf answer, buf;
29
30 /* parsed diff */
31 struct strbuf plain, colored;
32 struct file_diff {
33 struct hunk head;
34 struct hunk *hunk;
35 size_t hunk_nr, hunk_alloc;
36 unsigned deleted:1;
37 } *file_diff;
38 size_t file_diff_nr;
39 };
40
41 static void err(struct add_p_state *s, const char *fmt, ...)
42 {
43 va_list args;
44
45 va_start(args, fmt);
46 fputs(s->s.error_color, stderr);
47 vfprintf(stderr, fmt, args);
48 fputs(s->s.reset_color, stderr);
49 fputc('\n', stderr);
50 va_end(args);
51 }
52
53 static void setup_child_process(struct add_p_state *s,
54 struct child_process *cp, ...)
55 {
56 va_list ap;
57 const char *arg;
58
59 va_start(ap, cp);
60 while ((arg = va_arg(ap, const char *)))
61 argv_array_push(&cp->args, arg);
62 va_end(ap);
63
64 cp->git_cmd = 1;
65 argv_array_pushf(&cp->env_array,
66 INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
67 }
68
69 static int parse_range(const char **p,
70 unsigned long *offset, unsigned long *count)
71 {
72 char *pend;
73
74 *offset = strtoul(*p, &pend, 10);
75 if (pend == *p)
76 return -1;
77 if (*pend != ',') {
78 *count = 1;
79 *p = pend;
80 return 0;
81 }
82 *count = strtoul(pend + 1, (char **)p, 10);
83 return *p == pend + 1 ? -1 : 0;
84 }
85
86 static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk)
87 {
88 struct hunk_header *header = &hunk->header;
89 const char *line = s->plain.buf + hunk->start, *p = line;
90 char *eol = memchr(p, '\n', s->plain.len - hunk->start);
91
92 if (!eol)
93 eol = s->plain.buf + s->plain.len;
94
95 if (!skip_prefix(p, "@@ -", &p) ||
96 parse_range(&p, &header->old_offset, &header->old_count) < 0 ||
97 !skip_prefix(p, " +", &p) ||
98 parse_range(&p, &header->new_offset, &header->new_count) < 0 ||
99 !skip_prefix(p, " @@", &p))
100 return error(_("could not parse hunk header '%.*s'"),
101 (int)(eol - line), line);
102
103 hunk->start = eol - s->plain.buf + (*eol == '\n');
104 header->extra_start = p - s->plain.buf;
105 header->extra_end = hunk->start;
106
107 if (!s->colored.len) {
108 header->colored_extra_start = header->colored_extra_end = 0;
109 return 0;
110 }
111
112 /* Now find the extra text in the colored diff */
113 line = s->colored.buf + hunk->colored_start;
114 eol = memchr(line, '\n', s->colored.len - hunk->colored_start);
115 if (!eol)
116 eol = s->colored.buf + s->colored.len;
117 p = memmem(line, eol - line, "@@ -", 4);
118 if (!p)
119 return error(_("could not parse colored hunk header '%.*s'"),
120 (int)(eol - line), line);
121 p = memmem(p + 4, eol - p - 4, " @@", 3);
122 if (!p)
123 return error(_("could not parse colored hunk header '%.*s'"),
124 (int)(eol - line), line);
125 hunk->colored_start = eol - s->colored.buf + (*eol == '\n');
126 header->colored_extra_start = p + 3 - s->colored.buf;
127 header->colored_extra_end = hunk->colored_start;
128
129 return 0;
130 }
131
132 static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
133 {
134 struct argv_array args = ARGV_ARRAY_INIT;
135 struct strbuf *plain = &s->plain, *colored = NULL;
136 struct child_process cp = CHILD_PROCESS_INIT;
137 char *p, *pend, *colored_p = NULL, *colored_pend = NULL;
138 size_t file_diff_alloc = 0, i, color_arg_index;
139 struct file_diff *file_diff = NULL;
140 struct hunk *hunk = NULL;
141 int res;
142
143 /* Use `--no-color` explicitly, just in case `diff.color = always`. */
144 argv_array_pushl(&args, "diff-files", "-p", "--no-color", "--", NULL);
145 color_arg_index = args.argc - 2;
146 for (i = 0; i < ps->nr; i++)
147 argv_array_push(&args, ps->items[i].original);
148
149 setup_child_process(s, &cp, NULL);
150 cp.argv = args.argv;
151 res = capture_command(&cp, plain, 0);
152 if (res) {
153 argv_array_clear(&args);
154 return error(_("could not parse diff"));
155 }
156 if (!plain->len) {
157 argv_array_clear(&args);
158 return 0;
159 }
160 strbuf_complete_line(plain);
161
162 if (want_color_fd(1, -1)) {
163 struct child_process colored_cp = CHILD_PROCESS_INIT;
164
165 setup_child_process(s, &colored_cp, NULL);
166 xsnprintf((char *)args.argv[color_arg_index], 8, "--color");
167 colored_cp.argv = args.argv;
168 colored = &s->colored;
169 res = capture_command(&colored_cp, colored, 0);
170 argv_array_clear(&args);
171 if (res)
172 return error(_("could not parse colored diff"));
173 strbuf_complete_line(colored);
174 colored_p = colored->buf;
175 colored_pend = colored_p + colored->len;
176 }
177 argv_array_clear(&args);
178
179 /* parse files and hunks */
180 p = plain->buf;
181 pend = p + plain->len;
182 while (p != pend) {
183 char *eol = memchr(p, '\n', pend - p);
184 const char *deleted = NULL;
185
186 if (!eol)
187 eol = pend;
188
189 if (starts_with(p, "diff ")) {
190 s->file_diff_nr++;
191 ALLOC_GROW(s->file_diff, s->file_diff_nr,
192 file_diff_alloc);
193 file_diff = s->file_diff + s->file_diff_nr - 1;
194 memset(file_diff, 0, sizeof(*file_diff));
195 hunk = &file_diff->head;
196 hunk->start = p - plain->buf;
197 if (colored_p)
198 hunk->colored_start = colored_p - colored->buf;
199 } else if (p == plain->buf)
200 BUG("diff starts with unexpected line:\n"
201 "%.*s\n", (int)(eol - p), p);
202 else if (file_diff->deleted)
203 ; /* keep the rest of the file in a single "hunk" */
204 else if (starts_with(p, "@@ ") ||
205 (hunk == &file_diff->head &&
206 skip_prefix(p, "deleted file", &deleted))) {
207 file_diff->hunk_nr++;
208 ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,
209 file_diff->hunk_alloc);
210 hunk = file_diff->hunk + file_diff->hunk_nr - 1;
211 memset(hunk, 0, sizeof(*hunk));
212
213 hunk->start = p - plain->buf;
214 if (colored)
215 hunk->colored_start = colored_p - colored->buf;
216
217 if (deleted)
218 file_diff->deleted = 1;
219 else if (parse_hunk_header(s, hunk) < 0)
220 return -1;
221 }
222
223 p = eol == pend ? pend : eol + 1;
224 hunk->end = p - plain->buf;
225
226 if (colored) {
227 char *colored_eol = memchr(colored_p, '\n',
228 colored_pend - colored_p);
229 if (colored_eol)
230 colored_p = colored_eol + 1;
231 else
232 colored_p = colored_pend;
233
234 hunk->colored_end = colored_p - colored->buf;
235 }
236 }
237
238 return 0;
239 }
240
241 static void render_hunk(struct add_p_state *s, struct hunk *hunk,
242 ssize_t delta, int colored, struct strbuf *out)
243 {
244 struct hunk_header *header = &hunk->header;
245
246 if (hunk->header.old_offset != 0 || hunk->header.new_offset != 0) {
247 /*
248 * Generate the hunk header dynamically, except for special
249 * hunks (such as the diff header).
250 */
251 const char *p;
252 size_t len;
253 unsigned long old_offset = header->old_offset;
254 unsigned long new_offset = header->new_offset;
255
256 if (!colored) {
257 p = s->plain.buf + header->extra_start;
258 len = header->extra_end - header->extra_start;
259 } else {
260 strbuf_addstr(out, s->s.fraginfo_color);
261 p = s->colored.buf + header->colored_extra_start;
262 len = header->colored_extra_end
263 - header->colored_extra_start;
264 }
265
266 new_offset += delta;
267
268 strbuf_addf(out, "@@ -%lu,%lu +%lu,%lu @@",
269 old_offset, header->old_count,
270 new_offset, header->new_count);
271 if (len)
272 strbuf_add(out, p, len);
273 else if (colored)
274 strbuf_addf(out, "%s\n", GIT_COLOR_RESET);
275 else
276 strbuf_addch(out, '\n');
277 }
278
279 if (colored)
280 strbuf_add(out, s->colored.buf + hunk->colored_start,
281 hunk->colored_end - hunk->colored_start);
282 else
283 strbuf_add(out, s->plain.buf + hunk->start,
284 hunk->end - hunk->start);
285 }
286
287 static void reassemble_patch(struct add_p_state *s,
288 struct file_diff *file_diff, struct strbuf *out)
289 {
290 struct hunk *hunk;
291 size_t i;
292 ssize_t delta = 0;
293
294 render_hunk(s, &file_diff->head, 0, 0, out);
295
296 for (i = 0; i < file_diff->hunk_nr; i++) {
297 hunk = file_diff->hunk + i;
298 if (hunk->use != USE_HUNK)
299 delta += hunk->header.old_count
300 - hunk->header.new_count;
301 else
302 render_hunk(s, hunk, delta, 0, out);
303 }
304 }
305
306 static const char help_patch_text[] =
307 N_("y - stage this hunk\n"
308 "n - do not stage this hunk\n"
309 "a - stage this and all the remaining hunks\n"
310 "d - do not stage this hunk nor any of the remaining hunks\n"
311 "j - leave this hunk undecided, see next undecided hunk\n"
312 "J - leave this hunk undecided, see next hunk\n"
313 "k - leave this hunk undecided, see previous undecided hunk\n"
314 "K - leave this hunk undecided, see previous hunk\n"
315 "? - print help\n");
316
317 static int patch_update_file(struct add_p_state *s,
318 struct file_diff *file_diff)
319 {
320 size_t hunk_index = 0;
321 ssize_t i, undecided_previous, undecided_next;
322 struct hunk *hunk;
323 char ch;
324 struct child_process cp = CHILD_PROCESS_INIT;
325 int colored = !!s->colored.len;
326
327 if (!file_diff->hunk_nr)
328 return 0;
329
330 strbuf_reset(&s->buf);
331 render_hunk(s, &file_diff->head, 0, colored, &s->buf);
332 fputs(s->buf.buf, stdout);
333 for (;;) {
334 if (hunk_index >= file_diff->hunk_nr)
335 hunk_index = 0;
336 hunk = file_diff->hunk + hunk_index;
337
338 undecided_previous = -1;
339 for (i = hunk_index - 1; i >= 0; i--)
340 if (file_diff->hunk[i].use == UNDECIDED_HUNK) {
341 undecided_previous = i;
342 break;
343 }
344
345 undecided_next = -1;
346 for (i = hunk_index + 1; i < file_diff->hunk_nr; i++)
347 if (file_diff->hunk[i].use == UNDECIDED_HUNK) {
348 undecided_next = i;
349 break;
350 }
351
352 /* Everything decided? */
353 if (undecided_previous < 0 && undecided_next < 0 &&
354 hunk->use != UNDECIDED_HUNK)
355 break;
356
357 strbuf_reset(&s->buf);
358 render_hunk(s, hunk, 0, colored, &s->buf);
359 fputs(s->buf.buf, stdout);
360
361 strbuf_reset(&s->buf);
362 if (undecided_previous >= 0)
363 strbuf_addstr(&s->buf, ",k");
364 if (hunk_index)
365 strbuf_addstr(&s->buf, ",K");
366 if (undecided_next >= 0)
367 strbuf_addstr(&s->buf, ",j");
368 if (hunk_index + 1 < file_diff->hunk_nr)
369 strbuf_addstr(&s->buf, ",J");
370 color_fprintf(stdout, s->s.prompt_color,
371 "(%"PRIuMAX"/%"PRIuMAX") ",
372 (uintmax_t)hunk_index + 1,
373 (uintmax_t)file_diff->hunk_nr);
374 color_fprintf(stdout, s->s.prompt_color,
375 _("Stage this hunk [y,n,a,d%s,?]? "),
376 s->buf.buf);
377 fflush(stdout);
378 if (strbuf_getline(&s->answer, stdin) == EOF)
379 break;
380 strbuf_trim_trailing_newline(&s->answer);
381
382 if (!s->answer.len)
383 continue;
384 ch = tolower(s->answer.buf[0]);
385 if (ch == 'y') {
386 hunk->use = USE_HUNK;
387 soft_increment:
388 hunk_index = undecided_next < 0 ?
389 file_diff->hunk_nr : undecided_next;
390 } else if (ch == 'n') {
391 hunk->use = SKIP_HUNK;
392 goto soft_increment;
393 } else if (ch == 'a') {
394 for (; hunk_index < file_diff->hunk_nr; hunk_index++) {
395 hunk = file_diff->hunk + hunk_index;
396 if (hunk->use == UNDECIDED_HUNK)
397 hunk->use = USE_HUNK;
398 }
399 } else if (ch == 'd') {
400 for (; hunk_index < file_diff->hunk_nr; hunk_index++) {
401 hunk = file_diff->hunk + hunk_index;
402 if (hunk->use == UNDECIDED_HUNK)
403 hunk->use = SKIP_HUNK;
404 }
405 } else if (s->answer.buf[0] == 'K') {
406 if (hunk_index)
407 hunk_index--;
408 else
409 err(s, _("No previous hunk"));
410 } else if (s->answer.buf[0] == 'J') {
411 if (hunk_index + 1 < file_diff->hunk_nr)
412 hunk_index++;
413 else
414 err(s, _("No next hunk"));
415 } else if (s->answer.buf[0] == 'k') {
416 if (undecided_previous >= 0)
417 hunk_index = undecided_previous;
418 else
419 err(s, _("No previous hunk"));
420 } else if (s->answer.buf[0] == 'j') {
421 if (undecided_next >= 0)
422 hunk_index = undecided_next;
423 else
424 err(s, _("No next hunk"));
425 } else
426 color_fprintf(stdout, s->s.help_color,
427 _(help_patch_text));
428 }
429
430 /* Any hunk to be used? */
431 for (i = 0; i < file_diff->hunk_nr; i++)
432 if (file_diff->hunk[i].use == USE_HUNK)
433 break;
434
435 if (i < file_diff->hunk_nr) {
436 /* At least one hunk selected: apply */
437 strbuf_reset(&s->buf);
438 reassemble_patch(s, file_diff, &s->buf);
439
440 discard_index(s->s.r->index);
441 setup_child_process(s, &cp, "apply", "--cached", NULL);
442 if (pipe_command(&cp, s->buf.buf, s->buf.len,
443 NULL, 0, NULL, 0))
444 error(_("'git apply --cached' failed"));
445 if (!repo_read_index(s->s.r))
446 repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
447 1, NULL, NULL, NULL);
448 }
449
450 putchar('\n');
451 return 0;
452 }
453
454 int run_add_p(struct repository *r, const struct pathspec *ps)
455 {
456 struct add_p_state s = {
457 { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
458 };
459 size_t i;
460
461 init_add_i_state(&s.s, r);
462
463 if (discard_index(r->index) < 0 || repo_read_index(r) < 0 ||
464 repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
465 NULL, NULL, NULL) < 0 ||
466 parse_diff(&s, ps) < 0) {
467 strbuf_release(&s.plain);
468 strbuf_release(&s.colored);
469 return -1;
470 }
471
472 for (i = 0; i < s.file_diff_nr; i++)
473 if (patch_update_file(&s, s.file_diff + i))
474 break;
475
476 strbuf_release(&s.answer);
477 strbuf_release(&s.buf);
478 strbuf_release(&s.plain);
479 strbuf_release(&s.colored);
480 return 0;
481 }