Merge branch 'jc/checkout-refactor' into seen

The front-end code for 'git checkout', 'git switch', and 'git
restore' has been restructured to cleanly separate their pathspec
and branch handling, eliminating a common bottleneck and paving the
way to libify utility helpers.

* jc/checkout-refactor:
  checkout: move post_checkout_hook() to checkout.c
  checkout: wrap overly long lines
  checkout: restructure switch, restore, and checkout entrypoints
  checkout: extract branch setup and tracking helpers
  checkout: extract option validation and pathspec helpers
  checkout: validate stage and merge option compatibility in checkout_paths()
  checkout: validate new branch name in checkout_branch()
  checkout: pass cb_option explicitly to branch name parsers
seen
Junio C Hamano 2026-08-31 13:53:31 -07:00
commit 2934e3d3ef
3 changed files with 341 additions and 280 deletions

View File

@ -124,24 +124,6 @@ static void branch_info_release(struct branch_info *info)
free(info->checkout); 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 * Handle a tree object and determine if we need to recurse into the
* tree (READ_TREE_RECURSIVE) or skip it (0). * tree (READ_TREE_RECURSIVE) or skip it (0).
@ -591,6 +573,10 @@ static int checkout_paths(const struct checkout_opts *opts,
die(_("'%s', '%s', or '%s' cannot be used when checking out of a tree"), die(_("'%s', '%s', or '%s' cannot be used when checking out of a tree"),
"--merge", "--ours", "--theirs"); "--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) { if (opts->patch_mode) {
enum add_p_mode patch_mode; enum add_p_mode patch_mode;
struct interactive_options interactive_opts = { struct interactive_options interactive_opts = {
@ -714,7 +700,7 @@ static int checkout_paths(const struct checkout_opts *opts,
&rev, NULL); &rev, NULL);
head = lookup_commit_reference_gently(the_repository, &rev, 1); 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; return errs;
} }


@ -1272,7 +1258,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); branch_info_release(&old_branch_info);
strbuf_release(&old_commit_shortname); strbuf_release(&old_commit_shortname);
strbuf_release(&autostash_msg); strbuf_release(&autostash_msg);
@ -1340,29 +1327,23 @@ static void setup_new_branch_info_and_source_tree(
} }




