Re: [RFC] Introduce two new commands, switch-branch and restore-paths

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On 11/20, Duy Nguyen wrote:
> On Mon, Nov 19, 2018 at 04:19:53PM +0100, Duy Nguyen wrote:
> > I promise to come back with something better (at least it still
> > sounds better in my mind). If that idea does not work out, we can
> > come back and see if we can improve this.
> 
> So this is it. The patch isn't pretty, mostly as a proof of
> concept. Just look at the three functions at the bottom of checkout.c,
> which is the main thing.
> 
> This patch tries to split "git checkout" command in two new ones:
> 
> - git switch-branch is all about switching branches
> - git restore-paths (maybe restore-file is better) for checking out
>   paths
> 
> The main idea is these two commands will co-exist with the good old
> 'git checkout', which will NOT be deprecated. Old timers will still
> use "git checkout". But new people should be introduced to the new two
> instead. And the new ones are just as capable as "git checkout".
> 
> Since the three commands will co-exist (with duplicate functionality),
> maintenance cost must be kept to minimum. The way I did this is simply
> split the command line options into three pieces: common,
> switch-branch and checkout-paths. "git checkout" has all three, the
> other two have common and another piece.
>
> With this, a new option added to git checkout will be automatically
> available in either switch-branch or checkout-paths. Bug fixes apply
> to all relevant commands.
> 
> Later on, we could start to add a bit more stuff in, e.g. some form of
> disambiguation is no longer needed when running as switch-branch, or
> restore-paths.
> 
> So, what do you think?

I like the idea of splitting those commands up, in fact it is
something I've been considering working on myself.  I do think we
should consider if we want to change the behaviour of those new
commands in any way compared to 'git checkout', since we're starting
with a clean slate.

