Subject: Re: [PATCH v4 5/5] stash: implement builtin stash

From: Junio C Hamano <[email-masked]>
Date: Thu, 22 Jun 2017 10:07:03 -0700

Joel Teichroeb <[email-masked]> writes:

> +static int patch_working_tree(struct stash_info *info, const char *prefix,
> +		const char **argv)
> +{
> +	struct argv_array args = ARGV_ARRAY_INIT;
> +	struct child_process cp = CHILD_PROCESS_INIT;
> +	struct strbuf out = STRBUF_INIT;
> +	size_t unused;
> +	const char *index_file = get_index_file();
> +
> +	argv_array_pushl(&args, "read-tree", "HEAD", NULL);
> +	argv_array_pushf(&args, "--index-output=%s", stash_index_path);
> +	cmd_read_tree(args.argc, args.argv, prefix);

I do not think if cmd_read_tree() is prepared to be called like
this, and even if it happens to be OK, I do not think we should rely
on it.  

In general, cmd_foo() that implements subcommand "foo" expects only
to be called from main(), and expects that the calling main() will
exit with its return status.  This has implications that you, who
abuse a cmd_foo() function as if it is a reusable helper function,
need to watch out for.  For example, cmd_foo() may use static global
variables in builtin/foo.c that are initialized in a certain way
before it starts, so calling cmd_foo() twice may not work correctly
(the first invocation may change these variables and they won't be
reset when it returns).  cmd_foo() can and do leave resources
unreclaimed, because it expects the calling main() to exit
immediately, leaving descriptors it creates open or chunks of memory
it allocates unfreed.  cmd_foo() also can and do die() without
returning the control to the caller (this last item does not make
much difference to this particular codepath, as you'd end up dying
soon if this read-tree fails anyway, but in general you'd want to
give a more specific error message when it happens, i.e. instead of
an error message cmd_read_tree() internally gives, you want to say
"Cannot save the current worktree state").

> +
> +	cp.git_cmd = 1;
> +	argv_array_pushl(&cp.args, "add--interactive", "--patch=stash", "--", NULL);
> +	argv_array_pushf(&cp.env_array, "GIT_INDEX_FILE=%s", stash_index_path);
> +	if (run_command(&cp))
> +		return 1;

Unlike the above direct call to cmd_read_tree(), the way this
invokes "git add--interactive" is kosher.  By returning non-zero (by
the way, the prevailing convention in this codebase is to return
negative for an error), you give a chance to the caller of this
helper function to say "Cannot save the current worktree state".

> +	discard_cache();
> +	read_cache_from(stash_index_path);
> +
> +	if (write_cache_as_tree(info->w_tree.hash, 0, NULL))
> +		return 1;

OK.

> +	child_process_init(&cp);
> +	cp.git_cmd = 1;
> +	argv_array_pushl(&cp.args, "diff-tree", "-p", "HEAD", NULL);
> +	argv_array_push(&cp.args, sha1_to_hex(info->w_tree.hash));
> +	argv_array_push(&cp.args, "--");
> +	if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) || out.len == 0)
> +		return 1;

This "diff-tree" call is also reasonable.  Instead of getting the
patch text into a temporary file (which is what the original did),
we slurp it into a strbuf "out", and pass it out to the caller by
storing in info->patch.  OK.

> +	info->patch = strbuf_detach(&out, &unused);

You can pass NULL instead of a throw-away variable &unused.

> +
> +	set_alternate_index_output(index_file);
> +	discard_cache();
> +	read_cache();
> +
> +	return 0;
> +}

So this looks fairly faithful rewrite to C of a half of the
create_stash shell function (we already reviewed the other half done
in your save_working_tree() function).

> +static int do_create_stash(struct stash_info *info, const char *prefix,
> +		const char *message, int include_untracked, int include_ignored,
> +		int patch, const char **argv)
> +{
> +	struct object_id curr_head;
> +	char *branch_path = NULL;
> +	const char *branch_name = NULL;
> +	struct commit_list *parents = NULL;
> +	struct strbuf out_message = STRBUF_INIT;
> +	struct strbuf out = STRBUF_INIT;
> +	struct pretty_print_context ctx = {0};
> +
> +	struct commit *c = NULL;
> +	const char *hash;
> +
> +	read_cache_preload(NULL);
> +	refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL);
> +	if (check_no_changes(prefix, include_untracked, include_ignored, argv))
> +		return 1;

We find there is nothing to stash, so we tell the caller that fact
by returning 1.  The caller can tell that this is a "different kind
of success" and is not an error by checking the sign of the return
value.

> +	if (get_sha1_tree("HEAD", info->b_commit.hash))
> +		return error(_("You do not have the initial commit yet"));

And this is an error from the caller's point of view (error()
returns -1).

> +	branch_path = resolve_refdup("HEAD", 0, curr_head.hash, NULL);
> +
> +	if (branch_path == NULL || strcmp(branch_path, "HEAD") == 0)
> +		branch_name = "(no branch)";
> +	else
> +		skip_prefix(branch_path, "refs/heads/", &branch_name);
> +
> +	c = lookup_commit(&info->b_commit);
> +
> +	ctx.output_encoding = get_log_output_encoding();
> +	ctx.abbrev = 1;
> +	ctx.fmt = CMIT_FMT_ONELINE;
> +	hash = find_unique_abbrev(c->object.oid.hash, DEFAULT_ABBREV);
> +
> +	strbuf_addf(&out_message, "%s: %s ", branch_name, hash);
> +
> +	pretty_print_commit(&ctx, c, &out_message);
> +
> +	strbuf_addf(&out, "index on %s\n", out_message.buf);

OK, the above roughly correspond to "# state of the base commit"
part of the original.  This message is created with

	msg=$(printf '%s: %s' "$branch" "$head")

and your "branch_name" is computed to be the same as $branch, and
$head in the original is head=$(git rev-list --oneline -n 1 HEAD--)
which is your "hash" with the oneline.  The whole $msg corresponds
to your "out_message.buf".  Looks correct.

> +	commit_list_insert(lookup_commit(&info->b_commit), &parents);
> +
> +	if (write_cache_as_tree(info->i_tree.hash, 0, NULL))
> +		return error(_("git write-tree failed to write a tree"));

Shouldn't the user also see "Cannot save the current index state"
message in this case?  A failure by write-tree is an implementation
detail and the latter is what matters more to the end user.

> +	if (commit_tree(out.buf, out.len, info->i_tree.hash, parents, info->i_commit.hash, NULL, NULL))
> +		return error(_("Cannot save the current index state"));
> +
> +	strbuf_reset(&out);
> +
> +	if (include_untracked) {
> +		if (save_untracked(info, out_message.buf, include_untracked, include_ignored, argv))
> +			return error(_("Cannot save the untracked files"));
> +	}
> +
> +	if (patch) {
> +		if (patch_working_tree(info, prefix, argv))
> +			return error(_("Cannot save the current worktree state"));
> +	} else {
> +		if (save_working_tree(info, prefix, argv))
> +			return error(_("Cannot save the current worktree state"));
> +	}
> +	parents = NULL;

The elements on the parents list here are leaked here, by early
returns we see above and also at the end of this function.

> +	if (include_untracked)
> +		commit_list_insert(lookup_commit(&info->u_commit), &parents);
> +
> +	commit_list_insert(lookup_commit(&info->i_commit), &parents);
> +	commit_list_insert(lookup_commit(&info->b_commit), &parents);
> +
> +	if (message != NULL && strlen(message) != 0)
> +		strbuf_addf(&out, "On %s: %s\n", branch_name, message);
> +	else
> +		strbuf_addf(&out, "WIP on %s\n", out_message.buf);
> +
> +	if (commit_tree(out.buf, out.len, info->w_tree.hash, parents, info->w_commit.hash, NULL, NULL))
> +		return error(_("Cannot record working tree state"));
> +
> +	info->message = out.buf;
> +
> +	strbuf_release(&out_message);
> +	free(branch_path);
> +
> +	return 0;
> +}

> +static int create_stash(int argc, const char **argv, const char *prefix)
> +{
> +	int include_untracked = 0;
> +	const char *message = NULL;
> +	struct stash_info info;
> +	int ret;
> +	struct strbuf out = STRBUF_INIT;
> +	struct option options[] = {
> +		OPT_BOOL('u', "include-untracked", &include_untracked,
> +			N_("stash untracked filed")),
> +		OPT_STRING('m', "message", &message, N_("message"),
> +			N_("stash commit message")),
> +		OPT_END()
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, options,
> +			git_stash_create_usage, 0);
> +
> +	if (argc != 0) {
> +		int i;
> +		for (i = 0; i < argc; ++i) {
> +			if (i != 0) {
> +				strbuf_addf(&out, " ");
> +			}

Style tips:
    Do not enclose a single statement inside a block.  
    We prefer "if (i)" over "if (i != 0)".
    We prefer "if (!i)" over "if (i == 0)".
    We prefer "i++" over "++i", UNLESS you use the value before increment.

> +			strbuf_addf(&out, "%s", argv[i]);
> +		}
> +		message = out.buf;
> +	}
> +
> +	ret = do_create_stash(&info, prefix, message, include_untracked, 0, 0, NULL);
> +
> +	strbuf_release(&out);

I do not see a need for "message" variable; you can just pass
out.buf to do_create_stash().

> +
> +	if (ret)
> +		return 0;
> +
> +	printf("%s\n", sha1_to_hex(info.w_commit.hash));
> +	return 0;
> +}