enum checkout_command { static void advise_disambiguating_remotes(char cb_option,
CHECKOUT_CHECKOUT = 1,
CHECKOUT_SWITCH = 2,
CHECKOUT_RESTORE = 3,
};

static void advise_disambiguating_remotes(enum checkout_command which_command,
const char *branch, const char *branch,
const struct string_list *matched_remote_names) const struct string_list *matched_remote_names)
{ {
const char *cmdname; const char *cmdname;
struct string_list_item *item; struct string_list_item *item;


switch (which_command) { switch (cb_option) {
case CHECKOUT_CHECKOUT: case 'b':
cmdname = "checkout"; cmdname = "checkout";
break; break;
case CHECKOUT_SWITCH: case 'c':
cmdname = "switch"; cmdname = "switch";
break; break;
default: default:
BUG("command <%d> should not reach advise_disambiguating_remotes", BUG("command <%c> should not reach advise_disambiguating_remotes",
which_command); cb_option);
break; break;
} }


@ -1384,7 +1365,7 @@ static void advise_disambiguating_remotes(enum checkout_command which_command,
static char *parse_remote_branch(const char *arg, static char *parse_remote_branch(const char *arg,
struct object_id *rev, struct object_id *rev,
int could_be_checkout_paths, int could_be_checkout_paths,
enum checkout_command which_command) char cb_option)
{ {
int num_matches = 0; int num_matches = 0;
struct string_list matched_remote_names = STRING_LIST_INIT_DUP; struct string_list matched_remote_names = STRING_LIST_INIT_DUP;
@ -1400,7 +1381,7 @@ static char *parse_remote_branch(const char *arg,


if (!remote && num_matches > 1) { if (!remote && num_matches > 1) {
if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
advise_disambiguating_remotes(which_command, arg, advise_disambiguating_remotes(cb_option, arg,
&matched_remote_names); &matched_remote_names);
die(_("'%s' matched multiple (%d) remote tracking branches"), die(_("'%s' matched multiple (%d) remote tracking branches"),
arg, num_matches); arg, num_matches);
@ -1413,7 +1394,7 @@ static char *parse_remote_branch(const char *arg,


static int parse_branchname_arg(int argc, const char **argv, static int parse_branchname_arg(int argc, const char **argv,
int dwim_new_local_branch_ok, int dwim_new_local_branch_ok,
enum checkout_command which_command, char cb_option,
struct branch_info *new_branch_info, struct branch_info *new_branch_info,
struct checkout_opts *opts, struct checkout_opts *opts,
struct object_id *rev) struct object_id *rev)
@ -1524,7 +1505,7 @@ static int parse_branchname_arg(int argc, const char **argv,
if (recover_with_dwim) { if (recover_with_dwim) {
remote = parse_remote_branch(arg, rev, remote = parse_remote_branch(arg, rev,
could_be_checkout_paths, could_be_checkout_paths,
which_command); cb_option);
if (remote) { if (remote) {
*new_branch = arg; *new_branch = arg;
arg = remote; arg = remote;
@ -1754,6 +1735,17 @@ static int checkout_branch(struct checkout_opts *opts,
free(full_ref); 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) { if (!new_branch_info->commit && opts->new_branch) {
struct object_id rev; struct object_id rev;
int flag; int flag;
@ -1851,49 +1843,8 @@ static struct option *add_checkout_path_options(struct checkout_opts *opts,
return newopts; return newopts;
} }


/* create-branch option (either b or c) */ static void init_checkout_opts(struct checkout_opts *opts, const char *prefix)
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)
{ {
int parseopt_flags = 0;
struct branch_info new_branch_info = { 0 };
int ret;

static const char * const checkout_usage[] = {
N_("git checkout [<options>] <branch>"),
N_("git checkout [<options>] [<branch>] -- <file>..."),
NULL,
};

static const char * const switch_branch_usage[] = {
N_("git switch [<options>] [<branch>]"),
NULL,
};

static const char * const restore_usage[] = {
N_("git restore [<options>] [--source=<branch>] <file>..."),
NULL,
};

const char * const *usagestr;

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);
}

opts->overwrite_ignore = 1; opts->overwrite_ignore = 1;
opts->prefix = prefix; opts->prefix = prefix;
opts->show_progress = -1; opts->show_progress = -1;
@ -1905,15 +1856,10 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
} }


opts->track = BRANCH_TRACK_UNSPECIFIED; opts->track = BRANCH_TRACK_UNSPECIFIED;
}


if (!opts->accept_pathspec && !opts->accept_ref) static void validate_path_options(struct checkout_opts *opts)
BUG("make up your mind, you need to take _something_"); {
if (opts->accept_pathspec && opts->accept_ref)
parseopt_flags = PARSE_OPT_KEEP_DASHDASH;

argc = parse_options(argc, argv, prefix, options,
usagestr, parseopt_flags);

if (opts->patch_context < -1) if (opts->patch_context < -1)
die(_("'%s' cannot be negative"), "--unified"); die(_("'%s' cannot be negative"), "--unified");
if (opts->patch_interhunk_context < -1) if (opts->patch_interhunk_context < -1)
@ -1921,13 +1867,36 @@ static int checkout_main(int argc, const char **argv, const char *prefix,


if (!opts->patch_mode) { if (!opts->patch_mode) {
if (opts->patch_context != -1) 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) 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) if (!opts->auto_advance)
die(_("the option '%s' requires '%s'"), "--no-auto-advance", "--patch"); die(_("the option '%s' requires '%s'"),
"--no-auto-advance", "--patch");
} }


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)
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->show_progress < 0) {
if (opts->quiet) if (opts->quiet)
opts->show_progress = 0; opts->show_progress = 0;
@ -1944,45 +1913,61 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
opts->ignore_unmerged_opt = "--force"; opts->ignore_unmerged_opt = "--force";
opts->ignore_unmerged = 1; opts->ignore_unmerged = 1;
} }
}


if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) static void parse_pathspec_from_file_options(struct checkout_opts *opts,
die(_("options '-%c', '-%c', and '%s' cannot be used together"), const char *prefix)
cb_option, toupper(cb_option), "--orphan"); {
if (opts->pathspec_from_file) {
if (opts->pathspec.nr)
die(_("'%s' and pathspec arguments cannot be used together"),
"--pathspec-from-file");


if (opts->overlay_mode == 1 && opts->patch_mode) die_for_incompatible_opt2(!!opts->pathspec_from_file,
die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay"); "--pathspec-from-file",
opts->force_detach,
"--detach");


if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) { die_for_incompatible_opt2(!!opts->pathspec_from_file,
if (opts->checkout_index < 0) "--pathspec-from-file",
opts->checkout_index = 0; opts->patch_mode,
if (opts->checkout_worktree < 0) "--patch");
opts->checkout_worktree = 0;
} else { parse_pathspec_file(&opts->pathspec, 0, 0,
if (opts->checkout_index < 0) prefix, opts->pathspec_from_file,
opts->checkout_index = -opts->checkout_index - 1; opts->pathspec_file_nul);
if (opts->checkout_worktree < 0) } else if (opts->pathspec_file_nul) {
opts->checkout_worktree = -opts->checkout_worktree - 1; die(_("the option '%s' requires '%s'"),
"--pathspec-file-nul", "--pathspec-from-file");
} }
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"
*/
if (!opts->from_treeish && opts->checkout_index)
opts->from_treeish = "HEAD";


/* opts->pathspec.recursive = 1;
* 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. static void validate_branch_options(struct checkout_opts *opts, char cb_option)
*/ {
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) if (opts->new_branch_force)
opts->new_branch = opts->new_branch_force; opts->new_branch = opts->new_branch_force;


if (opts->new_orphan_branch) if (opts->new_orphan_branch)
opts->new_branch = 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 */ /* --track without -c/-C/-b/-B/--orphan should DWIM */
if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) { if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) {
const char *argv0 = argv[0]; const char *argv0 = argv[0];
@ -1995,114 +1980,174 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("missing branch name; try -%c"), cb_option); die(_("missing branch name; try -%c"), cb_option);
opts->new_branch = argv0 + 1; opts->new_branch = argv0 + 1;
} }
}


/* static int setup_branch_name_and_info(int argc, const char **argv,
* Extract branch name from command line arguments, so struct checkout_opts *opts,
* all that is left is pathspecs. struct branch_info *new_branch_info,
* char cb_option)
* Handle {
* validate_branch_options(opts, cb_option);
* 1) git checkout <tree> -- [<paths>] dwim_branch_track_option(argc, argv, opts, cb_option);
* 2) git checkout -- [<paths>]
* 3) git checkout <something> [<paths>] if (argc) {
*
* 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; struct object_id rev;
int dwim_ok = int dwim_ok =
!opts->patch_mode && !opts->patch_mode &&
opts->dwim_new_local_branch && opts->dwim_new_local_branch &&
opts->track == BRANCH_TRACK_UNSPECIFIED && opts->track == BRANCH_TRACK_UNSPECIFIED &&
!opts->new_branch; !opts->new_branch;
int n = parse_branchname_arg(argc, argv, dwim_ok, which_command, return parse_branchname_arg(argc, argv, dwim_ok, cb_option,
&new_branch_info, opts, &rev); new_branch_info, opts, &rev);
argv += n; }
argc -= n; return 0;
} else if (!opts->accept_ref && opts->from_treeish) { }

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 branch_info new_branch_info = { 0 };
int ret;
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 <no-such-branch>'")),
OPT_BOOL(0, "discard-changes", &opts.discard_changes,
N_("throw away local modifications")),
OPT_END()
};

static const char * const switch_branch_usage[] = {
N_("git switch [<options>] [<branch>]"),
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, "<tree-ish>",
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 [<options>] [--source=<branch>] <file>..."),
NULL,
};

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";

init_checkout_opts(&opts, prefix);

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,
restore_usage, 0);

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) {
struct object_id rev; struct object_id rev;


if (repo_get_oid_mb(the_repository, opts->from_treeish, &rev)) if (repo_get_oid_mb(the_repository, opts.from_treeish, &rev))
die(_("could not resolve '%s'"), opts->from_treeish); die(_("could not resolve '%s'"), opts.from_treeish);


setup_new_branch_info_and_source_tree(&new_branch_info, setup_new_branch_info_and_source_tree(&new_branch_info,
opts, &rev, &opts, &rev,
opts->from_treeish); opts.from_treeish);


if (!opts->source_tree) if (!opts.source_tree)
die(_("reference is not a tree: %s"), opts->from_treeish); die(_("reference is not a tree: %s"), opts.from_treeish);
} }


if (argc) { if (argc) {
parse_pathspec(&opts->pathspec, 0, parse_pathspec(&opts.pathspec, 0,
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0, opts.patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
prefix, argv); prefix, argv);


if (!opts->pathspec.nr) if (!opts.pathspec.nr)
die(_("invalid path specification")); 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]);
} }


if (opts->pathspec_from_file) { parse_pathspec_from_file_options(&opts, prefix);
if (opts->pathspec.nr)
die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file");


if (opts->force_detach) if (!opts.pathspec.nr && !opts.patch_mode)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach"); die(_("you must specify path(s) to restore"));


if (opts->patch_mode) ret = checkout_paths(&opts, &new_branch_info);
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;

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->accept_pathspec && !opts->empty_pathspec_ok &&
!opts->patch_mode) /* patch mode is special */
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
ret = checkout_branch(opts, &new_branch_info);


branch_info_release(&new_branch_info); branch_info_release(&new_branch_info);
clear_pathspec(&opts->pathspec); clear_pathspec(&opts.pathspec);
free(opts->pathspec_from_file); free(opts.pathspec_from_file);
free(options); free(options);


return ret; return ret;
@ -2115,20 +2160,30 @@ int cmd_checkout(int argc,
{ {
struct checkout_opts opts = CHECKOUT_OPTS_INIT; struct checkout_opts opts = CHECKOUT_OPTS_INIT;
struct option *options; struct option *options;
struct branch_info new_branch_info = { 0 };
int ret, n;
struct option checkout_options[] = { struct option checkout_options[] = {
OPT_STRING('b', NULL, &opts.new_branch, N_("branch"), OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
N_("create and checkout a new branch")), N_("create and checkout a new branch")),
OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"), OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"),
N_("create/reset and checkout a 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, OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
N_("second guess 'git checkout <no-such-branch>' (default)")), N_("second guess 'git checkout <no-such-branch>' (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, OPT_BOOL(0, "auto-advance", &opts.auto_advance,
N_("auto advance to the next file when selecting hunks interactively")), N_("auto advance to the next file when selecting hunks interactively")),
OPT_END() OPT_END()
}; };


static const char * const checkout_usage[] = {
N_("git checkout [<options>] <branch>"),
N_("git checkout [<options>] [<branch>] -- <file>..."),
NULL,
};

opts.dwim_new_local_branch = 1; opts.dwim_new_local_branch = 1;
opts.switch_branch_doing_nothing_is_ok = 1; opts.switch_branch_doing_nothing_is_ok = 1;
opts.only_merge_on_switching_branches = 0; opts.only_merge_on_switching_branches = 0;
@ -2151,86 +2206,55 @@ int cmd_checkout(int argc,
opts.only_merge_on_switching_branches = 1; opts.only_merge_on_switching_branches = 1;
} }


init_checkout_opts(&opts, prefix);

options = parse_options_dup(checkout_options); options = parse_options_dup(checkout_options);
options = add_common_options(&opts, options); options = add_common_options(&opts, options);
options = add_common_switch_branch_options(&opts, options); options = add_common_switch_branch_options(&opts, options);
options = add_checkout_path_options(&opts, options); options = add_checkout_path_options(&opts, options);


return checkout_main(argc, argv, prefix, &opts, options, argc = parse_options(argc, argv, prefix, options,
CHECKOUT_CHECKOUT); checkout_usage, PARSE_OPT_KEEP_DASHDASH);
}

