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

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).
@ -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"),
"--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 = {
@ -714,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;
}

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


enum checkout_command {
CHECKOUT_CHECKOUT = 1,
CHECKOUT_SWITCH = 2,
CHECKOUT_RESTORE = 3,
};

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

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 advise_disambiguating_remotes",
which_command);
BUG("command <%c> should not reach advise_disambiguating_remotes",
cb_option);
break;
}

@ -1384,7 +1365,7 @@ static void advise_disambiguating_remotes(enum checkout_command which_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;
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 (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME))
advise_disambiguating_remotes(which_command, arg,
advise_disambiguating_remotes(cb_option, arg,
&matched_remote_names);
die(_("'%s' matched multiple (%d) remote tracking branches"),
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,
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)
@ -1524,7 +1505,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;
@ -1754,6 +1735,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;
@ -1851,49 +1843,8 @@ 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)
static void init_checkout_opts(struct checkout_opts *opts, const char *prefix)
{
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->prefix = prefix;
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;
}

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;

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

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)
@ -1921,13 +1867,36 @@ static int checkout_main(int argc, const char **argv, const char *prefix,

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

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->quiet)
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 = 1;
}
}

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

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;
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);
} else if (opts->pathspec_file_nul) {
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";

/*
* 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.
*/
opts->pathspec.recursive = 1;
}

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)
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];
@ -1995,114 +1980,174 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
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 <tree> -- [<paths>]
* 2) git checkout -- [<paths>]
* 3) git checkout <something> [<paths>]
*
* including "last branch" syntax and DWIM-ery for names of
* remote branches, erroring out for invalid or ambiguous cases.
*/
if (argc && opts->accept_ref) {
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;
int n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
&new_branch_info, opts, &rev);
argv += n;
argc -= n;
} else if (!opts->accept_ref && opts->from_treeish) {
return parse_branchname_arg(argc, argv, dwim_ok, cb_option,
new_branch_info, opts, &rev);
}
return 0;
}

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;

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

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

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;

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 */
if (!opts.pathspec.nr && !opts.patch_mode)
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);
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;
@ -2115,20 +2160,30 @@ 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")),
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 <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,
N_("auto advance to the next file when selecting hunks interactively")),
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.switch_branch_doing_nothing_is_ok = 1;
opts.only_merge_on_switching_branches = 0;
@ -2151,86 +2206,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 <no-such-branch>'")),
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);

cb_option = 'c';

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

View File

@ -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"
@ -9,6 +12,7 @@
#include "config.h"
#include "strbuf.h"
#include "string-list.h"
#include "strvec.h"

struct tracking_name_data {
/* const */ char *src_ref;
@ -82,3 +86,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);
}

View File

@ -3,7 +3,9 @@

#include "hash.h"

struct commit;
struct string_list;
struct repository;

/*
* 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,
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 */