One thing in particular that I have in mind is something I'm currently
working on, namely adding a --index flag to 'git checkout', which
would make 'git checkout' work in non-overlay mode (for more
discussion on that see also [*1*].  I got something working, that
needs to be polished a bit and am hoping to send that to the list
sometime soon.

I wonder if such the --index behaviour could be the default in
restore-paths command?

Most of the underlying machinery for 'checkout' could and should of
course still be shared between the commands.

*1*: <xmqq4loqplou.fsf@xxxxxxxxxxxxxxxxxxxxxxxxxxx>

> -- 8< --
> diff --git a/builtin.h b/builtin.h
> index 6538932e99..6e321ec8a4 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -214,6 +214,7 @@ extern int cmd_remote_fd(int argc, const char **argv, const char *prefix);
>  extern int cmd_repack(int argc, const char **argv, const char *prefix);
>  extern int cmd_rerere(int argc, const char **argv, const char *prefix);
>  extern int cmd_reset(int argc, const char **argv, const char *prefix);
> +extern int cmd_restore_paths(int argc, const char **argv, const char *prefix);
>  extern int cmd_rev_list(int argc, const char **argv, const char *prefix);
>  extern int cmd_rev_parse(int argc, const char **argv, const char *prefix);
>  extern int cmd_revert(int argc, const char **argv, const char *prefix);
> @@ -227,6 +228,7 @@ extern int cmd_show_index(int argc, const char **argv, const char *prefix);
>  extern int cmd_status(int argc, const char **argv, const char *prefix);
>  extern int cmd_stripspace(int argc, const char **argv, const char *prefix);
>  extern int cmd_submodule__helper(int argc, const char **argv, const char *prefix);
> +extern int cmd_switch_branch(int argc, const char **argv, const char *prefix);
>  extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix);
>  extern int cmd_tag(int argc, const char **argv, const char *prefix);
>  extern int cmd_tar_tree(int argc, const char **argv, const char *prefix);
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index acdafc6e4c..868ca3c223 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -33,6 +33,16 @@ static const char * const checkout_usage[] = {
>  	NULL,
>  };
>  
> +static const char * const switch_branch_usage[] = {
> +	N_("git switch-branch [<options>] <branch>"),
> +	NULL,
> +};
> +
> +static const char * const restore_paths_usage[] = {
> +	N_("git restore-paths [<options>] [<branch>] -- <file>..."),
> +	NULL,
> +};
> +
>  struct checkout_opts {
>  	int patch_mode;
>  	int quiet;
> @@ -44,6 +54,7 @@ struct checkout_opts {
>  	int ignore_skipworktree;
>  	int ignore_other_worktrees;
>  	int show_progress;
> +	int dwim_new_local_branch;
>  	/*
>  	 * If new checkout options are added, skip_merge_working_tree
>  	 * should be updated accordingly.
> @@ -55,6 +66,7 @@ struct checkout_opts {
>  	int new_branch_log;
>  	enum branch_track track;
>  	struct diff_options diff_options;
> +	char *conflict_style;
>  
>  	int branch_exists;
>  	const char *prefix;
> @@ -1223,78 +1235,105 @@ static int checkout_branch(struct checkout_opts *opts,
>  	return switch_branches(opts, new_branch_info);
>  }
>  
> -int cmd_checkout(int argc, const char **argv, const char *prefix)
> +static struct option *add_common_options(struct checkout_opts *opts,
> +					 struct option *prevopts)
>  {
> -	struct checkout_opts opts;
> -	struct branch_info new_branch_info;
> -	char *conflict_style = NULL;
> -	int dwim_new_local_branch = 1;
> -	int dwim_remotes_matched = 0;
>  	struct option options[] = {
> -		OPT__QUIET(&opts.quiet, N_("suppress progress reporting")),
> -		OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
> +		OPT__QUIET(&opts->quiet, N_("suppress progress reporting")),
> +		OPT_BOOL(0, "ignore-skip-worktree-bits", &opts->ignore_skipworktree,
> +			 N_("do not limit pathspecs to sparse entries only")),
> +		{ OPTION_CALLBACK, 0, "recurse-submodules", NULL,
> +			    "checkout", "control recursive updating of submodules",
> +			    PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater },
> +		OPT_BOOL(0, "progress", &opts->show_progress, N_("force progress reporting")),
> +		OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"),
> +			   PARSE_OPT_NOCOMPLETE),
> +		OPT_STRING(0, "conflict", &opts->conflict_style, N_("style"),
> +			   N_("conflict style (merge or diff3)")),
> +		OPT_END()
> +	};
> +	struct option *newopts = parse_options_concat(prevopts, options);
> +	free(prevopts);
> +	return newopts;
> +}
> +
> +static struct option *add_switch_branch_options(struct checkout_opts *opts,
> +						struct option *prevopts)
> +{
> +	struct option 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"),
> +		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(0, "detach", &opts.force_detach, N_("detach HEAD at named commit")),
> -		OPT_SET_INT('t', "track",  &opts.track, N_("set upstream info for new branch"),
> +		OPT_BOOL('l', NULL, &opts->new_branch_log, N_("create reflog for new branch")),
> +		OPT_BOOL(0, "detach", &opts->force_detach, N_("detach HEAD at named commit")),
> +		OPT_SET_INT('t', "track",  &opts->track, N_("set upstream info for new branch"),
>  			BRANCH_TRACK_EXPLICIT),
> -		OPT_STRING(0, "orphan", &opts.new_orphan_branch, N_("new-branch"), N_("new unparented branch")),
> -		OPT_SET_INT_F('2', "ours", &opts.writeout_stage,
> +		OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unparented branch")),
> +		OPT_BOOL('m', "merge", &opts->merge, N_("perform a 3-way merge with the new branch")),
> +		OPT_HIDDEN_BOOL(0, "guess", &opts->dwim_new_local_branch,
> +				N_("second guess 'git checkout <no-such-branch>'")),
> +		OPT_BOOL(0, "ignore-other-worktrees", &opts->ignore_other_worktrees,
> +			 N_("do not check if another worktree is holding the given ref")),
> +		OPT_END()
> +	};
> +	struct option *newopts = parse_options_concat(prevopts, options);
> +	free(prevopts);
> +	return newopts;
> +}
> +
> +static struct option *add_checkout_path_options(struct checkout_opts *opts,
> +						struct option *prevopts)
> +{
> +	struct option options[] = {
> +		OPT_SET_INT_F('2', "ours", &opts->writeout_stage,
>  			      N_("checkout our version for unmerged files"),
>  			      2, PARSE_OPT_NONEG),
> -		OPT_SET_INT_F('3', "theirs", &opts.writeout_stage,
> +		OPT_SET_INT_F('3', "theirs", &opts->writeout_stage,
>  			      N_("checkout their version for unmerged files"),
>  			      3, PARSE_OPT_NONEG),
> -		OPT__FORCE(&opts.force, N_("force checkout (throw away local modifications)"),
> -			   PARSE_OPT_NOCOMPLETE),
> -		OPT_BOOL('m', "merge", &opts.merge, N_("perform a 3-way merge with the new branch")),
> -		OPT_BOOL_F(0, "overwrite-ignore", &opts.overwrite_ignore,
> -			   N_("update ignored files (default)"),
> -			   PARSE_OPT_NOCOMPLETE),
> -		OPT_STRING(0, "conflict", &conflict_style, N_("style"),
> -			   N_("conflict style (merge or diff3)")),
> -		OPT_BOOL('p', "patch", &opts.patch_mode, N_("select hunks interactively")),
> -		OPT_BOOL(0, "ignore-skip-worktree-bits", &opts.ignore_skipworktree,
> -			 N_("do not limit pathspecs to sparse entries only")),
> -		OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
> -				N_("second guess 'git checkout <no-such-branch>'")),
> -		OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees,
> -			 N_("do not check if another worktree is holding the given ref")),
> -		{ OPTION_CALLBACK, 0, "recurse-submodules", NULL,
> -			    "checkout", "control recursive updating of submodules",
> -			    PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater },
> -		OPT_BOOL(0, "progress", &opts.show_progress, N_("force progress reporting")),
> -		OPT_END(),
> +		OPT_BOOL('p', "patch", &opts->patch_mode, N_("select hunks interactively")),
> +		OPT_END()
>  	};
> +	struct option *newopts = parse_options_concat(prevopts, options);
> +	free(prevopts);
> +	return newopts;
> +}
>  
> -	memset(&opts, 0, sizeof(opts));
> +static int checkout_main(int argc, const char **argv, const char *prefix,
> +			 struct checkout_opts *opts, struct option *options,
> +			 const char * const usagestr[])
> +{
> +	struct branch_info new_branch_info;
> +	int dwim_remotes_matched = 0;
> +
> +	memset(opts, 0, sizeof(*opts));
> +	opts->dwim_new_local_branch = 1;
>  	memset(&new_branch_info, 0, sizeof(new_branch_info));
> -	opts.overwrite_ignore = 1;
> -	opts.prefix = prefix;
> -	opts.show_progress = -1;
> +	opts->overwrite_ignore = 1;
> +	opts->prefix = prefix;
> +	opts->show_progress = -1;
>  
> -	git_config(git_checkout_config, &opts);
> +	git_config(git_checkout_config, opts);
>  
> -	opts.track = BRANCH_TRACK_UNSPECIFIED;
> +	opts->track = BRANCH_TRACK_UNSPECIFIED;
>  
> -	argc = parse_options(argc, argv, prefix, options, checkout_usage,
> +	argc = parse_options(argc, argv, prefix, options, usagestr,
>  			     PARSE_OPT_KEEP_DASHDASH);
>  
> -	if (opts.show_progress < 0) {
> -		if (opts.quiet)
> -			opts.show_progress = 0;
> +	if (opts->show_progress < 0) {
> +		if (opts->quiet)
> +			opts->show_progress = 0;
>  		else
> -			opts.show_progress = isatty(2);
> +			opts->show_progress = isatty(2);
>  	}
>  
> -	if (conflict_style) {
> -		opts.merge = 1; /* implied */
> -		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
> +	if (opts->conflict_style) {
> +		opts->merge = 1; /* implied */
> +		git_xmerge_config("merge.conflictstyle", opts->conflict_style, NULL);
>  	}
>  
> -	if ((!!opts.new_branch + !!opts.new_branch_force + !!opts.new_orphan_branch) > 1)
> +	if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1)
>  		die(_("-b, -B and --orphan are mutually exclusive"));
>  
>  	/*
> @@ -1302,14 +1341,14 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
>  	 * and new_branch_force and new_orphan_branch will tell us which one of
>  	 * -b/-B/--orphan is being used.
>  	 */
> -	if (opts.new_branch_force)
> -		opts.new_branch = opts.new_branch_force;
> +	if (opts->new_branch_force)
> +		opts->new_branch = opts->new_branch_force;
>  
> -	if (opts.new_orphan_branch)
> -		opts.new_branch = opts.new_orphan_branch;
> +	if (opts->new_orphan_branch)
> +		opts->new_branch = opts->new_orphan_branch;
>  
>  	/* --track without -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];
>  		if (!argc || !strcmp(argv0, "--"))
>  			die(_("--track needs a branch name"));
> @@ -1318,7 +1357,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
>  		argv0 = strchr(argv0, '/');
>  		if (!argv0 || !argv0[1])
>  			die(_("missing branch name; try -b"));
> -		opts.new_branch = argv0 + 1;
> +		opts->new_branch = argv0 + 1;
>  	}
>  
>  	/*
> @@ -1337,56 +1376,56 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
>  	if (argc) {
>  		struct object_id rev;
>  		int dwim_ok =
> -			!opts.patch_mode &&
> -			dwim_new_local_branch &&
> -			opts.track == BRANCH_TRACK_UNSPECIFIED &&
> -			!opts.new_branch;
> +			!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,
> -					     &new_branch_info, &opts, &rev,
> +					     &new_branch_info, opts, &rev,
>  					     &dwim_remotes_matched);
>  		argv += n;
>  		argc -= n;
>  	}
>  
>  	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)
> +		if (opts->new_branch && argc == 1)
>  			die(_("'%s' is not a commit and a branch '%s' cannot be created from it"),
> -				argv[0], opts.new_branch);
> +				argv[0], opts->new_branch);
>  
> -		if (opts.force_detach)
> +		if (opts->force_detach)
>  			die(_("git checkout: --detach does not take a path argument '%s'"),
>  			    argv[0]);
>  
> -		if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
> +		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.new_branch) {
> +	if (opts->new_branch) {
>  		struct strbuf buf = STRBUF_INIT;
>  
> -		if (opts.new_branch_force)
> -			opts.branch_exists = validate_branchname(opts.new_branch, &buf);
> +		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);
> +			opts->branch_exists =
> +				validate_new_branchname(opts->new_branch, &buf, 0);
>  		strbuf_release(&buf);
>  	}
>  
>  	UNLEAK(opts);
> -	if (opts.patch_mode || opts.pathspec.nr) {
> -		int ret = checkout_paths(&opts, new_branch_info.name);
> +	if (opts->patch_mode || opts->pathspec.nr) {
> +		int ret = checkout_paths(opts, new_branch_info.name);
>  		if (ret && dwim_remotes_matched > 1 &&
>  		    advice_checkout_ambiguous_remote_branch_name)
>  			advise(_("'%s' matched more than one remote tracking branch.\n"
> @@ -1405,6 +1444,49 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
>  			       dwim_remotes_matched);
>  		return ret;
>  	} else {
> -		return checkout_branch(&opts, &new_branch_info);
> +		return checkout_branch(opts, &new_branch_info);
>  	}
>  }
> +
> +int cmd_checkout(int argc, const char **argv, const char *prefix)
> +{
> +	struct checkout_opts opts;
> +	struct option *options = NULL;
> +	int ret;
> +
> +	options = add_common_options(&opts, options);
> +	options = add_switch_branch_options(&opts, options);
> +	options = add_checkout_path_options(&opts, options);
> +	ret = checkout_main(argc, argv, prefix, &opts,
> +			    options, checkout_usage);
> +	FREE_AND_NULL(options);
> +	return ret;
> +}
> +
> +int cmd_switch_branch(int argc, const char **argv, const char *prefix)
> +{
> +	struct checkout_opts opts;
> +	struct option *options = NULL;
> +	int ret;
> +
> +	options = add_common_options(&opts, options);
> +	options = add_switch_branch_options(&opts, options);
> +	ret = checkout_main(argc, argv, prefix, &opts,
> +			    options, switch_branch_usage);
> +	FREE_AND_NULL(options);
> +	return ret;
> +}
> +
> +int cmd_restore_paths(int argc, const char **argv, const char *prefix)
> +{
> +	struct checkout_opts opts;
> +	struct option *options = NULL;
> +	int ret;
> +
> +	options = add_common_options(&opts, options);
> +	options = add_checkout_path_options(&opts, options);
> +	ret = checkout_main(argc, argv, prefix, &opts,
> +			    options, restore_paths_usage);
> +	FREE_AND_NULL(options);
> +	return ret;
> +}
> diff --git a/git.c b/git.c
> index 2f604a41ea..e8a76a99da 100644
> --- a/git.c
> +++ b/git.c
> @@ -542,6 +542,7 @@ static struct cmd_struct commands[] = {
>  	{ "replace", cmd_replace, RUN_SETUP },
>  	{ "rerere", cmd_rerere, RUN_SETUP },
>  	{ "reset", cmd_reset, RUN_SETUP },
> +	{ "restore-paths", cmd_restore_paths, RUN_SETUP | NEED_WORK_TREE },
>  	{ "rev-list", cmd_rev_list, RUN_SETUP | NO_PARSEOPT },
>  	{ "rev-parse", cmd_rev_parse, NO_PARSEOPT },
>  	{ "revert", cmd_revert, RUN_SETUP | NEED_WORK_TREE },
> @@ -557,6 +558,7 @@ static struct cmd_struct commands[] = {
>  	{ "status", cmd_status, RUN_SETUP | NEED_WORK_TREE },
>  	{ "stripspace", cmd_stripspace },
>  	{ "submodule--helper", cmd_submodule__helper, RUN_SETUP | SUPPORT_SUPER_PREFIX | NO_PARSEOPT },
> +	{ "switch-branch", cmd_switch_branch, RUN_SETUP | NEED_WORK_TREE },
>  	{ "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
>  	{ "tag", cmd_tag, RUN_SETUP | DELAY_PAGER_CONFIG },
>  	{ "unpack-file", cmd_unpack_file, RUN_SETUP | NO_PARSEOPT },
> diff --git a/parse-options-cb.c b/parse-options-cb.c
> index 8c9edce52f..c609d52926 100644
> --- a/parse-options-cb.c
> +++ b/parse-options-cb.c
> @@ -126,7 +126,7 @@ struct option *parse_options_concat(struct option *a, struct option *b)
>  	struct option *ret;
>  	size_t i, a_len = 0, b_len = 0;
>  
> -	for (i = 0; a[i].type != OPTION_END; i++)
> +	for (i = 0; a && a[i].type != OPTION_END; i++)
>  		a_len++;
>  	for (i = 0; b[i].type != OPTION_END; i++)
>  		b_len++;
> -- 8< --



[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux