@matheusbsbs / matheus-bagliano / commits / f6aa7ecc34
Fork 0

built-in add -i: start implementing the `patch` functionality in C

In the previous steps, we re-implemented the main loop of `git add -i` in C, and most of the commands. Notably, we left out the actual functionality of `patch`, as the relevant code makes up more than half of `git-add--interactive.perl`, and is actually pretty independent of the rest of the commands. With this commit, we start to tackle that `patch` part. For better separation of concerns, we keep the code in a separate file, `add-patch.c`. The new code is still guarded behind the `add.interactive.useBuiltin` config setting, and for the moment, it can only be called via `git add -p`. The actual functionality follows the original implementation of 5cde71d64aff (git-add --interactive, 2006-12-10), but not too closely (for example, we use string offsets rather than copying strings around, and after seeing whether the `k` and `j` commands are applicable, in the C version we remember which previous/next hunk was undecided, and use it rather than looking again when the user asked to jump). As a further deviation from that commit, We also use a comma instead of a slash to separate the available commands in the prompt, as the current version of the Perl script does this, and we also add a line about the question mark ("print help") to the help text. While it is tempting to use this conversion of `git add -p` as an excuse to work on `apply_all_patches()` so that it does _not_ want to read a file from `stdin` or from a file, but accepts, say, an `strbuf` instead, we will refrain from this particular rabbit hole at this stage. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Dec 13, 2019 at 08:07 UTC f6aa7ecc34373f32621b05659d5d2447d690be18
4 files changed +277 -5
Makefile
+1
@@ -824,6 +824,7 @@ LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat
824
825 LIB_OBJS += abspath.o
826 LIB_OBJS += add-interactive.o
827 +LIB_OBJS += add-patch.o
828 LIB_OBJS += advice.o
829 LIB_OBJS += alias.o
830 LIB_OBJS += alloc.o
add-interactive.h
+1
@@ -4,5 +4,6 @@
4 struct repository;
5 struct pathspec;
6 int run_add_i(struct repository *r, const struct pathspec *ps);
7 +int run_add_p(struct repository *r, const struct pathspec *ps);
8
9 #endif
add-patch.c new
+265
@@ -0,0 +1,265 @@
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 +
8 +struct hunk {
9 + size_t start, end;
10 + enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
11 +};
12 +
13 +struct add_p_state {
14 + struct repository *r;
15 + struct strbuf answer, buf;
16 +
17 + /* parsed diff */
18 + struct strbuf plain;
19 + struct hunk head;
20 + struct hunk *hunk;
21 + size_t hunk_nr, hunk_alloc;
22 +};
23 +
24 +static void setup_child_process(struct add_p_state *s,
25 + struct child_process *cp, ...)
26 +{
27 + va_list ap;
28 + const char *arg;
29 +
30 + va_start(ap, cp);
31 + while ((arg = va_arg(ap, const char *)))
32 + argv_array_push(&cp->args, arg);
33 + va_end(ap);
34 +
35 + cp->git_cmd = 1;
36 + argv_array_pushf(&cp->env_array,
37 + INDEX_ENVIRONMENT "=%s", s->r->index_file);
38 +}
39 +
40 +static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
41 +{
42 + struct strbuf *plain = &s->plain;
43 + struct child_process cp = CHILD_PROCESS_INIT;
44 + char *p, *pend;
45 + size_t i;
46 + struct hunk *hunk = NULL;
47 + int res;
48 +
49 + /* Use `--no-color` explicitly, just in case `diff.color = always`. */
50 + setup_child_process(s, &cp,
51 + "diff-files", "-p", "--no-color", "--", NULL);
52 + for (i = 0; i < ps->nr; i++)
53 + argv_array_push(&cp.args, ps->items[i].original);
54 +
55 + res = capture_command(&cp, plain, 0);
56 + if (res)
57 + return error(_("could not parse diff"));
58 + if (!plain->len)
59 + return 0;
60 + strbuf_complete_line(plain);
61 +
62 + /* parse hunks */
63 + p = plain->buf;
64 + pend = p + plain->len;
65 + while (p != pend) {
66 + char *eol = memchr(p, '\n', pend - p);
67 + if (!eol)
68 + eol = pend;
69 +
70 + if (starts_with(p, "diff ")) {
71 + if (p != plain->buf)
72 + BUG("multi-file diff not yet handled");
73 + hunk = &s->head;
74 + } else if (p == plain->buf)
75 + BUG("diff starts with unexpected line:\n"
76 + "%.*s\n", (int)(eol - p), p);
77 + else if (starts_with(p, "@@ ")) {
78 + s->hunk_nr++;
79 + ALLOC_GROW(s->hunk, s->hunk_nr,
80 + s->hunk_alloc);
81 + hunk = s->hunk + s->hunk_nr - 1;
82 + memset(hunk, 0, sizeof(*hunk));
83 +
84 + hunk->start = p - plain->buf;
85 + }
86 +
87 + p = eol == pend ? pend : eol + 1;
88 + hunk->end = p - plain->buf;
89 + }
90 +
91 + return 0;
92 +}
93 +
94 +static void render_hunk(struct add_p_state *s, struct hunk *hunk,
95 + struct strbuf *out)
96 +{
97 + strbuf_add(out, s->plain.buf + hunk->start,
98 + hunk->end - hunk->start);
99 +}
100 +
101 +static void reassemble_patch(struct add_p_state *s, struct strbuf *out)
102 +{
103 + struct hunk *hunk;
104 + size_t i;
105 +
106 + render_hunk(s, &s->head, out);
107 +
108 + for (i = 0; i < s->hunk_nr; i++) {
109 + hunk = s->hunk + i;
110 + if (hunk->use == USE_HUNK)
111 + render_hunk(s, hunk, out);
112 + }
113 +}
114 +
115 +static const char help_patch_text[] =
116 +N_("y - stage this hunk\n"
117 + "n - do not stage this hunk\n"
118 + "a - stage this and all the remaining hunks\n"
119 + "d - do not stage this hunk nor any of the remaining hunks\n"
120 + "j - leave this hunk undecided, see next undecided hunk\n"
121 + "J - leave this hunk undecided, see next hunk\n"
122 + "k - leave this hunk undecided, see previous undecided hunk\n"
123 + "K - leave this hunk undecided, see previous hunk\n"
124 + "? - print help\n");
125 +
126 +static int patch_update_file(struct add_p_state *s)
127 +{
128 + size_t hunk_index = 0;
129 + ssize_t i, undecided_previous, undecided_next;
130 + struct hunk *hunk;
131 + char ch;
132 + struct child_process cp = CHILD_PROCESS_INIT;
133 +
134 + if (!s->hunk_nr)
135 + return 0;
136 +
137 + strbuf_reset(&s->buf);
138 + render_hunk(s, &s->head, &s->buf);
139 + fputs(s->buf.buf, stdout);
140 + for (;;) {
141 + if (hunk_index >= s->hunk_nr)
142 + hunk_index = 0;
143 + hunk = s->hunk + hunk_index;
144 +
145 + undecided_previous = -1;
146 + for (i = hunk_index - 1; i >= 0; i--)
147 + if (s->hunk[i].use == UNDECIDED_HUNK) {
148 + undecided_previous = i;
149 + break;
150 + }
151 +
152 + undecided_next = -1;
153 + for (i = hunk_index + 1; i < s->hunk_nr; i++)
154 + if (s->hunk[i].use == UNDECIDED_HUNK) {
155 + undecided_next = i;
156 + break;
157 + }
158 +
159 + /* Everything decided? */
160 + if (undecided_previous < 0 && undecided_next < 0 &&
161 + hunk->use != UNDECIDED_HUNK)
162 + break;
163 +
164 + strbuf_reset(&s->buf);
165 + render_hunk(s, hunk, &s->buf);
166 + fputs(s->buf.buf, stdout);
167 +
168 + strbuf_reset(&s->buf);
169 + if (undecided_previous >= 0)
170 + strbuf_addstr(&s->buf, ",k");
171 + if (hunk_index)
172 + strbuf_addstr(&s->buf, ",K");
173 + if (undecided_next >= 0)
174 + strbuf_addstr(&s->buf, ",j");
175 + if (hunk_index + 1 < s->hunk_nr)
176 + strbuf_addstr(&s->buf, ",J");
177 + printf("(%"PRIuMAX"/%"PRIuMAX") ",
178 + (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);
179 + printf(_("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf);
180 + fflush(stdout);
181 + if (strbuf_getline(&s->answer, stdin) == EOF)
182 + break;
183 + strbuf_trim_trailing_newline(&s->answer);
184 +
185 + if (!s->answer.len)
186 + continue;
187 + ch = tolower(s->answer.buf[0]);
188 + if (ch == 'y') {
189 + hunk->use = USE_HUNK;
190 +soft_increment:
191 + hunk_index = undecided_next < 0 ?
192 + s->hunk_nr : undecided_next;
193 + } else if (ch == 'n') {
194 + hunk->use = SKIP_HUNK;
195 + goto soft_increment;
196 + } else if (ch == 'a') {
197 + for (; hunk_index < s->hunk_nr; hunk_index++) {
198 + hunk = s->hunk + hunk_index;
199 + if (hunk->use == UNDECIDED_HUNK)
200 + hunk->use = USE_HUNK;
201 + }
202 + } else if (ch == 'd') {
203 + for (; hunk_index < s->hunk_nr; hunk_index++) {
204 + hunk = s->hunk + hunk_index;
205 + if (hunk->use == UNDECIDED_HUNK)
206 + hunk->use = SKIP_HUNK;
207 + }
208 + } else if (hunk_index && s->answer.buf[0] == 'K')
209 + hunk_index--;
210 + else if (hunk_index + 1 < s->hunk_nr &&
211 + s->answer.buf[0] == 'J')
212 + hunk_index++;
213 + else if (undecided_previous >= 0 &&
214 + s->answer.buf[0] == 'k')
215 + hunk_index = undecided_previous;
216 + else if (undecided_next >= 0 && s->answer.buf[0] == 'j')
217 + hunk_index = undecided_next;
218 + else
219 + puts(_(help_patch_text));
220 + }
221 +
222 + /* Any hunk to be used? */
223 + for (i = 0; i < s->hunk_nr; i++)
224 + if (s->hunk[i].use == USE_HUNK)
225 + break;
226 +
227 + if (i < s->hunk_nr) {
228 + /* At least one hunk selected: apply */
229 + strbuf_reset(&s->buf);
230 + reassemble_patch(s, &s->buf);
231 +
232 + discard_index(s->r->index);
233 + setup_child_process(s, &cp, "apply", "--cached", NULL);
234 + if (pipe_command(&cp, s->buf.buf, s->buf.len,
235 + NULL, 0, NULL, 0))
236 + error(_("'git apply --cached' failed"));
237 + if (!repo_read_index(s->r))
238 + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
239 + 1, NULL, NULL, NULL);
240 + }
241 +
242 + putchar('\n');
243 + return 0;
244 +}
245 +
246 +int run_add_p(struct repository *r, const struct pathspec *ps)
247 +{
248 + struct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT };
249 +
250 + if (discard_index(r->index) < 0 || repo_read_index(r) < 0 ||
251 + repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
252 + NULL, NULL, NULL) < 0 ||
253 + parse_diff(&s, ps) < 0) {
254 + strbuf_release(&s.plain);
255 + return -1;
256 + }
257 +
258 + if (s.hunk_nr)
259 + patch_update_file(&s);
260 +
261 + strbuf_release(&s.answer);
262 + strbuf_release(&s.buf);
263 + strbuf_release(&s.plain);
264 + return 0;
265 +}
builtin/add.c
+10 -5
@@ -189,12 +189,17 @@ int run_add_interactive(const char *revision, const char *patch_mode,
189 int use_builtin_add_i =
190 git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1);
191
192 - if (!patch_mode) {
193 - if (use_builtin_add_i < 0)
194 - git_config_get_bool("add.interactive.usebuiltin",
195 - &use_builtin_add_i);
196 - if (use_builtin_add_i == 1)
192 + if (use_builtin_add_i < 0)
193 + git_config_get_bool("add.interactive.usebuiltin",
194 + &use_builtin_add_i);
195 +
196 + if (use_builtin_add_i == 1) {
197 + if (!patch_mode)
198 return !!run_add_i(the_repository, pathspec);
199 + if (strcmp(patch_mode, "--patch"))
200 + die("'%s' not yet supported in the built-in add -p",
201 + patch_mode);
202 + return !!run_add_p(the_repository, pathspec);
203 }
204
205 argv_array_push(&argv, "add--interactive");