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