Skip to content

Commit 5051b44

Browse files
committed
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 5cde71d (git-add --interactive, 2006-12-10), but not too closely (for example, we use string offsets rather than copying strings around, and we also remember which previous/next hunk was undecided, rather than looking again when the user asked to jump there). 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. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent cb1a9e2 commit 5051b44

File tree

4 files changed

+270
-2
lines changed

4 files changed

+270
-2
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@ LIB_H = $(shell $(FIND) . \
850850

851851
LIB_OBJS += abspath.o
852852
LIB_OBJS += add-interactive.o
853+
LIB_OBJS += add-patch.o
853854
LIB_OBJS += advice.o
854855
LIB_OBJS += alias.o
855856
LIB_OBJS += alloc.o

add-interactive.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
struct repository;
55
struct pathspec;
66
int run_add_i(struct repository *r, const struct pathspec *ps);
7+
int run_add_p(struct repository *r, const struct pathspec *ps);
78

89
#endif

add-patch.c

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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 child_process *cp,
25+
struct add_p_state *s, ...)
26+
{
27+
va_list ap;
28+
const char *arg;
29+
30+
va_start(ap, s);
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(&cp, s,
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(_("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf);
178+
fflush(stdout);
179+
if (strbuf_getline(&s->answer, stdin) == EOF)
180+
break;
181+
strbuf_trim_trailing_newline(&s->answer);
182+
183+
if (!s->answer.len)
184+
continue;
185+
ch = tolower(s->answer.buf[0]);
186+
if (ch == 'y') {
187+
hunk->use = USE_HUNK;
188+
soft_increment:
189+
while (++hunk_index < s->hunk_nr &&
190+
s->hunk[hunk_index].use
191+
!= UNDECIDED_HUNK)
192+
; /* continue looking */
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+
setup_child_process(&cp, s, "apply", "--cached", NULL);
233+
if (pipe_command(&cp, s->buf.buf, s->buf.len,
234+
NULL, 0, NULL, 0))
235+
error(_("'git apply --cached' failed"));
236+
repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0);
237+
}
238+
239+
putchar('\n');
240+
return 0;
241+
}
242+
243+
int run_add_p(struct repository *r, const struct pathspec *ps)
244+
{
245+
struct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT };
246+
247+
if (repo_refresh_and_write_index(r, REFRESH_QUIET, 0) < 0 ||
248+
parse_diff(&s, ps) < 0) {
249+
strbuf_release(&s.plain);
250+
return -1;
251+
}
252+
253+
if (s.hunk_nr)
254+
patch_update_file(&s);
255+
256+
strbuf_release(&s.answer);
257+
strbuf_release(&s.buf);
258+
strbuf_release(&s.plain);
259+
return 0;
260+
}

builtin/add.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,14 @@ int run_add_interactive(const char *revision, const char *patch_mode,
192192
git_config_get_bool("add.interactive.usebuiltin",
193193
&use_builtin_add_i);
194194

195-
if (use_builtin_add_i == 1 && !patch_mode)
196-
return !!run_add_i(the_repository, pathspec);
195+
if (use_builtin_add_i == 1) {
196+
if (!patch_mode)
197+
return !!run_add_i(the_repository, pathspec);
198+
if (strcmp(patch_mode, "--patch"))
199+
die("'%s' not yet supported in the built-in add -p",
200+
patch_mode);
201+
return !!run_add_p(the_repository, pathspec);
202+
}
197203

198204
argv_array_push(&argv, "add--interactive");
199205
if (patch_mode)

0 commit comments

Comments
 (0)