]> git.ipfire.org Git - thirdparty/git.git/blame - builtin/bisect--helper.c
bisect--helper: `get_terms` & `bisect_terms` shell function in C
[thirdparty/git.git] / builtin / bisect--helper.c
CommitLineData
1bf072e3
CC
1#include "builtin.h"
2#include "cache.h"
3#include "parse-options.h"
4#include "bisect.h"
4ba1e5c4 5#include "refs.h"
5e82c3dd
PB
6#include "dir.h"
7#include "argv-array.h"
8#include "run-command.h"
129a6cf3 9#include "prompt.h"
1bf072e3 10
ecb3f373 11static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS")
b903674b
PB
12static GIT_PATH_FUNC(git_path_bisect_expected_rev, "BISECT_EXPECTED_REV")
13static GIT_PATH_FUNC(git_path_bisect_ancestors_ok, "BISECT_ANCESTORS_OK")
5e82c3dd
PB
14static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
15static GIT_PATH_FUNC(git_path_bisect_head, "BISECT_HEAD")
0f30233a 16static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
ecb3f373 17
1bf072e3 18static const char * const git_bisect_helper_usage[] = {
9e7bbe2d 19 N_("git bisect--helper --next-all [--no-checkout]"),
ecb3f373 20 N_("git bisect--helper --write-terms <bad_term> <good_term>"),
fb71a329 21 N_("git bisect--helper --bisect-clean-state"),
5e82c3dd 22 N_("git bisect--helper --bisect-reset [<commit>]"),
0f30233a 23 N_("git bisect--helper --bisect-write [--no-log] <state> <revision> <good_term> <bad_term>"),
4fbdbd5b 24 N_("git bisect--helper --bisect-check-and-set-terms <command> <good_term> <bad_term>"),
129a6cf3 25 N_("git bisect--helper --bisect-next-check <good_term> <bad_term> [<term>]"),
450ebb73 26 N_("git bisect--helper --bisect-terms [--term-good | --term-old | --term-bad | --term-new]"),
1bf072e3
CC
27 NULL
28};
29
0f30233a
PB
30struct bisect_terms {
31 char *term_good;
32 char *term_bad;
33};
34
35static void free_terms(struct bisect_terms *terms)
36{
37 FREE_AND_NULL(terms->term_good);
38 FREE_AND_NULL(terms->term_bad);
39}
40
41static void set_terms(struct bisect_terms *terms, const char *bad,
42 const char *good)
43{
44 free((void *)terms->term_good);
45 terms->term_good = xstrdup(good);
46 free((void *)terms->term_bad);
47 terms->term_bad = xstrdup(bad);
48}
49
129a6cf3
PB
50static const char *vocab_bad = "bad|new";
51static const char *vocab_good = "good|old";
52
4ba1e5c4
PB
53/*
54 * Check whether the string `term` belongs to the set of strings
55 * included in the variable arguments.
56 */
57LAST_ARG_MUST_BE_NULL
58static int one_of(const char *term, ...)
59{
60 int res = 0;
61 va_list matches;
62 const char *match;
63
64 va_start(matches, term);
65 while (!res && (match = va_arg(matches, const char *)))
66 res = !strcmp(term, match);
67 va_end(matches);
68
69 return res;
70}
71
72static int check_term_format(const char *term, const char *orig_term)
73{
74 int res;
75 char *new_term = xstrfmt("refs/bisect/%s", term);
76
77 res = check_refname_format(new_term, 0);
78 free(new_term);
79
80 if (res)
81 return error(_("'%s' is not a valid term"), term);
82
83 if (one_of(term, "help", "start", "skip", "next", "reset",
dbc349bb 84 "visualize", "view", "replay", "log", "run", "terms", NULL))
4ba1e5c4
PB
85 return error(_("can't use the builtin command '%s' as a term"), term);
86
87 /*
88 * In theory, nothing prevents swapping completely good and bad,
89 * but this situation could be confusing and hasn't been tested
90 * enough. Forbid it for now.
91 */
92
93 if ((strcmp(orig_term, "bad") && one_of(term, "bad", "new", NULL)) ||
94 (strcmp(orig_term, "good") && one_of(term, "good", "old", NULL)))
95 return error(_("can't change the meaning of the term '%s'"), term);
96
97 return 0;
98}
99
ecb3f373
PB
100static int write_terms(const char *bad, const char *good)
101{
102 FILE *fp = NULL;
103 int res;
104
105 if (!strcmp(bad, good))
106 return error(_("please use two different terms"));
107
108 if (check_term_format(bad, "bad") || check_term_format(good, "good"))
109 return -1;
110
111 fp = fopen(git_path_bisect_terms(), "w");
112 if (!fp)
113 return error_errno(_("could not open the file BISECT_TERMS"));
114
115 res = fprintf(fp, "%s\n%s\n", bad, good);
116 res |= fclose(fp);
117 return (res < 0) ? -1 : 0;
118}
119
b903674b
PB
120static int is_expected_rev(const char *expected_hex)
121{
122 struct strbuf actual_hex = STRBUF_INIT;
123 int res = 0;
124 if (strbuf_read_file(&actual_hex, git_path_bisect_expected_rev(), 0) >= 40) {
125 strbuf_trim(&actual_hex);
126 res = !strcmp(actual_hex.buf, expected_hex);
127 }
128 strbuf_release(&actual_hex);
129 return res;
130}
131
132static void check_expected_revs(const char **revs, int rev_nr)
133{
134 int i;
135
136 for (i = 0; i < rev_nr; i++) {
137 if (!is_expected_rev(revs[i])) {
138 unlink_or_warn(git_path_bisect_ancestors_ok());
139 unlink_or_warn(git_path_bisect_expected_rev());
140 }
141 }
142}
143
5e82c3dd
PB
144static int bisect_reset(const char *commit)
145{
146 struct strbuf branch = STRBUF_INIT;
147
148 if (!commit) {
149 if (strbuf_read_file(&branch, git_path_bisect_start(), 0) < 1) {
150 printf(_("We are not bisecting.\n"));
151 return 0;
152 }
153 strbuf_rtrim(&branch);
154 } else {
155 struct object_id oid;
156
157 if (get_oid_commit(commit, &oid))
158 return error(_("'%s' is not a valid commit"), commit);
159 strbuf_addstr(&branch, commit);
160 }
161
162 if (!file_exists(git_path_bisect_head())) {
163 struct argv_array argv = ARGV_ARRAY_INIT;
164
165 argv_array_pushl(&argv, "checkout", branch.buf, "--", NULL);
166 if (run_command_v_opt(argv.argv, RUN_GIT_CMD)) {
167 strbuf_release(&branch);
168 argv_array_clear(&argv);
169 return error(_("could not check out original"
170 " HEAD '%s'. Try 'git bisect"
171 "reset <commit>'."), branch.buf);
172 }
173 argv_array_clear(&argv);
174 }
175
176 strbuf_release(&branch);
177 return bisect_clean_state();
178}
179
0f30233a
PB
180static void log_commit(FILE *fp, char *fmt, const char *state,
181 struct commit *commit)
182{
183 struct pretty_print_context pp = {0};
184 struct strbuf commit_msg = STRBUF_INIT;
185 char *label = xstrfmt(fmt, state);
186
187 format_commit_message(commit, "%s", &commit_msg, &pp);
188
189 fprintf(fp, "# %s: [%s] %s\n", label, oid_to_hex(&commit->object.oid),
190 commit_msg.buf);
191
192 strbuf_release(&commit_msg);
193 free(label);
194}
195
196static int bisect_write(const char *state, const char *rev,
197 const struct bisect_terms *terms, int nolog)
198{
199 struct strbuf tag = STRBUF_INIT;
200 struct object_id oid;
201 struct commit *commit;
202 FILE *fp = NULL;
203 int retval = 0;
204
205 if (!strcmp(state, terms->term_bad)) {
206 strbuf_addf(&tag, "refs/bisect/%s", state);
207 } else if (one_of(state, terms->term_good, "skip", NULL)) {
208 strbuf_addf(&tag, "refs/bisect/%s-%s", state, rev);
209 } else {
210 retval = error(_("Bad bisect_write argument: %s"), state);
211 goto finish;
212 }
213
214 if (get_oid(rev, &oid)) {
215 retval = error(_("couldn't get the oid of the rev '%s'"), rev);
216 goto finish;
217 }
218
219 if (update_ref(NULL, tag.buf, &oid, NULL, 0,
220 UPDATE_REFS_MSG_ON_ERR)) {
221 retval = -1;
222 goto finish;
223 }
224
225 fp = fopen(git_path_bisect_log(), "a");
226 if (!fp) {
227 retval = error_errno(_("couldn't open the file '%s'"), git_path_bisect_log());
228 goto finish;
229 }
230
231 commit = lookup_commit_reference(the_repository, &oid);
232 log_commit(fp, "%s", state, commit);
233
234 if (!nolog)
235 fprintf(fp, "git bisect %s %s\n", state, rev);
236
237finish:
238 if (fp)
239 fclose(fp);
240 strbuf_release(&tag);
241 return retval;
242}
243
4fbdbd5b
PB
244static int check_and_set_terms(struct bisect_terms *terms, const char *cmd)
245{
246 int has_term_file = !is_empty_or_missing_file(git_path_bisect_terms());
247
248 if (one_of(cmd, "skip", "start", "terms", NULL))
249 return 0;
250
251 if (has_term_file && strcmp(cmd, terms->term_bad) &&
252 strcmp(cmd, terms->term_good))
253 return error(_("Invalid command: you're currently in a "
254 "%s/%s bisect"), terms->term_bad,
255 terms->term_good);
256
257 if (!has_term_file) {
258 if (one_of(cmd, "bad", "good", NULL)) {
259 set_terms(terms, "bad", "good");
260 return write_terms(terms->term_bad, terms->term_good);
261 }
262 if (one_of(cmd, "new", "old", NULL)) {
263 set_terms(terms, "new", "old");
264 return write_terms(terms->term_bad, terms->term_good);
265 }
266 }
267
268 return 0;
269}
270
129a6cf3
PB
271static int mark_good(const char *refname, const struct object_id *oid,
272 int flag, void *cb_data)
273{
274 int *m_good = (int *)cb_data;
275 *m_good = 0;
276 return 1;
277}
278
279static const char *need_bad_and_good_revision_warning =
280 N_("You need to give me at least one %s and %s revision.\n"
281 "You can use \"git bisect %s\" and \"git bisect %s\" for that.");
282
283static const char *need_bisect_start_warning =
284 N_("You need to start by \"git bisect start\".\n"
285 "You then need to give me at least one %s and %s revision.\n"
286 "You can use \"git bisect %s\" and \"git bisect %s\" for that.");
287
288static int bisect_next_check(const struct bisect_terms *terms,
289 const char *current_term)
290{
291 int missing_good = 1, missing_bad = 1, retval = 0;
292 const char *bad_ref = xstrfmt("refs/bisect/%s", terms->term_bad);
293 const char *good_glob = xstrfmt("%s-*", terms->term_good);
294
295 if (ref_exists(bad_ref))
296 missing_bad = 0;
297
298 for_each_glob_ref_in(mark_good, good_glob, "refs/bisect/",
299 (void *) &missing_good);
300
301 if (!missing_good && !missing_bad)
302 goto finish;
303
304 if (!current_term) {
305 retval = -1;
306 goto finish;
307 }
308
309 if (missing_good && !missing_bad &&
310 !strcmp(current_term, terms->term_good)) {
311 char *yesno;
312 /*
313 * have bad (or new) but not good (or old). We could bisect
314 * although this is less optimum.
315 */
316 warning(_("bisecting only with a %s commit"), terms->term_bad);
317 if (!isatty(0))
318 goto finish;
319 /*
320 * TRANSLATORS: Make sure to include [Y] and [n] in your
321 * translation. The program will only accept English input
322 * at this point.
323 */
324 yesno = git_prompt(_("Are you sure [Y/n]? "), PROMPT_ECHO);
325 if (starts_with(yesno, "N") || starts_with(yesno, "n"))
326 retval = -1;
327 goto finish;
328 }
329 if (!is_empty_or_missing_file(git_path_bisect_start())) {
330 retval = error(_(need_bad_and_good_revision_warning),
331 vocab_bad, vocab_good, vocab_bad, vocab_good);
332 } else {
333 retval = error(_(need_bisect_start_warning),
334 vocab_good, vocab_bad, vocab_good, vocab_bad);
335 }
336
337finish:
338 free((void *) good_glob);
339 free((void *) bad_ref);
340 return retval;
341}
342
450ebb73
PB
343static int get_terms(struct bisect_terms *terms)
344{
345 struct strbuf str = STRBUF_INIT;
346 FILE *fp = NULL;
347 int res = 0;
348
349 fp = fopen(git_path_bisect_terms(), "r");
350 if (!fp) {
351 res = -1;
352 goto finish;
353 }
354
355 free_terms(terms);
356 strbuf_getline_lf(&str, fp);
357 terms->term_bad = strbuf_detach(&str, NULL);
358 strbuf_getline_lf(&str, fp);
359 terms->term_good = strbuf_detach(&str, NULL);
360
361finish:
362 if (fp)
363 fclose(fp);
364 strbuf_release(&str);
365 return res;
366}
367
368static int bisect_terms(struct bisect_terms *terms, const char *option)
369{
370 if (get_terms(terms))
371 return error(_("no terms defined"));
372
373 if (option == NULL) {
374 printf(_("Your current terms are %s for the old state\n"
375 "and %s for the new state.\n"),
376 terms->term_good, terms->term_bad);
377 return 0;
378 }
379 if (one_of(option, "--term-good", "--term-old", NULL))
380 printf("%s\n", terms->term_good);
381 else if (one_of(option, "--term-bad", "--term-new", NULL))
382 printf("%s\n", terms->term_bad);
383 else
384 return error(_("invalid argument %s for 'git bisect terms'.\n"
385 "Supported options are: "
386 "--term-good|--term-old and "
387 "--term-bad|--term-new."), option);
388
389 return 0;
390}
391
1bf072e3
CC
392int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
393{
4ba1e5c4
PB
394 enum {
395 NEXT_ALL = 1,
fb71a329 396 WRITE_TERMS,
b903674b 397 BISECT_CLEAN_STATE,
5e82c3dd 398 CHECK_EXPECTED_REVS,
0f30233a 399 BISECT_RESET,
4fbdbd5b 400 BISECT_WRITE,
129a6cf3 401 CHECK_AND_SET_TERMS,
450ebb73
PB
402 BISECT_NEXT_CHECK,
403 BISECT_TERMS
4ba1e5c4 404 } cmdmode = 0;
0f30233a 405 int no_checkout = 0, res = 0, nolog = 0;
1bf072e3 406 struct option options[] = {
9e1c84df
PB
407 OPT_CMDMODE(0, "next-all", &cmdmode,
408 N_("perform 'git bisect next'"), NEXT_ALL),
ecb3f373
PB
409 OPT_CMDMODE(0, "write-terms", &cmdmode,
410 N_("write the terms to .git/BISECT_TERMS"), WRITE_TERMS),
fb71a329
PB
411 OPT_CMDMODE(0, "bisect-clean-state", &cmdmode,
412 N_("cleanup the bisection state"), BISECT_CLEAN_STATE),
b903674b
PB
413 OPT_CMDMODE(0, "check-expected-revs", &cmdmode,
414 N_("check for expected revs"), CHECK_EXPECTED_REVS),
5e82c3dd
PB
415 OPT_CMDMODE(0, "bisect-reset", &cmdmode,
416 N_("reset the bisection state"), BISECT_RESET),
0f30233a
PB
417 OPT_CMDMODE(0, "bisect-write", &cmdmode,
418 N_("write out the bisection state in BISECT_LOG"), BISECT_WRITE),
4fbdbd5b
PB
419 OPT_CMDMODE(0, "check-and-set-terms", &cmdmode,
420 N_("check and set terms in a bisection state"), CHECK_AND_SET_TERMS),
129a6cf3
PB
421 OPT_CMDMODE(0, "bisect-next-check", &cmdmode,
422 N_("check whether bad or good terms exist"), BISECT_NEXT_CHECK),
450ebb73
PB
423 OPT_CMDMODE(0, "bisect-terms", &cmdmode,
424 N_("print out the bisect terms"), BISECT_TERMS),
d5d09d47
SB
425 OPT_BOOL(0, "no-checkout", &no_checkout,
426 N_("update BISECT_HEAD instead of checking out the current commit")),
0f30233a
PB
427 OPT_BOOL(0, "no-log", &nolog,
428 N_("no log for BISECT_WRITE ")),
1bf072e3
CC
429 OPT_END()
430 };
0f30233a 431 struct bisect_terms terms = { .term_good = NULL, .term_bad = NULL };
1bf072e3 432
37782920 433 argc = parse_options(argc, argv, prefix, options,
450ebb73 434 git_bisect_helper_usage, PARSE_OPT_KEEP_UNKNOWN);
1bf072e3 435
9e1c84df 436 if (!cmdmode)
1bf072e3
CC
437 usage_with_options(git_bisect_helper_usage, options);
438
9e1c84df
PB
439 switch (cmdmode) {
440 case NEXT_ALL:
441 return bisect_next_all(prefix, no_checkout);
ecb3f373 442 case WRITE_TERMS:
4ba1e5c4 443 if (argc != 2)
ecb3f373
PB
444 return error(_("--write-terms requires two arguments"));
445 return write_terms(argv[0], argv[1]);
fb71a329
PB
446 case BISECT_CLEAN_STATE:
447 if (argc != 0)
448 return error(_("--bisect-clean-state requires no arguments"));
449 return bisect_clean_state();
b903674b
PB
450 case CHECK_EXPECTED_REVS:
451 check_expected_revs(argv, argc);
452 return 0;
5e82c3dd
PB
453 case BISECT_RESET:
454 if (argc > 1)
455 return error(_("--bisect-reset requires either no argument or a commit"));
456 return !!bisect_reset(argc ? argv[0] : NULL);
0f30233a
PB
457 case BISECT_WRITE:
458 if (argc != 4 && argc != 5)
459 return error(_("--bisect-write requires either 4 or 5 arguments"));
460 set_terms(&terms, argv[3], argv[2]);
461 res = bisect_write(argv[0], argv[1], &terms, nolog);
462 break;
4fbdbd5b
PB
463 case CHECK_AND_SET_TERMS:
464 if (argc != 3)
465 return error(_("--check-and-set-terms requires 3 arguments"));
466 set_terms(&terms, argv[2], argv[1]);
467 res = check_and_set_terms(&terms, argv[0]);
468 break;
129a6cf3
PB
469 case BISECT_NEXT_CHECK:
470 if (argc != 2 && argc != 3)
471 return error(_("--bisect-next-check requires 2 or 3 arguments"));
472 set_terms(&terms, argv[1], argv[0]);
473 res = bisect_next_check(&terms, argc == 3 ? argv[2] : NULL);
474 break;
450ebb73
PB
475 case BISECT_TERMS:
476 if (argc > 1)
477 return error(_("--bisect-terms requires 0 or 1 argument"));
478 res = bisect_terms(&terms, argc == 1 ? argv[0] : NULL);
479 break;
9e1c84df
PB
480 default:
481 return error("BUG: unknown subcommand '%d'", cmdmode);
482 }
0f30233a
PB
483 free_terms(&terms);
484 return !!res;
1bf072e3 485}