validate_path_options(&opts);
int cmd_switch(int argc, prepare_common_options(&opts);
const char **argv,
const char *prefix, n = setup_branch_name_and_info(argc, argv, &opts, &new_branch_info, 'b');
struct repository *repo UNUSED) argv += n;
{ argc -= n;
struct checkout_opts opts = CHECKOUT_OPTS_INIT;
struct option *options = NULL; if (argc) {
struct option switch_options[] = { parse_pathspec(&opts.pathspec, 0,
OPT_STRING('c', "create", &opts.new_branch, N_("branch"), opts.patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
N_("create and switch to a new branch")), prefix, argv);
OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"),
N_("create/reset and switch to a branch")), if (!opts.pathspec.nr)
OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, die(_("invalid path specification"));
N_("second guess 'git switch <no-such-branch>'")),
OPT_BOOL(0, "discard-changes", &opts.discard_changes, /*
N_("throw away local modifications")), * Try to give more helpful suggestion.
OPT_END() * new_branch && argc > 1 will be caught later.
}; */

if (opts.new_branch && argc == 1 && !new_branch_info.commit)
opts.dwim_new_local_branch = 1; die(_("'%s' is not a commit and a branch '%s' cannot be created from it"),
opts.accept_ref = 1; argv[0], opts.new_branch);
opts.accept_pathspec = 0;
opts.switch_branch_doing_nothing_is_ok = 0; if (opts.force_detach)
opts.only_merge_on_switching_branches = 1; die(_("git checkout: --detach does not take a path argument '%s'"),
opts.implicit_detach = 0; argv[0]);
opts.can_switch_when_in_progress = 0; }
opts.orphan_from_empty_tree = 1;
opts.overlay_mode = -1; parse_pathspec_from_file_options(&opts, prefix);


options = parse_options_dup(switch_options); if (opts.patch_mode || opts.pathspec.nr)
options = add_common_options(&opts, options); ret = checkout_paths(&opts, &new_branch_info);
options = add_common_switch_branch_options(&opts, options); else

ret = checkout_branch(&opts, &new_branch_info);
cb_option = 'c';

branch_info_release(&new_branch_info);
return checkout_main(argc, argv, prefix, &opts, options, clear_pathspec(&opts.pathspec);
CHECKOUT_SWITCH); free(opts.pathspec_from_file);
} free(options);


