From e3eda4a6f793b6295fbc79cfe55ef95c4c6e5e72 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:28 -0700 Subject: [PATCH 1/8] checkout: pass cb_option explicitly to branch name parsers The file-scope static variable 'cb_option' is used to record whether a new branch is being created via '-b' (in 'git checkout') or '-c' (in 'git switch'), primarily for error reporting and advice messages in parse_remote_branch(). Global mutable state makes the code harder to reason about and refactor. Pass 'cb_option' explicitly as a parameter to parse_remote_branch() and parse_branchname_arg(), removing the file-scope static variable. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 55e3a89a85..774e4fd5b3 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1346,7 +1346,7 @@ enum checkout_command { static char *parse_remote_branch(const char *arg, struct object_id *rev, int could_be_checkout_paths, - enum checkout_command which_command) + char cb_option) { int num_matches = 0; char *remote = unique_tracking_name(arg, rev, &num_matches); @@ -1361,16 +1361,15 @@ static char *parse_remote_branch(const char *arg, if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) { const char *cmdname; - switch (which_command) { - case CHECKOUT_CHECKOUT: + switch (cb_option) { + case 'b': cmdname = "checkout"; break; - case CHECKOUT_SWITCH: + case 'c': cmdname = "switch"; break; default: - BUG("command <%d> should not reach parse_remote_branch", - which_command); + BUG("unexpected cb_option '%c'", cb_option); break; } @@ -1394,7 +1393,7 @@ static char *parse_remote_branch(const char *arg, static int parse_branchname_arg(int argc, const char **argv, int dwim_new_local_branch_ok, - enum checkout_command which_command, + char cb_option, struct branch_info *new_branch_info, struct checkout_opts *opts, struct object_id *rev) @@ -1505,7 +1504,7 @@ static int parse_branchname_arg(int argc, const char **argv, if (recover_with_dwim) { remote = parse_remote_branch(arg, rev, could_be_checkout_paths, - which_command); + cb_option); if (remote) { *new_branch = arg; arg = remote; @@ -1832,9 +1831,6 @@ static struct option *add_checkout_path_options(struct checkout_opts *opts, return newopts; } -/* create-branch option (either b or c) */ -static char cb_option = 'b'; - static int checkout_main(int argc, const char **argv, const char *prefix, struct checkout_opts *opts, struct option *options, enum checkout_command which_command) @@ -1842,6 +1838,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, int parseopt_flags = 0; struct branch_info new_branch_info = { 0 }; int ret; + char cb_option = (which_command == CHECKOUT_SWITCH) ? 'c' : 'b'; static const char * const checkout_usage[] = { N_("git checkout [] "), @@ -1997,7 +1994,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, opts->dwim_new_local_branch && opts->track == BRANCH_TRACK_UNSPECIFIED && !opts->new_branch; - int n = parse_branchname_arg(argc, argv, dwim_ok, which_command, + int n = parse_branchname_arg(argc, argv, dwim_ok, cb_option, &new_branch_info, opts, &rev); argv += n; argc -= n; @@ -2174,8 +2171,6 @@ int cmd_switch(int argc, options = add_common_options(&opts, options); options = add_common_switch_branch_options(&opts, options); - cb_option = 'c'; - return checkout_main(argc, argv, prefix, &opts, options, CHECKOUT_SWITCH); } From 3b87cfb731e5ffd9c8daa8fbebd566f0fb82e91a Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:29 -0700 Subject: [PATCH 2/8] checkout: validate new branch name in checkout_branch() In checkout_main(), new branch name validation is performed before dispatching to checkout_branch() or checkout_paths(). Checking out paths does not create new branches, so this validation only belongs in checkout_branch(). Move the validate_branchname() and validate_new_branchname() calls from checkout_main() into checkout_branch(). checkout_paths() checks and fails if '.new_branch' is set before doing anything, which indicates that this change is safe and makes good sense. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 774e4fd5b3..14542626e9 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1734,6 +1734,17 @@ static int checkout_branch(struct checkout_opts *opts, free(full_ref); } + if (opts->new_branch) { + struct strbuf buf = STRBUF_INIT; + + if (opts->new_branch_force) + opts->branch_exists = validate_branchname(opts->new_branch, &buf); + else + opts->branch_exists = + validate_new_branchname(opts->new_branch, &buf, 0); + strbuf_release(&buf); + } + if (!new_branch_info->commit && opts->new_branch) { struct object_id rev; int flag; @@ -2062,17 +2073,6 @@ static int checkout_main(int argc, const char **argv, const char *prefix, die(_("you must specify path(s) to restore")); } - if (opts->new_branch) { - struct strbuf buf = STRBUF_INIT; - - if (opts->new_branch_force) - opts->branch_exists = validate_branchname(opts->new_branch, &buf); - else - opts->branch_exists = - validate_new_branchname(opts->new_branch, &buf, 0); - strbuf_release(&buf); - } - if (opts->patch_mode || opts->pathspec.nr) ret = checkout_paths(opts, &new_branch_info); else From d2e9d3597a6c121c1affb0c609c039a31ffa9a60 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:30 -0700 Subject: [PATCH 3/8] checkout: validate stage and merge option compatibility in checkout_paths() In checkout_main(), checking that no more than one of --ours/--theirs, --force, and --merge is specified is performed when pathspecs are present, before dispatching to checkout_paths(). Checking out a branch does not use index stages, so this validation belongs in checkout_paths(). Move the incompatibility check from checkout_main() into checkout_paths(). Signed-off-by: Junio C Hamano --- builtin/checkout.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 14542626e9..e3d23256e2 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -591,6 +591,10 @@ static int checkout_paths(const struct checkout_opts *opts, die(_("'%s', '%s', or '%s' cannot be used when checking out of a tree"), "--merge", "--ours", "--theirs"); + if (1 < !!opts->writeout_stage + !!opts->force + !!opts->merge) + die(_("git checkout: --ours/--theirs, --force and --merge are incompatible when\n" + "checking out of the index.")); + if (opts->patch_mode) { enum add_p_mode patch_mode; struct interactive_options interactive_opts = { @@ -2063,11 +2067,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, opts->pathspec.recursive = 1; - if (opts->pathspec.nr) { - if (1 < !!opts->writeout_stage + !!opts->force + !!opts->merge) - die(_("git checkout: --ours/--theirs, --force and --merge are incompatible when\n" - "checking out of the index.")); - } else { + if (!opts->pathspec.nr) { if (opts->accept_pathspec && !opts->empty_pathspec_ok && !opts->patch_mode) /* patch mode is special */ die(_("you must specify path(s) to restore")); From 0ae025f69c385217fae7ffeb5a4676b043caa65f Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:31 -0700 Subject: [PATCH 4/8] checkout: extract option validation and pathspec helpers The checkout_main() function handles initialization, option parsing, option validation, and pathspec resolution. Extract option validation and pathspec handling logic into static helper functions: - init_checkout_opts() initializes default checkout options and repo settings. - validate_path_options() validates patch context and index/worktree flags. - prepare_common_options() prepares progress, merge, and force options. - parse_pathspec_from_file_options() parses and validates pathspecs read from a file. Call these helpers from checkout_main(). Signed-off-by: Junio C Hamano --- builtin/checkout.c | 171 +++++++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 75 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index e3d23256e2..8d567def7e 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1846,6 +1846,98 @@ static struct option *add_checkout_path_options(struct checkout_opts *opts, return newopts; } +static void init_checkout_opts(struct checkout_opts *opts, const char *prefix) +{ + opts->overwrite_ignore = 1; + opts->prefix = prefix; + opts->show_progress = -1; + + repo_config(the_repository, git_checkout_config, opts); + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + opts->track = BRANCH_TRACK_UNSPECIFIED; +} + +static void validate_path_options(struct checkout_opts *opts) +{ + if (opts->patch_context < -1) + die(_("'%s' cannot be negative"), "--unified"); + if (opts->patch_interhunk_context < -1) + die(_("'%s' cannot be negative"), "--inter-hunk-context"); + + if (!opts->patch_mode) { + if (opts->patch_context != -1) + die(_("the option '%s' requires '%s'"), "--unified", "--patch"); + if (opts->patch_interhunk_context != -1) + die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch"); + if (!opts->auto_advance) + die(_("the option '%s' requires '%s'"), "--no-auto-advance", "--patch"); + } + + if (opts->overlay_mode == 1 && opts->patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay"); + + if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) { + if (opts->checkout_index < 0) + opts->checkout_index = 0; + if (opts->checkout_worktree < 0) + opts->checkout_worktree = 0; + } else { + if (opts->checkout_index < 0) + opts->checkout_index = -opts->checkout_index - 1; + if (opts->checkout_worktree < 0) + opts->checkout_worktree = -opts->checkout_worktree - 1; + } + if (opts->checkout_index < 0 || opts->checkout_worktree < 0) + BUG("these flags should be non-negative by now"); +} + +static void prepare_common_options(struct checkout_opts *opts) +{ + if (opts->show_progress < 0) { + if (opts->quiet) + opts->show_progress = 0; + else + opts->show_progress = isatty(2); + } + + /* --conflicts implies --merge */ + if (opts->merge == -1) + opts->merge = opts->conflict_style >= 0; + + if (opts->force) { + opts->discard_changes = 1; + opts->ignore_unmerged_opt = "--force"; + opts->ignore_unmerged = 1; + } +} + +static void parse_pathspec_from_file_options(struct checkout_opts *opts, + const char *prefix) +{ + if (opts->pathspec_from_file) { + if (opts->pathspec.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + if (opts->force_detach) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach"); + + if (opts->patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); + + parse_pathspec_file(&opts->pathspec, 0, + 0, + prefix, opts->pathspec_from_file, opts->pathspec_file_nul); + } else if (opts->pathspec_file_nul) { + die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + } + + opts->pathspec.recursive = 1; +} + static int checkout_main(int argc, const char **argv, const char *prefix, struct checkout_opts *opts, struct option *options, enum checkout_command which_command) @@ -1887,17 +1979,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, BUG("no such checkout variant %d", which_command); } - opts->overwrite_ignore = 1; - opts->prefix = prefix; - opts->show_progress = -1; - - repo_config(the_repository, git_checkout_config, opts); - if (the_repository->gitdir) { - prepare_repo_settings(the_repository); - the_repository->settings.command_requires_full_index = 0; - } - - opts->track = BRANCH_TRACK_UNSPECIFIED; + init_checkout_opts(opts, prefix); if (!opts->accept_pathspec && !opts->accept_ref) BUG("make up your mind, you need to take _something_"); @@ -1907,57 +1989,13 @@ static int checkout_main(int argc, const char **argv, const char *prefix, argc = parse_options(argc, argv, prefix, options, usagestr, parseopt_flags); - if (opts->patch_context < -1) - die(_("'%s' cannot be negative"), "--unified"); - if (opts->patch_interhunk_context < -1) - die(_("'%s' cannot be negative"), "--inter-hunk-context"); - - if (!opts->patch_mode) { - if (opts->patch_context != -1) - die(_("the option '%s' requires '%s'"), "--unified", "--patch"); - if (opts->patch_interhunk_context != -1) - die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch"); - if (!opts->auto_advance) - die(_("the option '%s' requires '%s'"), "--no-auto-advance", "--patch"); - } - - if (opts->show_progress < 0) { - if (opts->quiet) - opts->show_progress = 0; - else - opts->show_progress = isatty(2); - } - - /* --conflicts implies --merge */ - if (opts->merge == -1) - opts->merge = opts->conflict_style >= 0; - - if (opts->force) { - opts->discard_changes = 1; - opts->ignore_unmerged_opt = "--force"; - opts->ignore_unmerged = 1; - } + validate_path_options(opts); + prepare_common_options(opts); if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) die(_("options '-%c', '-%c', and '%s' cannot be used together"), cb_option, toupper(cb_option), "--orphan"); - if (opts->overlay_mode == 1 && opts->patch_mode) - die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay"); - - if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) { - if (opts->checkout_index < 0) - opts->checkout_index = 0; - if (opts->checkout_worktree < 0) - opts->checkout_worktree = 0; - } else { - if (opts->checkout_index < 0) - opts->checkout_index = -opts->checkout_index - 1; - if (opts->checkout_worktree < 0) - opts->checkout_worktree = -opts->checkout_worktree - 1; - } - if (opts->checkout_index < 0 || opts->checkout_worktree < 0) - BUG("these flags should be non-negative by now"); /* * convenient shortcut: "git restore --staged [--worktree]" equals * "git restore --staged [--worktree] --source HEAD" @@ -2048,24 +2086,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, argv[0]); } - if (opts->pathspec_from_file) { - if (opts->pathspec.nr) - die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); - - if (opts->force_detach) - die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach"); - - if (opts->patch_mode) - die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); - - parse_pathspec_file(&opts->pathspec, 0, - 0, - prefix, opts->pathspec_from_file, opts->pathspec_file_nul); - } else if (opts->pathspec_file_nul) { - die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); - } - - opts->pathspec.recursive = 1; + parse_pathspec_from_file_options(opts, prefix); if (!opts->pathspec.nr) { if (opts->accept_pathspec && !opts->empty_pathspec_ok && From 3daa89ed0f3d9420b5c508f42e04c4e5229b53fc Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:32 -0700 Subject: [PATCH 5/8] checkout: extract branch setup and tracking helpers The checkout_main() function validates branch-creation options, DWIMs tracking branch options, and sets up branch information directly in its body. Extract these branch setup operations into static helper functions: - validate_branch_options() validates compatibility of '-b', '-B', and '--orphan' options. - dwim_branch_track_option() infers the branch name when '--track' is given without an explicit branch name. - setup_branch_name_and_info() drives branch validation and parses the branch name argument. Call the new setup helper from checkout_main(). Signed-off-by: Junio C Hamano --- builtin/checkout.c | 104 +++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 8d567def7e..2edaca5539 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1938,6 +1938,57 @@ static void parse_pathspec_from_file_options(struct checkout_opts *opts, opts->pathspec.recursive = 1; } +static void validate_branch_options(struct checkout_opts *opts, char cb_option) +{ + if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) + die(_("options '-%c', '-%c', and '%s' cannot be used together"), + cb_option, toupper(cb_option), "--orphan"); + + if (opts->new_branch_force) + opts->new_branch = opts->new_branch_force; + + if (opts->new_orphan_branch) + opts->new_branch = opts->new_orphan_branch; +} + +static void dwim_branch_track_option(int argc, const char **argv, + struct checkout_opts *opts, char cb_option) +{ + /* --track without -c/-C/-b/-B/--orphan should DWIM */ + if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) { + const char *argv0 = argv[0]; + if (!argc || !strcmp(argv0, "--")) + die(_("--track needs a branch name")); + skip_prefix(argv0, "refs/", &argv0); + skip_prefix(argv0, "remotes/", &argv0); + argv0 = strchr(argv0, '/'); + if (!argv0 || !argv0[1]) + die(_("missing branch name; try -%c"), cb_option); + opts->new_branch = argv0 + 1; + } +} + +static int setup_branch_name_and_info(int argc, const char **argv, + struct checkout_opts *opts, + struct branch_info *new_branch_info, + char cb_option) +{ + validate_branch_options(opts, cb_option); + dwim_branch_track_option(argc, argv, opts, cb_option); + + if (argc) { + struct object_id rev; + int dwim_ok = + !opts->patch_mode && + opts->dwim_new_local_branch && + opts->track == BRANCH_TRACK_UNSPECIFIED && + !opts->new_branch; + return parse_branchname_arg(argc, argv, dwim_ok, cb_option, + new_branch_info, opts, &rev); + } + return 0; +} + static int checkout_main(int argc, const char **argv, const char *prefix, struct checkout_opts *opts, struct option *options, enum checkout_command which_command) @@ -1992,10 +2043,6 @@ static int checkout_main(int argc, const char **argv, const char *prefix, validate_path_options(opts); prepare_common_options(opts); - if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) - die(_("options '-%c', '-%c', and '%s' cannot be used together"), - cb_option, toupper(cb_option), "--orphan"); - /* * convenient shortcut: "git restore --staged [--worktree]" equals * "git restore --staged [--worktree] --source HEAD" @@ -2003,52 +2050,9 @@ static int checkout_main(int argc, const char **argv, const char *prefix, if (!opts->from_treeish && opts->checkout_index) opts->from_treeish = "HEAD"; - /* - * From here on, new_branch will contain the branch to be checked out, - * and new_branch_force and new_orphan_branch will tell us which one of - * -b/-B/-c/-C/--orphan is being used. - */ - if (opts->new_branch_force) - opts->new_branch = opts->new_branch_force; - - if (opts->new_orphan_branch) - opts->new_branch = opts->new_orphan_branch; - - /* --track without -c/-C/-b/-B/--orphan should DWIM */ - if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) { - const char *argv0 = argv[0]; - if (!argc || !strcmp(argv0, "--")) - die(_("--track needs a branch name")); - skip_prefix(argv0, "refs/", &argv0); - skip_prefix(argv0, "remotes/", &argv0); - argv0 = strchr(argv0, '/'); - if (!argv0 || !argv0[1]) - die(_("missing branch name; try -%c"), cb_option); - opts->new_branch = argv0 + 1; - } - - /* - * Extract branch name from command line arguments, so - * all that is left is pathspecs. - * - * Handle - * - * 1) git checkout -- [] - * 2) git checkout -- [] - * 3) git checkout [] - * - * including "last branch" syntax and DWIM-ery for names of - * remote branches, erroring out for invalid or ambiguous cases. - */ - if (argc && opts->accept_ref) { - struct object_id rev; - int dwim_ok = - !opts->patch_mode && - opts->dwim_new_local_branch && - opts->track == BRANCH_TRACK_UNSPECIFIED && - !opts->new_branch; - int n = parse_branchname_arg(argc, argv, dwim_ok, cb_option, - &new_branch_info, opts, &rev); + if (opts->accept_ref) { + int n = setup_branch_name_and_info(argc, argv, opts, + &new_branch_info, cb_option); argv += n; argc -= n; } else if (!opts->accept_ref && opts->from_treeish) { From 164d722de4ef2ea569ac56ca060ca832433735a3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:33 -0700 Subject: [PATCH 6/8] checkout: restructure switch, restore, and checkout entrypoints cmd_switch(), cmd_restore(), and cmd_checkout() pass their options to checkout_main(), which parses options and configuration, validates and dispatches to checkout_branch() or checkout_paths(). Now that option initialization, validation, and branch setup have been split into dedicated helper functions, restructure cmd_switch(), cmd_restore(), and cmd_checkout() to invoke these helpers directly and dispatch to checkout_branch() or checkout_paths(). In cmd_restore(), handle the --staged default from_treeish = "HEAD" and resolve opts.from_treeish into new_branch_info and opts.source_tree. This allows us to remove checkout_main() and enum checkout_command as they are no longer needed. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 311 +++++++++++++++++++++++---------------------- 1 file changed, 156 insertions(+), 155 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 2edaca5539..b18515ac7f 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1341,12 +1341,6 @@ static void setup_new_branch_info_and_source_tree( } -enum checkout_command { - CHECKOUT_CHECKOUT = 1, - CHECKOUT_SWITCH = 2, - CHECKOUT_RESTORE = 3, -}; - static char *parse_remote_branch(const char *arg, struct object_id *rev, int could_be_checkout_paths, @@ -1989,19 +1983,25 @@ static int setup_branch_name_and_info(int argc, const char **argv, return 0; } -static int checkout_main(int argc, const char **argv, const char *prefix, - struct checkout_opts *opts, struct option *options, - enum checkout_command which_command) +int cmd_switch(int argc, + const char **argv, + const char *prefix, + struct repository *repo UNUSED) { - int parseopt_flags = 0; + struct checkout_opts opts = CHECKOUT_OPTS_INIT; + struct option *options = NULL; struct branch_info new_branch_info = { 0 }; int ret; - char cb_option = (which_command == CHECKOUT_SWITCH) ? 'c' : 'b'; - - static const char * const checkout_usage[] = { - N_("git checkout [] "), - N_("git checkout [] [] -- ..."), - NULL, + struct option switch_options[] = { + OPT_STRING('c', "create", &opts.new_branch, N_("branch"), + N_("create and switch to a new branch")), + OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"), + N_("create/reset and switch to a branch")), + OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, + N_("second guess 'git switch '")), + OPT_BOOL(0, "discard-changes", &opts.discard_changes, + N_("throw away local modifications")), + OPT_END() }; static const char * const switch_branch_usage[] = { @@ -2009,103 +2009,125 @@ static int checkout_main(int argc, const char **argv, const char *prefix, NULL, }; + opts.dwim_new_local_branch = 1; + opts.accept_ref = 1; + opts.accept_pathspec = 0; + opts.switch_branch_doing_nothing_is_ok = 0; + opts.only_merge_on_switching_branches = 1; + opts.implicit_detach = 0; + opts.can_switch_when_in_progress = 0; + opts.orphan_from_empty_tree = 1; + opts.overlay_mode = -1; + + init_checkout_opts(&opts, prefix); + + options = parse_options_dup(switch_options); + options = add_common_options(&opts, options); + options = add_common_switch_branch_options(&opts, options); + + argc = parse_options(argc, argv, prefix, options, + switch_branch_usage, 0); + + prepare_common_options(&opts); + setup_branch_name_and_info(argc, argv, &opts, &new_branch_info, 'c'); + + ret = checkout_branch(&opts, &new_branch_info); + + branch_info_release(&new_branch_info); + clear_pathspec(&opts.pathspec); + free(opts.pathspec_from_file); + free(options); + + return ret; +} + +int cmd_restore(int argc, + const char **argv, + const char *prefix, + struct repository *repo UNUSED) +{ + struct checkout_opts opts = CHECKOUT_OPTS_INIT; + struct option *options; + struct branch_info new_branch_info = { 0 }; + int ret; + struct option restore_options[] = { + OPT_STRING('s', "source", &opts.from_treeish, "", + N_("which tree-ish to checkout from")), + OPT_BOOL('S', "staged", &opts.checkout_index, + N_("restore the index")), + OPT_BOOL('W', "worktree", &opts.checkout_worktree, + N_("restore the working tree (default)")), + OPT_BOOL(0, "ignore-unmerged", &opts.ignore_unmerged, + N_("ignore unmerged entries")), + OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode")), + OPT_END() + }; + static const char * const restore_usage[] = { N_("git restore [] [--source=] ..."), NULL, }; - const char * const *usagestr; + opts.accept_ref = 0; + opts.accept_pathspec = 1; + opts.empty_pathspec_ok = 0; + opts.overlay_mode = 0; + opts.checkout_index = -1; /* default off */ + opts.checkout_worktree = -2; /* default on */ + opts.ignore_unmerged_opt = "--ignore-unmerged"; - switch (which_command) { - case CHECKOUT_CHECKOUT: - usagestr = checkout_usage; - break; - case CHECKOUT_SWITCH: - usagestr = switch_branch_usage; - break; - case CHECKOUT_RESTORE: - usagestr = restore_usage; - break; - default: - BUG("no such checkout variant %d", which_command); - } + init_checkout_opts(&opts, prefix); - init_checkout_opts(opts, prefix); - - if (!opts->accept_pathspec && !opts->accept_ref) - BUG("make up your mind, you need to take _something_"); - if (opts->accept_pathspec && opts->accept_ref) - parseopt_flags = PARSE_OPT_KEEP_DASHDASH; + options = parse_options_dup(restore_options); + options = add_common_options(&opts, options); + options = add_checkout_path_options(&opts, options); argc = parse_options(argc, argv, prefix, options, - usagestr, parseopt_flags); + restore_usage, 0); - validate_path_options(opts); - prepare_common_options(opts); + validate_path_options(&opts); + prepare_common_options(&opts); /* * convenient shortcut: "git restore --staged [--worktree]" equals * "git restore --staged [--worktree] --source HEAD" */ - if (!opts->from_treeish && opts->checkout_index) - opts->from_treeish = "HEAD"; + if (!opts.from_treeish && opts.checkout_index) + opts.from_treeish = "HEAD"; - if (opts->accept_ref) { - int n = setup_branch_name_and_info(argc, argv, opts, - &new_branch_info, cb_option); - argv += n; - argc -= n; - } else if (!opts->accept_ref && opts->from_treeish) { + if (opts.from_treeish) { struct object_id rev; - if (repo_get_oid_mb(the_repository, opts->from_treeish, &rev)) - die(_("could not resolve '%s'"), opts->from_treeish); + if (repo_get_oid_mb(the_repository, opts.from_treeish, &rev)) + die(_("could not resolve '%s'"), opts.from_treeish); setup_new_branch_info_and_source_tree(&new_branch_info, - opts, &rev, - opts->from_treeish); + &opts, &rev, + opts.from_treeish); - if (!opts->source_tree) - die(_("reference is not a tree: %s"), opts->from_treeish); + if (!opts.source_tree) + die(_("reference is not a tree: %s"), opts.from_treeish); } if (argc) { - parse_pathspec(&opts->pathspec, 0, - opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0, + parse_pathspec(&opts.pathspec, 0, + opts.patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0, prefix, argv); - if (!opts->pathspec.nr) + if (!opts.pathspec.nr) die(_("invalid path specification")); - - /* - * Try to give more helpful suggestion. - * new_branch && argc > 1 will be caught later. - */ - if (opts->new_branch && argc == 1 && !new_branch_info.commit) - die(_("'%s' is not a commit and a branch '%s' cannot be created from it"), - argv[0], opts->new_branch); - - if (opts->force_detach) - die(_("git checkout: --detach does not take a path argument '%s'"), - argv[0]); } - parse_pathspec_from_file_options(opts, prefix); + parse_pathspec_from_file_options(&opts, prefix); - if (!opts->pathspec.nr) { - if (opts->accept_pathspec && !opts->empty_pathspec_ok && - !opts->patch_mode) /* patch mode is special */ - die(_("you must specify path(s) to restore")); - } + if (!opts.pathspec.nr && !opts.patch_mode) + die(_("you must specify path(s) to restore")); - if (opts->patch_mode || opts->pathspec.nr) - ret = checkout_paths(opts, &new_branch_info); - else - ret = checkout_branch(opts, &new_branch_info); + ret = checkout_paths(&opts, &new_branch_info); branch_info_release(&new_branch_info); - clear_pathspec(&opts->pathspec); - free(opts->pathspec_from_file); + clear_pathspec(&opts.pathspec); + free(opts.pathspec_from_file); free(options); return ret; @@ -2118,6 +2140,8 @@ int cmd_checkout(int argc, { struct checkout_opts opts = CHECKOUT_OPTS_INIT; struct option *options; + struct branch_info new_branch_info = { 0 }; + int ret, n; struct option checkout_options[] = { OPT_STRING('b', NULL, &opts.new_branch, N_("branch"), N_("create and checkout a new branch")), @@ -2132,6 +2156,12 @@ int cmd_checkout(int argc, OPT_END() }; + static const char * const checkout_usage[] = { + N_("git checkout [] "), + N_("git checkout [] [] -- ..."), + NULL, + }; + opts.dwim_new_local_branch = 1; opts.switch_branch_doing_nothing_is_ok = 1; opts.only_merge_on_switching_branches = 0; @@ -2154,84 +2184,55 @@ int cmd_checkout(int argc, opts.only_merge_on_switching_branches = 1; } + init_checkout_opts(&opts, prefix); + options = parse_options_dup(checkout_options); options = add_common_options(&opts, options); options = add_common_switch_branch_options(&opts, options); options = add_checkout_path_options(&opts, options); - return checkout_main(argc, argv, prefix, &opts, options, - CHECKOUT_CHECKOUT); -} - -int cmd_switch(int argc, - const char **argv, - const char *prefix, - struct repository *repo UNUSED) -{ - struct checkout_opts opts = CHECKOUT_OPTS_INIT; - struct option *options = NULL; - struct option switch_options[] = { - OPT_STRING('c', "create", &opts.new_branch, N_("branch"), - N_("create and switch to a new branch")), - OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"), - N_("create/reset and switch to a branch")), - OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, - N_("second guess 'git switch '")), - OPT_BOOL(0, "discard-changes", &opts.discard_changes, - N_("throw away local modifications")), - OPT_END() - }; - - opts.dwim_new_local_branch = 1; - opts.accept_ref = 1; - opts.accept_pathspec = 0; - opts.switch_branch_doing_nothing_is_ok = 0; - opts.only_merge_on_switching_branches = 1; - opts.implicit_detach = 0; - opts.can_switch_when_in_progress = 0; - opts.orphan_from_empty_tree = 1; - opts.overlay_mode = -1; - - options = parse_options_dup(switch_options); - options = add_common_options(&opts, options); - options = add_common_switch_branch_options(&opts, options); - - return checkout_main(argc, argv, prefix, &opts, options, - CHECKOUT_SWITCH); -} - -int cmd_restore(int argc, - const char **argv, - const char *prefix, - struct repository *repo UNUSED) -{ - struct checkout_opts opts = CHECKOUT_OPTS_INIT; - struct option *options; - struct option restore_options[] = { - OPT_STRING('s', "source", &opts.from_treeish, "", - N_("which tree-ish to checkout from")), - OPT_BOOL('S', "staged", &opts.checkout_index, - N_("restore the index")), - OPT_BOOL('W', "worktree", &opts.checkout_worktree, - N_("restore the working tree (default)")), - OPT_BOOL(0, "ignore-unmerged", &opts.ignore_unmerged, - N_("ignore unmerged entries")), - OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode")), - OPT_END() - }; - - opts.accept_ref = 0; - opts.accept_pathspec = 1; - opts.empty_pathspec_ok = 0; - opts.overlay_mode = 0; - opts.checkout_index = -1; /* default off */ - opts.checkout_worktree = -2; /* default on */ - opts.ignore_unmerged_opt = "--ignore-unmerged"; - - options = parse_options_dup(restore_options); - options = add_common_options(&opts, options); - options = add_checkout_path_options(&opts, options); - - return checkout_main(argc, argv, prefix, &opts, options, - CHECKOUT_RESTORE); + argc = parse_options(argc, argv, prefix, options, + checkout_usage, PARSE_OPT_KEEP_DASHDASH); + + validate_path_options(&opts); + prepare_common_options(&opts); + + n = setup_branch_name_and_info(argc, argv, &opts, &new_branch_info, 'b'); + argv += n; + argc -= n; + + if (argc) { + parse_pathspec(&opts.pathspec, 0, + opts.patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0, + prefix, argv); + + if (!opts.pathspec.nr) + die(_("invalid path specification")); + + /* + * Try to give more helpful suggestion. + * new_branch && argc > 1 will be caught later. + */ + if (opts.new_branch && argc == 1 && !new_branch_info.commit) + die(_("'%s' is not a commit and a branch '%s' cannot be created from it"), + argv[0], opts.new_branch); + + if (opts.force_detach) + die(_("git checkout: --detach does not take a path argument '%s'"), + argv[0]); + } + + parse_pathspec_from_file_options(&opts, prefix); + + if (opts.patch_mode || opts.pathspec.nr) + ret = checkout_paths(&opts, &new_branch_info); + else + ret = checkout_branch(&opts, &new_branch_info); + + branch_info_release(&new_branch_info); + clear_pathspec(&opts.pathspec); + free(opts.pathspec_from_file); + free(options); + + return ret; } From 3ac7c3315c475184703bebe5ae169832066f5e4e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:34 -0700 Subject: [PATCH 7/8] checkout: wrap overly long lines So far, the patches in this series have tried to leave the original code intact as much as possible when moving it, to make the refactoring easier to review. However, there are quite a few overly long lines that are hard to read. There are also several manual checks for mutually incompatible options where die_for_incompatible_optN() could be used instead. Now that most of the refactoring is complete, tidy up these warts to finish off the series. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 57 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index b18515ac7f..9771c848c5 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1864,15 +1864,18 @@ static void validate_path_options(struct checkout_opts *opts) if (!opts->patch_mode) { if (opts->patch_context != -1) - die(_("the option '%s' requires '%s'"), "--unified", "--patch"); + die(_("the option '%s' requires '%s'"), + "--unified", "--patch"); if (opts->patch_interhunk_context != -1) - die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch"); + die(_("the option '%s' requires '%s'"), + "--inter-hunk-context", "--patch"); if (!opts->auto_advance) - die(_("the option '%s' requires '%s'"), "--no-auto-advance", "--patch"); + die(_("the option '%s' requires '%s'"), + "--no-auto-advance", "--patch"); } - if (opts->overlay_mode == 1 && opts->patch_mode) - die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay"); + die_for_incompatible_opt2(opts->patch_mode, "-p", + opts->overlay_mode == 1, "--overlay"); if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) { if (opts->checkout_index < 0) @@ -1914,19 +1917,25 @@ static void parse_pathspec_from_file_options(struct checkout_opts *opts, { if (opts->pathspec_from_file) { if (opts->pathspec.nr) - die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + die(_("'%s' and pathspec arguments cannot be used together"), + "--pathspec-from-file"); - if (opts->force_detach) - die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach"); + die_for_incompatible_opt2(!!opts->pathspec_from_file, + "--pathspec-from-file", + opts->force_detach, + "--detach"); - if (opts->patch_mode) - die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); + die_for_incompatible_opt2(!!opts->pathspec_from_file, + "--pathspec-from-file", + opts->patch_mode, + "--patch"); - parse_pathspec_file(&opts->pathspec, 0, - 0, - prefix, opts->pathspec_from_file, opts->pathspec_file_nul); + parse_pathspec_file(&opts->pathspec, 0, 0, + prefix, opts->pathspec_from_file, + opts->pathspec_file_nul); } else if (opts->pathspec_file_nul) { - die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + die(_("the option '%s' requires '%s'"), + "--pathspec-file-nul", "--pathspec-from-file"); } opts->pathspec.recursive = 1; @@ -1934,9 +1943,17 @@ static void parse_pathspec_from_file_options(struct checkout_opts *opts, static void validate_branch_options(struct checkout_opts *opts, char cb_option) { - if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) - die(_("options '-%c', '-%c', and '%s' cannot be used together"), - cb_option, toupper(cb_option), "--orphan"); + char new_branch_opt[] = "-c"; + char new_branch_force_opt[] = "-C"; + + new_branch_opt[1] = cb_option; + new_branch_force_opt[1] = toupper(cb_option); + + die_for_incompatible_opt3(!!opts->new_branch, + new_branch_opt, + !!opts->new_branch_force, + new_branch_force_opt, + !!opts->new_orphan_branch, "--orphan"); if (opts->new_branch_force) opts->new_branch = opts->new_branch_force; @@ -2147,10 +2164,12 @@ int cmd_checkout(int argc, N_("create and checkout a new branch")), OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"), N_("create/reset and checkout a branch")), - OPT_BOOL('l', NULL, &opts.new_branch_log, N_("create reflog for new branch")), + OPT_BOOL('l', NULL, &opts.new_branch_log, + N_("create reflog for new branch")), OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, N_("second guess 'git checkout ' (default)")), - OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")), + OPT_BOOL(0, "overlay", &opts.overlay_mode, + N_("use overlay mode (default)")), OPT_BOOL(0, "auto-advance", &opts.auto_advance, N_("auto advance to the next file when selecting hunks interactively")), OPT_END() From 5ca19ca0b1e6cc6ec887460ffcd279f3c21d950e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Aug 2026 13:48:35 -0700 Subject: [PATCH 8/8] checkout: move post_checkout_hook() to checkout.c post_checkout_hook() in builtin/checkout.c runs the 'post-checkout' hook after switching branches or checking out paths. Move post_checkout_hook() to checkout.c and declare it in checkout.h so that other subsystems can invoke the post-checkout hook without depending on builtin/checkout.c. Remove the dependency on 'the_repository'. While OK when the helper was in builtin/checkout.c as an integral part of 'git checkout' (and 'git restore'), this is no longer true for a common utility function. Have it take a pointer to 'struct repository' and use its associated hash algorithm. This step in the series is entirely optional and is here primarily for illustration. We may later want to teach 'git worktree' to trigger the 'post-checkout' hook, for example, in which case such libification may turn out to be useful. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 23 +++-------------------- checkout.c | 28 ++++++++++++++++++++++++++++ checkout.h | 10 ++++++++++ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 9771c848c5..c6e29e9526 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -124,24 +124,6 @@ static void branch_info_release(struct branch_info *info) free(info->checkout); } -static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit, - int changed) -{ - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL; - - /* - * "new_commit" can be NULL when checking out from the index before - * a commit exists. - */ - strvec_pushl(&opt.args, - oid_to_hex(old_commit ? &old_commit->object.oid : null_oid(the_hash_algo)), - oid_to_hex(new_commit ? &new_commit->object.oid : null_oid(the_hash_algo)), - changed ? "1" : "0", - NULL); - - return run_hooks_opt(the_repository, "post-checkout", &opt); -} - /* * Handle a tree object and determine if we need to recurse into the * tree (READ_TREE_RECURSIVE) or skip it (0). @@ -718,7 +700,7 @@ static int checkout_paths(const struct checkout_opts *opts, &rev, NULL); head = lookup_commit_reference_gently(the_repository, &rev, 1); - errs |= post_checkout_hook(head, head, 0); + errs |= post_checkout_hook(the_repository, head, head, 0); return errs; } @@ -1273,7 +1255,8 @@ static int switch_branches(const struct checkout_opts *opts, } } - ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1); + ret = post_checkout_hook(the_repository, + old_branch_info.commit, new_branch_info->commit, 1); branch_info_release(&old_branch_info); strbuf_release(&old_commit_shortname); strbuf_release(&autostash_msg); diff --git a/checkout.c b/checkout.c index 1588b116ee..28c5c18d96 100644 --- a/checkout.c +++ b/checkout.c @@ -1,6 +1,9 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "commit.h" +#include "hex.h" +#include "hook.h" #include "object-name.h" #include "remote.h" #include "refspec.h" @@ -8,6 +11,7 @@ #include "checkout.h" #include "config.h" #include "strbuf.h" +#include "strvec.h" struct tracking_name_data { /* const */ char *src_ref; @@ -73,3 +77,27 @@ char *unique_tracking_name(const char *name, struct object_id *oid, } return NULL; } + +int post_checkout_hook(struct repository *repo, + struct commit *old_commit, struct commit *new_commit, + int changed) +{ + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL; + const struct git_hash_algo *hash_algo = repo->hash_algo; + + /* + * "new_commit" can be NULL when checking out from the index before + * a commit exists. + */ + strvec_pushl(&opt.args, + oid_to_hex(old_commit + ? &old_commit->object.oid + : null_oid(hash_algo)), + oid_to_hex(new_commit ? + &new_commit->object.oid + : null_oid(hash_algo)), + changed ? "1" : "0", + NULL); + + return run_hooks_opt(repo, "post-checkout", &opt); +} diff --git a/checkout.h b/checkout.h index 55920e7aeb..6d8d2c2bab 100644 --- a/checkout.h +++ b/checkout.h @@ -3,6 +3,9 @@ #include "hash.h" +struct commit; +struct repository; + /* * Check if the branch name uniquely matches a branch name on a remote * tracking branch. Return the name of the remote if such a branch @@ -12,4 +15,11 @@ char *unique_tracking_name(const char *name, struct object_id *oid, int *dwim_remotes_matched); +/* + * Run the post-checkout hook. + */ +int post_checkout_hook(struct repository *, + struct commit *old_commit, struct commit *new_commit, + int changed); + #endif /* CHECKOUT_H */