02ea5a96112c02637f0d26f9609e11522f6d5fab
   1#include "builtin.h"
   2#include "cache.h"
   3#include "parse-options.h"
   4#include "bisect.h"
   5#include "refs.h"
   6#include "dir.h"
   7#include "argv-array.h"
   8#include "run-command.h"
   9
  10static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS")
  11static GIT_PATH_FUNC(git_path_bisect_expected_rev, "BISECT_EXPECTED_REV")
  12static GIT_PATH_FUNC(git_path_bisect_ancestors_ok, "BISECT_ANCESTORS_OK")
  13static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
  14static GIT_PATH_FUNC(git_path_bisect_head, "BISECT_HEAD")
  15static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
  16
  17static const char * const git_bisect_helper_usage[] = {
  18        N_("git bisect--helper --next-all [--no-checkout]"),
  19        N_("git bisect--helper --write-terms <bad_term> <good_term>"),
  20        N_("git bisect--helper --bisect-clean-state"),
  21        N_("git bisect--helper --bisect-reset [<commit>]"),
  22        N_("git bisect--helper --bisect-write [--no-log] <state> <revision> <good_term> <bad_term>"),
  23        N_("git bisect--helper --bisect-check-and-set-terms <command> <good_term> <bad_term>"),
  24        NULL
  25};
  26
  27struct bisect_terms {
  28        char *term_good;
  29        char *term_bad;
  30};
  31
  32static void free_terms(struct bisect_terms *terms)
  33{
  34        FREE_AND_NULL(terms->term_good);
  35        FREE_AND_NULL(terms->term_bad);
  36}
  37
  38static void set_terms(struct bisect_terms *terms, const char *bad,
  39                      const char *good)
  40{
  41        free((void *)terms->term_good);
  42        terms->term_good = xstrdup(good);
  43        free((void *)terms->term_bad);
  44        terms->term_bad = xstrdup(bad);
  45}
  46
  47/*
  48 * Check whether the string `term` belongs to the set of strings
  49 * included in the variable arguments.
  50 */
  51LAST_ARG_MUST_BE_NULL
  52static int one_of(const char *term, ...)
  53{
  54        int res = 0;
  55        va_list matches;
  56        const char *match;
  57
  58        va_start(matches, term);
  59        while (!res && (match = va_arg(matches, const char *)))
  60                res = !strcmp(term, match);
  61        va_end(matches);
  62
  63        return res;
  64}
  65
  66static int check_term_format(const char *term, const char *orig_term)
  67{
  68        int res;
  69        char *new_term = xstrfmt("refs/bisect/%s", term);
  70
  71        res = check_refname_format(new_term, 0);
  72        free(new_term);
  73
  74        if (res)
  75                return error(_("'%s' is not a valid term"), term);
  76
  77        if (one_of(term, "help", "start", "skip", "next", "reset",
  78                        "visualize", "view", "replay", "log", "run", "terms", NULL))
  79                return error(_("can't use the builtin command '%s' as a term"), term);
  80
  81        /*
  82         * In theory, nothing prevents swapping completely good and bad,
  83         * but this situation could be confusing and hasn't been tested
  84         * enough. Forbid it for now.
  85         */
  86
  87        if ((strcmp(orig_term, "bad") && one_of(term, "bad", "new", NULL)) ||
  88                 (strcmp(orig_term, "good") && one_of(term, "good", "old", NULL)))
  89                return error(_("can't change the meaning of the term '%s'"), term);
  90
  91        return 0;
  92}
  93
  94static int write_terms(const char *bad, const char *good)
  95{
  96        FILE *fp = NULL;
  97        int res;
  98
  99        if (!strcmp(bad, good))
 100                return error(_("please use two different terms"));
 101
 102        if (check_term_format(bad, "bad") || check_term_format(good, "good"))
 103                return -1;
 104
 105        fp = fopen(git_path_bisect_terms(), "w");
 106        if (!fp)
 107                return error_errno(_("could not open the file BISECT_TERMS"));
 108
 109        res = fprintf(fp, "%s\n%s\n", bad, good);
 110        res |= fclose(fp);
 111        return (res < 0) ? -1 : 0;
 112}
 113
 114static int is_expected_rev(const char *expected_hex)
 115{
 116        struct strbuf actual_hex = STRBUF_INIT;
 117        int res = 0;
 118        if (strbuf_read_file(&actual_hex, git_path_bisect_expected_rev(), 0) >= 40) {
 119                strbuf_trim(&actual_hex);
 120                res = !strcmp(actual_hex.buf, expected_hex);
 121        }
 122        strbuf_release(&actual_hex);
 123        return res;
 124}
 125
 126static void check_expected_revs(const char **revs, int rev_nr)
 127{
 128        int i;
 129
 130        for (i = 0; i < rev_nr; i++) {
 131                if (!is_expected_rev(revs[i])) {
 132                        unlink_or_warn(git_path_bisect_ancestors_ok());
 133                        unlink_or_warn(git_path_bisect_expected_rev());
 134                }
 135        }
 136}
 137
 138static int bisect_reset(const char *commit)
 139{
 140        struct strbuf branch = STRBUF_INIT;
 141
 142        if (!commit) {
 143                if (strbuf_read_file(&branch, git_path_bisect_start(), 0) < 1) {
 144                        printf(_("We are not bisecting.\n"));
 145                        return 0;
 146                }
 147                strbuf_rtrim(&branch);
 148        } else {
 149                struct object_id oid;
 150
 151                if (get_oid_commit(commit, &oid))
 152                        return error(_("'%s' is not a valid commit"), commit);
 153                strbuf_addstr(&branch, commit);
 154        }
 155
 156        if (!file_exists(git_path_bisect_head())) {
 157                struct argv_array argv = ARGV_ARRAY_INIT;
 158
 159                argv_array_pushl(&argv, "checkout", branch.buf, "--", NULL);
 160                if (run_command_v_opt(argv.argv, RUN_GIT_CMD)) {
 161                        strbuf_release(&branch);
 162                        argv_array_clear(&argv);
 163                        return error(_("could not check out original"
 164                                       " HEAD '%s'. Try 'git bisect"
 165                                       "reset <commit>'."), branch.buf);
 166                }
 167                argv_array_clear(&argv);
 168        }
 169
 170        strbuf_release(&branch);
 171        return bisect_clean_state();
 172}
 173
 174static void log_commit(FILE *fp, char *fmt, const char *state,
 175                       struct commit *commit)
 176{
 177        struct pretty_print_context pp = {0};
 178        struct strbuf commit_msg = STRBUF_INIT;
 179        char *label = xstrfmt(fmt, state);
 180
 181        format_commit_message(commit, "%s", &commit_msg, &pp);
 182
 183        fprintf(fp, "# %s: [%s] %s\n", label, oid_to_hex(&commit->object.oid),
 184                commit_msg.buf);
 185
 186        strbuf_release(&commit_msg);
 187        free(label);
 188}
 189
 190static int bisect_write(const char *state, const char *rev,
 191                        const struct bisect_terms *terms, int nolog)
 192{
 193        struct strbuf tag = STRBUF_INIT;
 194        struct object_id oid;
 195        struct commit *commit;
 196        FILE *fp = NULL;
 197        int retval = 0;
 198
 199        if (!strcmp(state, terms->term_bad)) {
 200                strbuf_addf(&tag, "refs/bisect/%s", state);
 201        } else if (one_of(state, terms->term_good, "skip", NULL)) {
 202                strbuf_addf(&tag, "refs/bisect/%s-%s", state, rev);
 203        } else {
 204                retval = error(_("Bad bisect_write argument: %s"), state);
 205                goto finish;
 206        }
 207
 208        if (get_oid(rev, &oid)) {
 209                retval = error(_("couldn't get the oid of the rev '%s'"), rev);
 210                goto finish;
 211        }
 212
 213        if (update_ref(NULL, tag.buf, &oid, NULL, 0,
 214                       UPDATE_REFS_MSG_ON_ERR)) {
 215                retval = -1;
 216                goto finish;
 217        }
 218
 219        fp = fopen(git_path_bisect_log(), "a");
 220        if (!fp) {
 221                retval = error_errno(_("couldn't open the file '%s'"), git_path_bisect_log());
 222                goto finish;
 223        }
 224
 225        commit = lookup_commit_reference(the_repository, &oid);
 226        log_commit(fp, "%s", state, commit);
 227
 228        if (!nolog)
 229                fprintf(fp, "git bisect %s %s\n", state, rev);
 230
 231finish:
 232        if (fp)
 233                fclose(fp);
 234        strbuf_release(&tag);
 235        return retval;
 236}
 237
 238static int check_and_set_terms(struct bisect_terms *terms, const char *cmd)
 239{
 240        int has_term_file = !is_empty_or_missing_file(git_path_bisect_terms());
 241
 242        if (one_of(cmd, "skip", "start", "terms", NULL))
 243                return 0;
 244
 245        if (has_term_file && strcmp(cmd, terms->term_bad) &&
 246            strcmp(cmd, terms->term_good))
 247                return error(_("Invalid command: you're currently in a "
 248                                "%s/%s bisect"), terms->term_bad,
 249                                terms->term_good);
 250
 251        if (!has_term_file) {
 252                if (one_of(cmd, "bad", "good", NULL)) {
 253                        set_terms(terms, "bad", "good");
 254                        return write_terms(terms->term_bad, terms->term_good);
 255                }
 256                if (one_of(cmd, "new", "old", NULL)) {
 257                        set_terms(terms, "new", "old");
 258                        return write_terms(terms->term_bad, terms->term_good);
 259                }
 260        }
 261
 262        return 0;
 263}
 264
 265int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 266{
 267        enum {
 268                NEXT_ALL = 1,
 269                WRITE_TERMS,
 270                BISECT_CLEAN_STATE,
 271                CHECK_EXPECTED_REVS,
 272                BISECT_RESET,
 273                BISECT_WRITE,
 274                CHECK_AND_SET_TERMS
 275        } cmdmode = 0;
 276        int no_checkout = 0, res = 0, nolog = 0;
 277        struct option options[] = {
 278                OPT_CMDMODE(0, "next-all", &cmdmode,
 279                         N_("perform 'git bisect next'"), NEXT_ALL),
 280                OPT_CMDMODE(0, "write-terms", &cmdmode,
 281                         N_("write the terms to .git/BISECT_TERMS"), WRITE_TERMS),
 282                OPT_CMDMODE(0, "bisect-clean-state", &cmdmode,
 283                         N_("cleanup the bisection state"), BISECT_CLEAN_STATE),
 284                OPT_CMDMODE(0, "check-expected-revs", &cmdmode,
 285                         N_("check for expected revs"), CHECK_EXPECTED_REVS),
 286                OPT_CMDMODE(0, "bisect-reset", &cmdmode,
 287                         N_("reset the bisection state"), BISECT_RESET),
 288                OPT_CMDMODE(0, "bisect-write", &cmdmode,
 289                         N_("write out the bisection state in BISECT_LOG"), BISECT_WRITE),
 290                OPT_CMDMODE(0, "check-and-set-terms", &cmdmode,
 291                         N_("check and set terms in a bisection state"), CHECK_AND_SET_TERMS),
 292                OPT_BOOL(0, "no-checkout", &no_checkout,
 293                         N_("update BISECT_HEAD instead of checking out the current commit")),
 294                OPT_BOOL(0, "no-log", &nolog,
 295                         N_("no log for BISECT_WRITE ")),
 296                OPT_END()
 297        };
 298        struct bisect_terms terms = { .term_good = NULL, .term_bad = NULL };
 299
 300        argc = parse_options(argc, argv, prefix, options,
 301                             git_bisect_helper_usage, 0);
 302
 303        if (!cmdmode)
 304                usage_with_options(git_bisect_helper_usage, options);
 305
 306        switch (cmdmode) {
 307        case NEXT_ALL:
 308                return bisect_next_all(prefix, no_checkout);
 309        case WRITE_TERMS:
 310                if (argc != 2)
 311                        return error(_("--write-terms requires two arguments"));
 312                return write_terms(argv[0], argv[1]);
 313        case BISECT_CLEAN_STATE:
 314                if (argc != 0)
 315                        return error(_("--bisect-clean-state requires no arguments"));
 316                return bisect_clean_state();
 317        case CHECK_EXPECTED_REVS:
 318                check_expected_revs(argv, argc);
 319                return 0;
 320        case BISECT_RESET:
 321                if (argc > 1)
 322                        return error(_("--bisect-reset requires either no argument or a commit"));
 323                return !!bisect_reset(argc ? argv[0] : NULL);
 324        case BISECT_WRITE:
 325                if (argc != 4 && argc != 5)
 326                        return error(_("--bisect-write requires either 4 or 5 arguments"));
 327                set_terms(&terms, argv[3], argv[2]);
 328                res = bisect_write(argv[0], argv[1], &terms, nolog);
 329                break;
 330        case CHECK_AND_SET_TERMS:
 331                if (argc != 3)
 332                        return error(_("--check-and-set-terms requires 3 arguments"));
 333                set_terms(&terms, argv[2], argv[1]);
 334                res = check_and_set_terms(&terms, argv[0]);
 335                break;
 336        default:
 337                return error("BUG: unknown subcommand '%d'", cmdmode);
 338        }
 339        free_terms(&terms);
 340        return !!res;
 341}