int cmd_restore(int argc, return ret;
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, "<tree-ish>",
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);
} }

View File

@ -1,6 +1,9 @@
#define USE_THE_REPOSITORY_VARIABLE #define USE_THE_REPOSITORY_VARIABLE


#include "git-compat-util.h" #include "git-compat-util.h"
#include "commit.h"
#include "hex.h"
#include "hook.h"
#include "object-name.h" #include "object-name.h"
#include "remote.h" #include "remote.h"
#include "refspec.h" #include "refspec.h"
@ -9,6 +12,7 @@
#include "config.h" #include "config.h"
#include "strbuf.h" #include "strbuf.h"
#include "string-list.h" #include "string-list.h"
#include "strvec.h"


struct tracking_name_data { struct tracking_name_data {
/* const */ char *src_ref; /* const */ char *src_ref;
@ -82,3 +86,27 @@ char *unique_tracking_name(const char *name, struct object_id *oid,
} }
return NULL; 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);
}

View File

@ -3,7 +3,9 @@


#include "hash.h" #include "hash.h"


struct commit;
struct string_list; struct string_list;
struct repository;


/* /*
* Check if the branch name uniquely matches a branch name on a remote * Check if the branch name uniquely matches a branch name on a remote
@ -15,4 +17,11 @@ char *unique_tracking_name(const char *name,
int *dwim_remotes_matched, int *dwim_remotes_matched,
struct string_list *dwim_remote_names); struct string_list *dwim_remote_names);


/*
* Run the post-checkout hook.
*/
int post_checkout_hook(struct repository *,
struct commit *old_commit, struct commit *new_commit,
int changed);

#endif /* CHECKOUT_H */ #endif /* CHECKOUT_H */