Merge branch 'dl/merge-autostash'

"git merge" learns the "--autostash" option.

* dl/merge-autostash: (22 commits)
  pull: pass --autostash to merge
  t5520: make test_pull_autostash() accept expect_parent_num
  merge: teach --autostash option
  sequencer: implement apply_autostash_oid()
  sequencer: implement save_autostash()
  sequencer: unlink autostash in apply_autostash()
  sequencer: extract perform_autostash() from rebase
  rebase: generify create_autostash()
  rebase: extract create_autostash()
  reset: extract reset_head() from rebase
  rebase: generify reset_head()
  rebase: use apply_autostash() from sequencer.c
  sequencer: rename stash_sha1 to stash_oid
  sequencer: make apply_autostash() accept a path
  rebase: use read_oneliner()
  sequencer: make read_oneliner() extern
  sequencer: configurably warn on non-existent files
  sequencer: make read_oneliner() accept flags
  sequencer: make file exists check more efficient
  sequencer: stop leaking buf
  ...
This commit is contained in:
Junio C Hamano 2020-04-29 16:15:27 -07:00
commit bf10200871
20 changed files with 666 additions and 391 deletions

View File

@ -70,6 +70,16 @@ merge.stat::
Whether to print the diffstat between ORIG_HEAD and the merge result
at the end of the merge. True by default.
merge.autoStash::
When set to true, automatically create a temporary stash entry
before the operation begins, and apply it after the operation
ends. This means that you can run merge on a dirty worktree.
However, use with care: the final stash application after a
successful merge might result in non-trivial conflicts.
This option can be overridden by the `--no-autostash` and
`--autostash` options of linkgit:git-merge[1].
Defaults to false.
merge.tool::
Controls which merge tool is used by linkgit:git-mergetool[1].
The list below shows the valid built-in values.

View File

@ -94,7 +94,8 @@ will be appended to the specified message.
--abort::
Abort the current conflict resolution process, and
try to reconstruct the pre-merge state.
try to reconstruct the pre-merge state. If an autostash entry is
present, apply it to the worktree.
+
If there were uncommitted worktree changes present when the merge
started, 'git merge --abort' will in some cases be unable to
@ -102,11 +103,15 @@ reconstruct these changes. It is therefore recommended to always
commit or stash your changes before running 'git merge'.
+
'git merge --abort' is equivalent to 'git reset --merge' when
`MERGE_HEAD` is present.
`MERGE_HEAD` is present unless `MERGE_AUTOSTASH` is also present in
which case 'git merge --abort' applies the stash entry to the worktree
whereas 'git reset --merge' will save the stashed changes in the stash
reflog.
--quit::
Forget about the current merge in progress. Leave the index
and the working tree as-is.
and the working tree as-is. If `MERGE_AUTOSTASH` is present, the
stash entry will be saved to the stash reflog.
--continue::
After a 'git merge' stops due to conflicts you can conclude the

View File

@ -134,15 +134,6 @@ unless you have read linkgit:git-rebase[1] carefully.
--no-rebase::
Override earlier --rebase.
--autostash::
--no-autostash::
Before starting rebase, stash local modifications away (see
linkgit:git-stash[1]) if needed, and apply the stash entry when
done. `--no-autostash` is useful to override the `rebase.autoStash`
configuration variable (see linkgit:git-config[1]).
+
This option is only valid when "--rebase" is used.
Options related to fetching
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -160,6 +160,14 @@ ifndef::git-pull[]
endif::git-pull[]
--autostash::
--no-autostash::
Automatically create a temporary stash entry before the operation
begins, and apply it after the operation ends. This means
that you can run the operation on a dirty worktree. However, use
with care: the final stash application after a successful
merge might result in non-trivial conflicts.
--allow-unrelated-histories::
By default, `git merge` command refuses to merge histories
that do not share a common ancestor. This option can be

View File

@ -610,8 +610,8 @@ SCRIPT_SH += git-web--browse.sh
SCRIPT_LIB += git-mergetool--lib
SCRIPT_LIB += git-parse-remote
SCRIPT_LIB += git-rebase--preserve-merges
SCRIPT_LIB += git-sh-setup
SCRIPT_LIB += git-sh-i18n
SCRIPT_LIB += git-sh-setup
SCRIPT_PERL += git-add--interactive.perl
SCRIPT_PERL += git-archimport.perl
@ -679,9 +679,9 @@ PROGRAM_OBJS += daemon.o
PROGRAM_OBJS += fast-import.o
PROGRAM_OBJS += http-backend.o
PROGRAM_OBJS += imap-send.o
PROGRAM_OBJS += remote-testsvn.o
PROGRAM_OBJS += sh-i18n--envsubst.o
PROGRAM_OBJS += shell.o
PROGRAM_OBJS += remote-testsvn.o
# Binary suffix, set to .exe for Windows builds
X =
@ -703,15 +703,16 @@ TEST_BUILTINS_OBJS += test-dump-untracked-cache.o
TEST_BUILTINS_OBJS += test-example-decorate.o
TEST_BUILTINS_OBJS += test-genrandom.o
TEST_BUILTINS_OBJS += test-genzeros.o
TEST_BUILTINS_OBJS += test-hash-speed.o
TEST_BUILTINS_OBJS += test-hash.o
TEST_BUILTINS_OBJS += test-hashmap.o
TEST_BUILTINS_OBJS += test-hash-speed.o
TEST_BUILTINS_OBJS += test-index-version.o
TEST_BUILTINS_OBJS += test-json-writer.o
TEST_BUILTINS_OBJS += test-lazy-init-name-hash.o
TEST_BUILTINS_OBJS += test-match-trees.o
TEST_BUILTINS_OBJS += test-mergesort.o
TEST_BUILTINS_OBJS += test-mktemp.o
TEST_BUILTINS_OBJS += test-oid-array.o
TEST_BUILTINS_OBJS += test-oidmap.o
TEST_BUILTINS_OBJS += test-online-cpus.o
TEST_BUILTINS_OBJS += test-parse-options.o
@ -732,7 +733,6 @@ TEST_BUILTINS_OBJS += test-run-command.o
TEST_BUILTINS_OBJS += test-scrap-cache-tree.o
TEST_BUILTINS_OBJS += test-serve-v2.o
TEST_BUILTINS_OBJS += test-sha1.o
TEST_BUILTINS_OBJS += test-oid-array.o
TEST_BUILTINS_OBJS += test-sha256.o
TEST_BUILTINS_OBJS += test-sigchain.o
TEST_BUILTINS_OBJS += test-strcmp-offset.o
@ -742,10 +742,10 @@ TEST_BUILTINS_OBJS += test-submodule-nested-repo-config.o
TEST_BUILTINS_OBJS += test-subprocess.o
TEST_BUILTINS_OBJS += test-trace2.o
TEST_BUILTINS_OBJS += test-urlmatch-normalization.o
TEST_BUILTINS_OBJS += test-xml-encode.o
TEST_BUILTINS_OBJS += test-wildmatch.o
TEST_BUILTINS_OBJS += test-windows-named-pipe.o
TEST_BUILTINS_OBJS += test-write-cache.o
TEST_BUILTINS_OBJS += test-xml-encode.o
# Do not add more tests here unless they have extra dependencies. Add
# them in TEST_BUILTINS_OBJS above.
@ -782,10 +782,10 @@ OTHER_PROGRAMS = git$X
# what test wrappers are needed and 'install' will install, in bindir
BINDIR_PROGRAMS_NEED_X += git
BINDIR_PROGRAMS_NEED_X += git-upload-pack
BINDIR_PROGRAMS_NEED_X += git-receive-pack
BINDIR_PROGRAMS_NEED_X += git-upload-archive
BINDIR_PROGRAMS_NEED_X += git-shell
BINDIR_PROGRAMS_NEED_X += git-upload-archive
BINDIR_PROGRAMS_NEED_X += git-upload-pack
BINDIR_PROGRAMS_NO_X += git-cvsserver
@ -825,9 +825,9 @@ LIB_OBJS += advice.o
LIB_OBJS += alias.o
LIB_OBJS += alloc.o
LIB_OBJS += apply.o
LIB_OBJS += archive.o
LIB_OBJS += archive-tar.o
LIB_OBJS += archive-zip.o
LIB_OBJS += archive.o
LIB_OBJS += argv-array.o
LIB_OBJS += attr.o
LIB_OBJS += base85.o
@ -843,9 +843,9 @@ LIB_OBJS += checkout.o
LIB_OBJS += color.o
LIB_OBJS += column.o
LIB_OBJS += combine-diff.o
LIB_OBJS += commit.o
LIB_OBJS += commit-graph.o
LIB_OBJS += commit-reach.o
LIB_OBJS += commit.o
LIB_OBJS += compat/obstack.o
LIB_OBJS += compat/terminal.o
LIB_OBJS += config.o
@ -859,17 +859,17 @@ LIB_OBJS += ctype.o
LIB_OBJS += date.o
LIB_OBJS += decorate.o
LIB_OBJS += delta-islands.o
LIB_OBJS += diff-delta.o
LIB_OBJS += diff-lib.o
LIB_OBJS += diff-no-index.o
LIB_OBJS += diff.o
LIB_OBJS += diffcore-break.o
LIB_OBJS += diffcore-delta.o
LIB_OBJS += diffcore-order.o
LIB_OBJS += diffcore-pickaxe.o
LIB_OBJS += diffcore-rename.o
LIB_OBJS += diff-delta.o
LIB_OBJS += diff-lib.o
LIB_OBJS += diff-no-index.o
LIB_OBJS += diff.o
LIB_OBJS += dir.o
LIB_OBJS += dir-iterator.o
LIB_OBJS += dir.o
LIB_OBJS += editor.o
LIB_OBJS += entry.o
LIB_OBJS += environment.o
@ -888,7 +888,6 @@ LIB_OBJS += gpg-interface.o
LIB_OBJS += graph.o
LIB_OBJS += grep.o
LIB_OBJS += hashmap.o
LIB_OBJS += linear-assignment.o
LIB_OBJS += help.o
LIB_OBJS += hex.o
LIB_OBJS += ident.o
@ -898,9 +897,10 @@ LIB_OBJS += kwset.o
LIB_OBJS += levenshtein.o
LIB_OBJS += line-log.o
LIB_OBJS += line-range.o
LIB_OBJS += list-objects.o
LIB_OBJS += list-objects-filter.o
LIB_OBJS += linear-assignment.o
LIB_OBJS += list-objects-filter-options.o
LIB_OBJS += list-objects-filter.o
LIB_OBJS += list-objects.o
LIB_OBJS += ll-merge.o
LIB_OBJS += lockfile.o
LIB_OBJS += log-tree.o
@ -909,32 +909,32 @@ LIB_OBJS += mailinfo.o
LIB_OBJS += mailmap.o
LIB_OBJS += match-trees.o
LIB_OBJS += mem-pool.o
LIB_OBJS += merge.o
LIB_OBJS += merge-blobs.o
LIB_OBJS += merge-recursive.o
LIB_OBJS += merge.o
LIB_OBJS += mergesort.o
LIB_OBJS += midx.o
LIB_OBJS += name-hash.o
LIB_OBJS += negotiator/default.o
LIB_OBJS += negotiator/skipping.o
LIB_OBJS += notes.o
LIB_OBJS += notes-cache.o
LIB_OBJS += notes-merge.o
LIB_OBJS += notes-utils.o
LIB_OBJS += notes.o
LIB_OBJS += object.o
LIB_OBJS += oid-array.o
LIB_OBJS += oidmap.o
LIB_OBJS += oidset.o
LIB_OBJS += oid-array.o
LIB_OBJS += packfile.o
LIB_OBJS += pack-bitmap.o
LIB_OBJS += pack-bitmap-write.o
LIB_OBJS += pack-bitmap.o
LIB_OBJS += pack-check.o
LIB_OBJS += pack-objects.o
LIB_OBJS += pack-revindex.o
LIB_OBJS += pack-write.o
LIB_OBJS += packfile.o
LIB_OBJS += pager.o
LIB_OBJS += parse-options.o
LIB_OBJS += parse-options-cb.o
LIB_OBJS += parse-options.o
LIB_OBJS += patch-delta.o
LIB_OBJS += patch-ids.o
LIB_OBJS += path.o
@ -952,8 +952,9 @@ LIB_OBJS += quote.o
LIB_OBJS += range-diff.o
LIB_OBJS += reachable.o
LIB_OBJS += read-cache.o
LIB_OBJS += rebase.o
LIB_OBJS += rebase-interactive.o
LIB_OBJS += rebase.o
LIB_OBJS += ref-filter.o
LIB_OBJS += reflog-walk.o
LIB_OBJS += refs.o
LIB_OBJS += refs/files-backend.o
@ -961,12 +962,12 @@ LIB_OBJS += refs/iterator.o
LIB_OBJS += refs/packed-backend.o
LIB_OBJS += refs/ref-cache.o
LIB_OBJS += refspec.o
LIB_OBJS += ref-filter.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
LIB_OBJS += repo-settings.o
LIB_OBJS += repository.o
LIB_OBJS += rerere.o
LIB_OBJS += reset.o
LIB_OBJS += resolve-undo.o
LIB_OBJS += revision.o
LIB_OBJS += run-command.o
@ -975,8 +976,8 @@ LIB_OBJS += sequencer.o
LIB_OBJS += serve.o
LIB_OBJS += server-info.o
LIB_OBJS += setup.o
LIB_OBJS += sha1-lookup.o
LIB_OBJS += sha1-file.o
LIB_OBJS += sha1-lookup.o
LIB_OBJS += sha1-name.o
LIB_OBJS += shallow.o
LIB_OBJS += sideband.o
@ -986,9 +987,9 @@ LIB_OBJS += stable-qsort.o
LIB_OBJS += strbuf.o
LIB_OBJS += streaming.o
LIB_OBJS += string-list.o
LIB_OBJS += submodule.o
LIB_OBJS += submodule-config.o
LIB_OBJS += sub-process.o
LIB_OBJS += submodule-config.o
LIB_OBJS += submodule.o
LIB_OBJS += symlinks.o
LIB_OBJS += tag.o
LIB_OBJS += tempfile.o
@ -1007,11 +1008,11 @@ LIB_OBJS += trace2/tr2_tgt_normal.o
LIB_OBJS += trace2/tr2_tgt_perf.o
LIB_OBJS += trace2/tr2_tls.o
LIB_OBJS += trailer.o
LIB_OBJS += transport.o
LIB_OBJS += transport-helper.o
LIB_OBJS += transport.o
LIB_OBJS += tree-diff.o
LIB_OBJS += tree.o
LIB_OBJS += tree-walk.o
LIB_OBJS += tree.o
LIB_OBJS += unpack-trees.o
LIB_OBJS += upload-pack.o
LIB_OBJS += url.o
@ -1051,9 +1052,9 @@ BUILTIN_OBJS += builtin/checkout.o
BUILTIN_OBJS += builtin/clean.o
BUILTIN_OBJS += builtin/clone.o
BUILTIN_OBJS += builtin/column.o
BUILTIN_OBJS += builtin/commit-graph.o
BUILTIN_OBJS += builtin/commit-tree.o
BUILTIN_OBJS += builtin/commit.o
BUILTIN_OBJS += builtin/commit-graph.o
BUILTIN_OBJS += builtin/config.o
BUILTIN_OBJS += builtin/count-objects.o
BUILTIN_OBJS += builtin/credential.o
@ -1084,13 +1085,13 @@ BUILTIN_OBJS += builtin/ls-remote.o
BUILTIN_OBJS += builtin/ls-tree.o
BUILTIN_OBJS += builtin/mailinfo.o
BUILTIN_OBJS += builtin/mailsplit.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/merge-base.o
BUILTIN_OBJS += builtin/merge-file.o
BUILTIN_OBJS += builtin/merge-index.o
BUILTIN_OBJS += builtin/merge-ours.o
BUILTIN_OBJS += builtin/merge-recursive.o
BUILTIN_OBJS += builtin/merge-tree.o
BUILTIN_OBJS += builtin/merge.o
BUILTIN_OBJS += builtin/mktag.o
BUILTIN_OBJS += builtin/mktree.o
BUILTIN_OBJS += builtin/multi-pack-index.o
@ -1110,9 +1111,9 @@ BUILTIN_OBJS += builtin/read-tree.o
BUILTIN_OBJS += builtin/rebase.o
BUILTIN_OBJS += builtin/receive-pack.o
BUILTIN_OBJS += builtin/reflog.o
BUILTIN_OBJS += builtin/remote.o
BUILTIN_OBJS += builtin/remote-ext.o
BUILTIN_OBJS += builtin/remote-fd.o
BUILTIN_OBJS += builtin/remote.o
BUILTIN_OBJS += builtin/repack.o
BUILTIN_OBJS += builtin/replace.o
BUILTIN_OBJS += builtin/rerere.o
@ -2331,16 +2332,16 @@ reconfigure config.mak.autogen: config.status
endif
XDIFF_OBJS += xdiff/xdiffi.o
XDIFF_OBJS += xdiff/xprepare.o
XDIFF_OBJS += xdiff/xutils.o
XDIFF_OBJS += xdiff/xemit.o
XDIFF_OBJS += xdiff/xhistogram.o
XDIFF_OBJS += xdiff/xmerge.o
XDIFF_OBJS += xdiff/xpatience.o
XDIFF_OBJS += xdiff/xhistogram.o
XDIFF_OBJS += xdiff/xprepare.o
XDIFF_OBJS += xdiff/xutils.o
VCSSVN_OBJS += vcs-svn/fast_export.o
VCSSVN_OBJS += vcs-svn/line_buffer.o
VCSSVN_OBJS += vcs-svn/sliding_window.o
VCSSVN_OBJS += vcs-svn/fast_export.o
VCSSVN_OBJS += vcs-svn/svndiff.o
VCSSVN_OBJS += vcs-svn/svndump.o
@ -3148,9 +3149,10 @@ endif
#
ALL_COMMANDS = $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS)
ALL_COMMANDS += git
ALL_COMMANDS += git-citool
ALL_COMMANDS += git-gui
ALL_COMMANDS += gitk
ALL_COMMANDS += gitweb
ALL_COMMANDS += git-gui git-citool
.PHONY: check-docs
check-docs::

View File

@ -344,6 +344,7 @@ void remove_merge_branch_state(struct repository *r)
unlink(git_path_merge_rr(r));
unlink(git_path_merge_msg(r));
unlink(git_path_merge_mode(r));
save_autostash(git_path_merge_autostash(r));
}
void remove_branch_state(struct repository *r, int verbose)

View File

@ -1721,6 +1721,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
&oid, flags);
}
apply_autostash(git_path_merge_autostash(the_repository));
UNLEAK(err);
UNLEAK(sb);
return 0;

View File

@ -82,6 +82,7 @@ static int show_progress = -1;
static int default_to_upstream = 1;
static int signoff;
static const char *sign_commit;
static int autostash;
static int no_verify;
static struct strategy all_strategy[] = {
@ -286,6 +287,7 @@ static struct option builtin_merge_options[] = {
OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1),
{ OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"),
N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
OPT_AUTOSTASH(&autostash),
OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")),
OPT_BOOL(0, "signoff", &signoff, N_("add Signed-off-by:")),
OPT_BOOL(0, "no-verify", &no_verify, N_("bypass pre-merge-commit and commit-msg hooks")),
@ -475,6 +477,7 @@ static void finish(struct commit *head_commit,
/* Run a post-merge hook */
run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
apply_autostash(git_path_merge_autostash(the_repository));
strbuf_release(&reflog_message);
}
@ -636,6 +639,9 @@ static int git_merge_config(const char *k, const char *v, void *cb)
return 0;
} else if (!strcmp(k, "gpg.mintrustlevel")) {
check_trust_level = 0;
} else if (!strcmp(k, "merge.autostash")) {
autostash = git_config_bool(k, v);
return 0;
}
status = fmt_merge_msg_config(k, v, cb);
@ -1283,6 +1289,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
if (abort_current_merge) {
int nargc = 2;
const char *nargv[] = {"reset", "--merge", NULL};
struct strbuf stash_oid = STRBUF_INIT;
if (orig_argc != 2)
usage_msg_opt(_("--abort expects no arguments"),
@ -1291,8 +1298,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
if (!file_exists(git_path_merge_head(the_repository)))
die(_("There is no merge to abort (MERGE_HEAD missing)."));
if (read_oneliner(&stash_oid, git_path_merge_autostash(the_repository),
READ_ONELINER_SKIP_IF_EMPTY))
unlink(git_path_merge_autostash(the_repository));
/* Invoke 'git reset --merge' */
ret = cmd_reset(nargc, nargv, prefix);
if (stash_oid.len)
apply_autostash_oid(stash_oid.buf);
strbuf_release(&stash_oid);
goto done;
}
@ -1515,6 +1531,10 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
goto done;
}
if (autostash)
create_autostash(the_repository,
git_path_merge_autostash(the_repository),
"merge");
if (checkout_fast_forward(the_repository,
&head_commit->object.oid,
&commit->object.oid,
@ -1581,6 +1601,11 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
if (fast_forward == FF_ONLY)
die(_("Not possible to fast-forward, aborting."));
if (autostash)
create_autostash(the_repository,
git_path_merge_autostash(the_repository),
"merge");
/* We are going to make a new commit. */
git_committer_info(IDENT_STRICT);

View File

@ -164,7 +164,7 @@ static struct option pull_options[] = {
N_("verify that the named commit has a valid GPG signature"),
PARSE_OPT_NOARG),
OPT_BOOL(0, "autostash", &opt_autostash,
N_("automatically stash/stash pop before and after rebase")),
N_("automatically stash/stash pop before and after")),
OPT_PASSTHRU_ARGV('s', "strategy", &opt_strategies, N_("strategy"),
N_("merge strategy to use"),
0),
@ -695,6 +695,10 @@ static int run_merge(void)
argv_array_pushv(&args, opt_strategy_opts.argv);
if (opt_gpg_sign)
argv_array_push(&args, opt_gpg_sign);
if (opt_autostash == 0)
argv_array_push(&args, "--no-autostash");
else if (opt_autostash == 1)
argv_array_push(&args, "--autostash");
if (opt_allow_unrelated_histories > 0)
argv_array_push(&args, "--allow-unrelated-histories");
@ -942,9 +946,6 @@ int cmd_pull(int argc, const char **argv, const char *prefix)
if (get_oid("HEAD", &orig_head))
oidclr(&orig_head);
if (!opt_rebase && opt_autostash != -1)
die(_("--[no-]autostash option is only valid with --rebase."));
autostash = config_autostash;
if (opt_rebase) {
if (opt_autostash != -1)

View File

@ -27,6 +27,9 @@
#include "branch.h"
#include "sequencer.h"
#include "rebase-interactive.h"
#include "reset.h"
#define DEFAULT_REFLOG_ACTION "rebase"
static char const * const builtin_rebase_usage[] = {
N_("git rebase [-i] [options] [--exec <cmd>] "
@ -590,15 +593,6 @@ static const char *state_dir_path(const char *filename, struct rebase_options *o
return path.buf;
}
/* Read one file, then strip line endings */
static int read_one(const char *path, struct strbuf *buf)
{
if (strbuf_read_file(buf, path, 0) < 0)
return error_errno(_("could not read '%s'"), path);
strbuf_trim_trailing_newline(buf);
return 0;
}
/* Initialize the rebase options from the state directory. */
static int read_basic_state(struct rebase_options *opts)
{
@ -606,8 +600,10 @@ static int read_basic_state(struct rebase_options *opts)
struct strbuf buf = STRBUF_INIT;
struct object_id oid;
if (read_one(state_dir_path("head-name", opts), &head_name) ||
read_one(state_dir_path("onto", opts), &buf))
if (!read_oneliner(&head_name, state_dir_path("head-name", opts),
READ_ONELINER_WARN_MISSING) ||
!read_oneliner(&buf, state_dir_path("onto", opts),
READ_ONELINER_WARN_MISSING))
return -1;
opts->head_name = starts_with(head_name.buf, "refs/") ?
xstrdup(head_name.buf) : NULL;
@ -623,9 +619,11 @@ static int read_basic_state(struct rebase_options *opts)
*/
strbuf_reset(&buf);
if (file_exists(state_dir_path("orig-head", opts))) {
if (read_one(state_dir_path("orig-head", opts), &buf))
if (!read_oneliner(&buf, state_dir_path("orig-head", opts),
READ_ONELINER_WARN_MISSING))
return -1;
} else if (read_one(state_dir_path("head", opts), &buf))
} else if (!read_oneliner(&buf, state_dir_path("head", opts),
READ_ONELINER_WARN_MISSING))
return -1;
if (get_oid(buf.buf, &opts->orig_head))
return error(_("invalid orig-head: '%s'"), buf.buf);
@ -645,8 +643,8 @@ static int read_basic_state(struct rebase_options *opts)
if (file_exists(state_dir_path("allow_rerere_autoupdate", opts))) {
strbuf_reset(&buf);
if (read_one(state_dir_path("allow_rerere_autoupdate", opts),
&buf))
if (!read_oneliner(&buf, state_dir_path("allow_rerere_autoupdate", opts),
READ_ONELINER_WARN_MISSING))
return -1;
if (!strcmp(buf.buf, "--rerere-autoupdate"))
opts->allow_rerere_autoupdate = RERERE_AUTOUPDATE;
@ -659,8 +657,8 @@ static int read_basic_state(struct rebase_options *opts)
if (file_exists(state_dir_path("gpg_sign_opt", opts))) {
strbuf_reset(&buf);
if (read_one(state_dir_path("gpg_sign_opt", opts),
&buf))
if (!read_oneliner(&buf, state_dir_path("gpg_sign_opt", opts),
READ_ONELINER_WARN_MISSING))
return -1;
free(opts->gpg_sign_opt);
opts->gpg_sign_opt = xstrdup(buf.buf);
@ -668,7 +666,8 @@ static int read_basic_state(struct rebase_options *opts)
if (file_exists(state_dir_path("strategy", opts))) {
strbuf_reset(&buf);
if (read_one(state_dir_path("strategy", opts), &buf))
if (!read_oneliner(&buf, state_dir_path("strategy", opts),
READ_ONELINER_WARN_MISSING))
return -1;
free(opts->strategy);
opts->strategy = xstrdup(buf.buf);
@ -676,7 +675,8 @@ static int read_basic_state(struct rebase_options *opts)
if (file_exists(state_dir_path("strategy_opts", opts))) {
strbuf_reset(&buf);
if (read_one(state_dir_path("strategy_opts", opts), &buf))
if (!read_oneliner(&buf, state_dir_path("strategy_opts", opts),
READ_ONELINER_WARN_MISSING))
return -1;
free(opts->strategy_opts);
opts->strategy_opts = xstrdup(buf.buf);
@ -719,51 +719,6 @@ static int rebase_write_basic_state(struct rebase_options *opts)
return 0;
}
static int apply_autostash(struct rebase_options *opts)
{
const char *path = state_dir_path("autostash", opts);
struct strbuf autostash = STRBUF_INIT;
struct child_process stash_apply = CHILD_PROCESS_INIT;
if (!file_exists(path))
return 0;
if (read_one(path, &autostash))
return error(_("Could not read '%s'"), path);
/* Ensure that the hash is not mistaken for a number */
strbuf_addstr(&autostash, "^0");
argv_array_pushl(&stash_apply.args,
"stash", "apply", autostash.buf, NULL);
stash_apply.git_cmd = 1;
stash_apply.no_stderr = stash_apply.no_stdout =
stash_apply.no_stdin = 1;
if (!run_command(&stash_apply))
printf(_("Applied autostash.\n"));
else {
struct argv_array args = ARGV_ARRAY_INIT;
int res = 0;
argv_array_pushl(&args,
"stash", "store", "-m", "autostash", "-q",
autostash.buf, NULL);
if (run_command_v_opt(args.argv, RUN_GIT_CMD))
res = error(_("Cannot store %s"), autostash.buf);
argv_array_clear(&args);
strbuf_release(&autostash);
if (res)
return res;
fprintf(stderr,
_("Applying autostash resulted in conflicts.\n"
"Your changes are safe in the stash.\n"
"You can run \"git stash pop\" or \"git stash drop\" "
"at any time.\n"));
}
strbuf_release(&autostash);
return 0;
}
static int finish_rebase(struct rebase_options *opts)
{
struct strbuf dir = STRBUF_INIT;
@ -771,7 +726,7 @@ static int finish_rebase(struct rebase_options *opts)
int ret = 0;
delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF);
apply_autostash(opts);
apply_autostash(state_dir_path("autostash", opts));
close_object_store(the_repository->objects);
/*
* We ignore errors in 'gc --auto', since the
@ -816,144 +771,6 @@ static void add_var(struct strbuf *buf, const char *name, const char *value)
}
}
#define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION"
#define RESET_HEAD_DETACH (1<<0)
#define RESET_HEAD_HARD (1<<1)
#define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2)
#define RESET_HEAD_REFS_ONLY (1<<3)
#define RESET_ORIG_HEAD (1<<4)
static int reset_head(struct object_id *oid, const char *action,
const char *switch_to_branch, unsigned flags,
const char *reflog_orig_head, const char *reflog_head)
{
unsigned detach_head = flags & RESET_HEAD_DETACH;
unsigned reset_hard = flags & RESET_HEAD_HARD;
unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
unsigned refs_only = flags & RESET_HEAD_REFS_ONLY;
unsigned update_orig_head = flags & RESET_ORIG_HEAD;
struct object_id head_oid;
struct tree_desc desc[2] = { { NULL }, { NULL } };
struct lock_file lock = LOCK_INIT;
struct unpack_trees_options unpack_tree_opts;
struct tree *tree;
const char *reflog_action;
struct strbuf msg = STRBUF_INIT;
size_t prefix_len;
struct object_id *orig = NULL, oid_orig,
*old_orig = NULL, oid_old_orig;
int ret = 0, nr = 0;
if (switch_to_branch && !starts_with(switch_to_branch, "refs/"))
BUG("Not a fully qualified branch: '%s'", switch_to_branch);
if (!refs_only && hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0) {
ret = -1;
goto leave_reset_head;
}
if ((!oid || !reset_hard) && get_oid("HEAD", &head_oid)) {
ret = error(_("could not determine HEAD revision"));
goto leave_reset_head;
}
if (!oid)
oid = &head_oid;
if (refs_only)
goto reset_head_refs;
memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
setup_unpack_trees_porcelain(&unpack_tree_opts, action);
unpack_tree_opts.head_idx = 1;
unpack_tree_opts.src_index = the_repository->index;
unpack_tree_opts.dst_index = the_repository->index;
unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge;
unpack_tree_opts.update = 1;
unpack_tree_opts.merge = 1;
init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL);
if (!detach_head)
unpack_tree_opts.reset = 1;
if (repo_read_index_unmerged(the_repository) < 0) {
ret = error(_("could not read index"));
goto leave_reset_head;
}
if (!reset_hard && !fill_tree_descriptor(the_repository, &desc[nr++], &head_oid)) {
ret = error(_("failed to find tree of %s"),
oid_to_hex(&head_oid));
goto leave_reset_head;
}
if (!fill_tree_descriptor(the_repository, &desc[nr++], oid)) {
ret = error(_("failed to find tree of %s"), oid_to_hex(oid));
goto leave_reset_head;
}
if (unpack_trees(nr, desc, &unpack_tree_opts)) {
ret = -1;
goto leave_reset_head;
}
tree = parse_tree_indirect(oid);
prime_cache_tree(the_repository, the_repository->index, tree);
if (write_locked_index(the_repository->index, &lock, COMMIT_LOCK) < 0) {
ret = error(_("could not write index"));
goto leave_reset_head;
}
reset_head_refs:
reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT);
strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : "rebase");
prefix_len = msg.len;
if (update_orig_head) {
if (!get_oid("ORIG_HEAD", &oid_old_orig))
old_orig = &oid_old_orig;
if (!get_oid("HEAD", &oid_orig)) {
orig = &oid_orig;
if (!reflog_orig_head) {
strbuf_addstr(&msg, "updating ORIG_HEAD");
reflog_orig_head = msg.buf;
}
update_ref(reflog_orig_head, "ORIG_HEAD", orig,
old_orig, 0, UPDATE_REFS_MSG_ON_ERR);
} else if (old_orig)
delete_ref(NULL, "ORIG_HEAD", old_orig, 0);
}
if (!reflog_head) {
strbuf_setlen(&msg, prefix_len);
strbuf_addstr(&msg, "updating HEAD");
reflog_head = msg.buf;
}
if (!switch_to_branch)
ret = update_ref(reflog_head, "HEAD", oid, orig,
detach_head ? REF_NO_DEREF : 0,
UPDATE_REFS_MSG_ON_ERR);
else {
ret = update_ref(reflog_head, switch_to_branch, oid,
NULL, 0, UPDATE_REFS_MSG_ON_ERR);
if (!ret)
ret = create_symref("HEAD", switch_to_branch,
reflog_head);
}
if (run_hook)
run_hook_le(NULL, "post-checkout",
oid_to_hex(orig ? orig : &null_oid),
oid_to_hex(oid), "1", NULL);
leave_reset_head:
strbuf_release(&msg);
rollback_lock_file(&lock);
while (nr)
free((void *)desc[--nr].buffer);
return ret;
}
static int move_to_original_branch(struct rebase_options *opts)
{
struct strbuf orig_head_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT;
@ -969,8 +786,10 @@ static int move_to_original_branch(struct rebase_options *opts)
opts->head_name, oid_to_hex(&opts->onto->object.oid));
strbuf_addf(&head_reflog, "rebase finished: returning to %s",
opts->head_name);
ret = reset_head(NULL, "", opts->head_name, RESET_HEAD_REFS_ONLY,
orig_head_reflog.buf, head_reflog.buf);
ret = reset_head(the_repository, NULL, "", opts->head_name,
RESET_HEAD_REFS_ONLY,
orig_head_reflog.buf, head_reflog.buf,
DEFAULT_REFLOG_ACTION);
strbuf_release(&orig_head_reflog);
strbuf_release(&head_reflog);
@ -1058,8 +877,9 @@ static int run_am(struct rebase_options *opts)
free(rebased_patches);
argv_array_clear(&am.args);
reset_head(&opts->orig_head, "checkout", opts->head_name, 0,
"HEAD", NULL);
reset_head(the_repository, &opts->orig_head, "checkout",
opts->head_name, 0,
"HEAD", NULL, DEFAULT_REFLOG_ACTION);
error(_("\ngit encountered an error while preparing the "
"patches to replay\n"
"these revisions:\n"
@ -1218,7 +1038,7 @@ finished_rebase:
} else if (status == 2) {
struct strbuf dir = STRBUF_INIT;
apply_autostash(opts);
apply_autostash(state_dir_path("autostash", opts));
strbuf_addstr(&dir, opts->state_dir);
remove_dir_recursively(&dir, 0);
strbuf_release(&dir);
@ -1459,7 +1279,6 @@ static int check_exec_cmd(const char *cmd)
return 0;
}
int cmd_rebase(int argc, const char **argv, const char *prefix)
{
struct rebase_options options = REBASE_OPTIONS_INIT;
@ -1562,8 +1381,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
{ OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
N_("GPG-sign commits"),
PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
OPT_BOOL(0, "autostash", &options.autostash,
N_("automatically stash/stash pop before and after")),
OPT_AUTOSTASH(&options.autostash),
OPT_STRING_LIST('x', "exec", &exec, N_("exec"),
N_("add exec lines after each commit of the "
"editable list")),
@ -1720,8 +1538,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
rerere_clear(the_repository, &merge_rr);
string_list_clear(&merge_rr, 1);
if (reset_head(NULL, "reset", NULL, RESET_HEAD_HARD,
NULL, NULL) < 0)
if (reset_head(the_repository, NULL, "reset", NULL, RESET_HEAD_HARD,
NULL, NULL, DEFAULT_REFLOG_ACTION) < 0)
die(_("could not discard worktree changes"));
remove_branch_state(the_repository, 0);
if (read_basic_state(&options))
@ -1738,9 +1556,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
if (read_basic_state(&options))
exit(1);
if (reset_head(&options.orig_head, "reset",
if (reset_head(the_repository, &options.orig_head, "reset",
options.head_name, RESET_HEAD_HARD,
NULL, NULL) < 0)
NULL, NULL, DEFAULT_REFLOG_ACTION) < 0)
die(_("could not move back to %s"),
oid_to_hex(&options.orig_head));
remove_branch_state(the_repository, 0);
@ -2098,49 +1916,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
die(_("could not read index"));
if (options.autostash) {
struct lock_file lock_file = LOCK_INIT;
int fd;
fd = hold_locked_index(&lock_file, 0);
refresh_cache(REFRESH_QUIET);
if (0 <= fd)
repo_update_index_if_able(the_repository, &lock_file);
rollback_lock_file(&lock_file);
if (has_unstaged_changes(the_repository, 1) ||
has_uncommitted_changes(the_repository, 1)) {
const char *autostash =
state_dir_path("autostash", &options);
struct child_process stash = CHILD_PROCESS_INIT;
struct object_id oid;
argv_array_pushl(&stash.args,
"stash", "create", "autostash", NULL);
stash.git_cmd = 1;
stash.no_stdin = 1;
strbuf_reset(&buf);
if (capture_command(&stash, &buf, GIT_MAX_HEXSZ))
die(_("Cannot autostash"));
strbuf_trim_trailing_newline(&buf);
if (get_oid(buf.buf, &oid))
die(_("Unexpected stash response: '%s'"),
buf.buf);
strbuf_reset(&buf);
strbuf_add_unique_abbrev(&buf, &oid, DEFAULT_ABBREV);
if (safe_create_leading_directories_const(autostash))
die(_("Could not create directory for '%s'"),
options.state_dir);
write_file(autostash, "%s", oid_to_hex(&oid));
printf(_("Created autostash: %s\n"), buf.buf);
if (reset_head(NULL, "reset --hard",
NULL, RESET_HEAD_HARD, NULL, NULL) < 0)
die(_("could not reset --hard"));
if (discard_index(the_repository->index) < 0 ||
repo_read_index(the_repository) < 0)
die(_("could not read index"));
}
create_autostash(the_repository, state_dir_path("autostash", &options),
DEFAULT_REFLOG_ACTION);
}
if (require_clean_work_tree(the_repository, "rebase",
@ -2174,10 +1951,12 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
strbuf_addf(&buf, "%s: checkout %s",
getenv(GIT_REFLOG_ACTION_ENVIRONMENT),
options.switch_to);
if (reset_head(&options.orig_head, "checkout",
if (reset_head(the_repository,
&options.orig_head, "checkout",
options.head_name,
RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
NULL, buf.buf) < 0) {
NULL, buf.buf,
DEFAULT_REFLOG_ACTION) < 0) {
ret = !!error(_("could not switch to "
"%s"),
options.switch_to);
@ -2249,10 +2028,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
strbuf_addf(&msg, "%s: checkout %s",
getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name);
if (reset_head(&options.onto->object.oid, "checkout", NULL,
if (reset_head(the_repository, &options.onto->object.oid, "checkout", NULL,
RESET_HEAD_DETACH | RESET_ORIG_HEAD |
RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
NULL, msg.buf))
NULL, msg.buf, DEFAULT_REFLOG_ACTION))
die(_("Could not detach HEAD"));
strbuf_release(&msg);
@ -2267,8 +2046,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
strbuf_addf(&msg, "rebase finished: %s onto %s",
options.head_name ? options.head_name : "detached HEAD",
oid_to_hex(&options.onto->object.oid));
reset_head(NULL, "Fast-forwarded", options.head_name,
RESET_HEAD_REFS_ONLY, "HEAD", msg.buf);
reset_head(the_repository, NULL, "Fast-forwarded", options.head_name,
RESET_HEAD_REFS_ONLY, "HEAD", msg.buf,
DEFAULT_REFLOG_ACTION);
strbuf_release(&msg);
ret = !!finish_rebase(&options);
goto cleanup;

View File

@ -336,5 +336,6 @@ int parse_opt_passthru_argv(const struct option *, const char *, int);
#define OPT_CLEANUP(v) OPT_STRING(0, "cleanup", v, N_("mode"), N_("how to strip spaces and #comments from message"))
#define OPT_PATHSPEC_FROM_FILE(v) OPT_FILENAME(0, "pathspec-from-file", v, N_("read pathspec from file"))
#define OPT_PATHSPEC_FILE_NUL(v) OPT_BOOL(0, "pathspec-file-nul", v, N_("with --pathspec-from-file, pathspec elements are separated with NUL character"))
#define OPT_AUTOSTASH(v) OPT_BOOL(0, "autostash", v, N_("automatically stash/stash pop before and after"))
#endif

1
path.c
View File

@ -1535,5 +1535,6 @@ REPO_GIT_PATH_FUNC(merge_msg, "MERGE_MSG")
REPO_GIT_PATH_FUNC(merge_rr, "MERGE_RR")
REPO_GIT_PATH_FUNC(merge_mode, "MERGE_MODE")
REPO_GIT_PATH_FUNC(merge_head, "MERGE_HEAD")
REPO_GIT_PATH_FUNC(merge_autostash, "MERGE_AUTOSTASH")
REPO_GIT_PATH_FUNC(fetch_head, "FETCH_HEAD")
REPO_GIT_PATH_FUNC(shallow, "shallow")

4
path.h
View File

@ -177,11 +177,12 @@ struct path_cache {
const char *merge_rr;
const char *merge_mode;
const char *merge_head;
const char *merge_autostash;
const char *fetch_head;
const char *shallow;
};
#define PATH_CACHE_INIT { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
#define PATH_CACHE_INIT { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
const char *git_path_cherry_pick_head(struct repository *r);
const char *git_path_revert_head(struct repository *r);
@ -190,6 +191,7 @@ const char *git_path_merge_msg(struct repository *r);
const char *git_path_merge_rr(struct repository *r);
const char *git_path_merge_mode(struct repository *r);
const char *git_path_merge_head(struct repository *r);
const char *git_path_merge_autostash(struct repository *r);
const char *git_path_fetch_head(struct repository *r);
const char *git_path_shallow(struct repository *r);

141
reset.c Normal file
View File

@ -0,0 +1,141 @@
#include "git-compat-util.h"
#include "cache-tree.h"
#include "lockfile.h"
#include "refs.h"
#include "reset.h"
#include "run-command.h"
#include "tree-walk.h"
#include "tree.h"
#include "unpack-trees.h"
int reset_head(struct repository *r, struct object_id *oid, const char *action,
const char *switch_to_branch, unsigned flags,
const char *reflog_orig_head, const char *reflog_head,
const char *default_reflog_action)
{
unsigned detach_head = flags & RESET_HEAD_DETACH;
unsigned reset_hard = flags & RESET_HEAD_HARD;
unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
unsigned refs_only = flags & RESET_HEAD_REFS_ONLY;
unsigned update_orig_head = flags & RESET_ORIG_HEAD;
struct object_id head_oid;
struct tree_desc desc[2] = { { NULL }, { NULL } };
struct lock_file lock = LOCK_INIT;
struct unpack_trees_options unpack_tree_opts;
struct tree *tree;
const char *reflog_action;
struct strbuf msg = STRBUF_INIT;
size_t prefix_len;
struct object_id *orig = NULL, oid_orig,
*old_orig = NULL, oid_old_orig;
int ret = 0, nr = 0;
if (switch_to_branch && !starts_with(switch_to_branch, "refs/"))
BUG("Not a fully qualified branch: '%s'", switch_to_branch);
if (!refs_only && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {
ret = -1;
goto leave_reset_head;
}
if ((!oid || !reset_hard) && get_oid("HEAD", &head_oid)) {
ret = error(_("could not determine HEAD revision"));
goto leave_reset_head;
}
if (!oid)
oid = &head_oid;
if (refs_only)
goto reset_head_refs;
memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
setup_unpack_trees_porcelain(&unpack_tree_opts, action);
unpack_tree_opts.head_idx = 1;
unpack_tree_opts.src_index = r->index;
unpack_tree_opts.dst_index = r->index;
unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge;
unpack_tree_opts.update = 1;
unpack_tree_opts.merge = 1;
init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL);
if (!detach_head)
unpack_tree_opts.reset = 1;
if (repo_read_index_unmerged(r) < 0) {
ret = error(_("could not read index"));
goto leave_reset_head;
}
if (!reset_hard && !fill_tree_descriptor(r, &desc[nr++], &head_oid)) {
ret = error(_("failed to find tree of %s"),
oid_to_hex(&head_oid));
goto leave_reset_head;
}
if (!fill_tree_descriptor(r, &desc[nr++], oid)) {
ret = error(_("failed to find tree of %s"), oid_to_hex(oid));
goto leave_reset_head;
}
if (unpack_trees(nr, desc, &unpack_tree_opts)) {
ret = -1;
goto leave_reset_head;
}
tree = parse_tree_indirect(oid);
prime_cache_tree(r, r->index, tree);
if (write_locked_index(r->index, &lock, COMMIT_LOCK) < 0) {
ret = error(_("could not write index"));
goto leave_reset_head;
}
reset_head_refs:
reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT);
strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : default_reflog_action);
prefix_len = msg.len;
if (update_orig_head) {
if (!get_oid("ORIG_HEAD", &oid_old_orig))
old_orig = &oid_old_orig;
if (!get_oid("HEAD", &oid_orig)) {
orig = &oid_orig;
if (!reflog_orig_head) {
strbuf_addstr(&msg, "updating ORIG_HEAD");
reflog_orig_head = msg.buf;
}
update_ref(reflog_orig_head, "ORIG_HEAD", orig,
old_orig, 0, UPDATE_REFS_MSG_ON_ERR);
} else if (old_orig)
delete_ref(NULL, "ORIG_HEAD", old_orig, 0);
}
if (!reflog_head) {
strbuf_setlen(&msg, prefix_len);
strbuf_addstr(&msg, "updating HEAD");
reflog_head = msg.buf;
}
if (!switch_to_branch)
ret = update_ref(reflog_head, "HEAD", oid, orig,
detach_head ? REF_NO_DEREF : 0,
UPDATE_REFS_MSG_ON_ERR);
else {
ret = update_ref(reflog_head, switch_to_branch, oid,
NULL, 0, UPDATE_REFS_MSG_ON_ERR);
if (!ret)
ret = create_symref("HEAD", switch_to_branch,
reflog_head);
}
if (run_hook)
run_hook_le(NULL, "post-checkout",
oid_to_hex(orig ? orig : &null_oid),
oid_to_hex(oid), "1", NULL);
leave_reset_head:
strbuf_release(&msg);
rollback_lock_file(&lock);
while (nr)
free((void *)desc[--nr].buffer);
return ret;
}

20
reset.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef RESET_H
#define RESET_H
#include "hash.h"
#include "repository.h"
#define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION"
#define RESET_HEAD_DETACH (1<<0)
#define RESET_HEAD_HARD (1<<1)
#define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2)
#define RESET_HEAD_REFS_ONLY (1<<3)
#define RESET_ORIG_HEAD (1<<4)
int reset_head(struct repository *r, struct object_id *oid, const char *action,
const char *switch_to_branch, unsigned flags,
const char *reflog_orig_head, const char *reflog_head,
const char *default_reflog_action);
#endif

View File

@ -32,6 +32,7 @@
#include "alias.h"
#include "commit-reach.h"
#include "rebase-interactive.h"
#include "reset.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@ -419,25 +420,15 @@ static int write_message(const void *buf, size_t len, const char *filename,
return 0;
}
/*
* Reads a file that was presumably written by a shell script, i.e. with an
* end-of-line marker that needs to be stripped.
*
* Note that only the last end-of-line marker is stripped, consistent with the
* behavior of "$(cat path)" in a shell script.
*
* Returns 1 if the file was read, 0 if it could not be read or does not exist.
*/
static int read_oneliner(struct strbuf *buf,
const char *path, int skip_if_empty)
int read_oneliner(struct strbuf *buf,
const char *path, unsigned flags)
{
int orig_len = buf->len;
if (!file_exists(path))
return 0;
if (strbuf_read_file(buf, path, 0) < 0) {
warning_errno(_("could not read '%s'"), path);
if ((flags & READ_ONELINER_WARN_MISSING) ||
(errno != ENOENT && errno != ENOTDIR))
warning_errno(_("could not read '%s'"), path);
return 0;
}
@ -447,7 +438,7 @@ static int read_oneliner(struct strbuf *buf,
buf->buf[buf->len] = '\0';
}
if (skip_if_empty && buf->len == orig_len)
if ((flags & READ_ONELINER_SKIP_IF_EMPTY) && buf->len == orig_len)
return 0;
return 1;
@ -2504,8 +2495,10 @@ static int read_populate_opts(struct replay_opts *opts)
{
if (is_rebase_i(opts)) {
struct strbuf buf = STRBUF_INIT;
int ret = 0;
if (read_oneliner(&buf, rebase_path_gpg_sign_opt(), 1)) {
if (read_oneliner(&buf, rebase_path_gpg_sign_opt(),
READ_ONELINER_SKIP_IF_EMPTY)) {
if (!starts_with(buf.buf, "-S"))
strbuf_reset(&buf);
else {
@ -2515,7 +2508,8 @@ static int read_populate_opts(struct replay_opts *opts)
strbuf_reset(&buf);
}
if (read_oneliner(&buf, rebase_path_allow_rerere_autoupdate(), 1)) {
if (read_oneliner(&buf, rebase_path_allow_rerere_autoupdate(),
READ_ONELINER_SKIP_IF_EMPTY)) {
if (!strcmp(buf.buf, "--rerere-autoupdate"))
opts->allow_rerere_auto = RERERE_AUTOUPDATE;
else if (!strcmp(buf.buf, "--no-rerere-autoupdate"))
@ -2544,10 +2538,11 @@ static int read_populate_opts(struct replay_opts *opts)
opts->keep_redundant_commits = 1;
read_strategy_opts(opts, &buf);
strbuf_release(&buf);
strbuf_reset(&buf);
if (read_oneliner(&opts->current_fixups,
rebase_path_current_fixups(), 1)) {
rebase_path_current_fixups(),
READ_ONELINER_SKIP_IF_EMPTY)) {
const char *p = opts->current_fixups.buf;
opts->current_fixup_count = 1;
while ((p = strchr(p, '\n'))) {
@ -2557,12 +2552,16 @@ static int read_populate_opts(struct replay_opts *opts)
}
if (read_oneliner(&buf, rebase_path_squash_onto(), 0)) {
if (get_oid_hex(buf.buf, &opts->squash_onto) < 0)
return error(_("unusable squash-onto"));
if (get_oid_hex(buf.buf, &opts->squash_onto) < 0) {
ret = error(_("unusable squash-onto"));
goto done_rebase_i;
}
opts->have_squash_onto = 1;
}
return 0;
done_rebase_i:
strbuf_release(&buf);
return ret;
}
if (!file_exists(git_path_opts_file()))
@ -3677,25 +3676,71 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)
return -1;
}
static int apply_autostash(struct replay_opts *opts)
void create_autostash(struct repository *r, const char *path,
const char *default_reflog_action)
{
struct strbuf buf = STRBUF_INIT;
struct lock_file lock_file = LOCK_INIT;
int fd;
fd = repo_hold_locked_index(r, &lock_file, 0);
refresh_index(r->index, REFRESH_QUIET, NULL, NULL, NULL);
if (0 <= fd)
repo_update_index_if_able(r, &lock_file);
rollback_lock_file(&lock_file);
if (has_unstaged_changes(r, 1) ||
has_uncommitted_changes(r, 1)) {
struct child_process stash = CHILD_PROCESS_INIT;
struct object_id oid;
argv_array_pushl(&stash.args,
"stash", "create", "autostash", NULL);
stash.git_cmd = 1;
stash.no_stdin = 1;
strbuf_reset(&buf);
if (capture_command(&stash, &buf, GIT_MAX_HEXSZ))
die(_("Cannot autostash"));
strbuf_trim_trailing_newline(&buf);
if (get_oid(buf.buf, &oid))
die(_("Unexpected stash response: '%s'"),
buf.buf);
strbuf_reset(&buf);
strbuf_add_unique_abbrev(&buf, &oid, DEFAULT_ABBREV);
if (safe_create_leading_directories_const(path))
die(_("Could not create directory for '%s'"),
path);
write_file(path, "%s", oid_to_hex(&oid));
printf(_("Created autostash: %s\n"), buf.buf);
if (reset_head(r, NULL, "reset --hard",
NULL, RESET_HEAD_HARD, NULL, NULL,
default_reflog_action) < 0)
die(_("could not reset --hard"));
if (discard_index(r->index) < 0 ||
repo_read_index(r) < 0)
die(_("could not read index"));
}
strbuf_release(&buf);
}
static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
{
struct strbuf stash_sha1 = STRBUF_INIT;
struct child_process child = CHILD_PROCESS_INIT;
int ret = 0;
if (!read_oneliner(&stash_sha1, rebase_path_autostash(), 1)) {
strbuf_release(&stash_sha1);
return 0;
if (attempt_apply) {
child.git_cmd = 1;
child.no_stdout = 1;
child.no_stderr = 1;
argv_array_push(&child.args, "stash");
argv_array_push(&child.args, "apply");
argv_array_push(&child.args, stash_oid);
ret = run_command(&child);
}
strbuf_trim(&stash_sha1);
child.git_cmd = 1;
child.no_stdout = 1;
child.no_stderr = 1;
argv_array_push(&child.args, "stash");
argv_array_push(&child.args, "apply");
argv_array_push(&child.args, stash_sha1.buf);
if (!run_command(&child))
if (attempt_apply && !ret)
fprintf(stderr, _("Applied autostash.\n"));
else {
struct child_process store = CHILD_PROCESS_INIT;
@ -3706,21 +3751,57 @@ static int apply_autostash(struct replay_opts *opts)
argv_array_push(&store.args, "-m");
argv_array_push(&store.args, "autostash");
argv_array_push(&store.args, "-q");
argv_array_push(&store.args, stash_sha1.buf);
argv_array_push(&store.args, stash_oid);
if (run_command(&store))
ret = error(_("cannot store %s"), stash_sha1.buf);
ret = error(_("cannot store %s"), stash_oid);
else
fprintf(stderr,
_("Applying autostash resulted in conflicts.\n"
_("%s\n"
"Your changes are safe in the stash.\n"
"You can run \"git stash pop\" or"
" \"git stash drop\" at any time.\n"));
" \"git stash drop\" at any time.\n"),
attempt_apply ?
_("Applying autostash resulted in conflicts.") :
_("Autostash exists; creating a new stash entry."));
}
strbuf_release(&stash_sha1);
return ret;
}
static int apply_save_autostash(const char *path, int attempt_apply)
{
struct strbuf stash_oid = STRBUF_INIT;
int ret = 0;
if (!read_oneliner(&stash_oid, path,
READ_ONELINER_SKIP_IF_EMPTY)) {
strbuf_release(&stash_oid);
return 0;
}
strbuf_trim(&stash_oid);
ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply);
unlink(path);
strbuf_release(&stash_oid);
return ret;
}
int save_autostash(const char *path)
{
return apply_save_autostash(path, 0);
}
int apply_autostash(const char *path)
{
return apply_save_autostash(path, 1);
}
int apply_autostash_oid(const char *stash_oid)
{
return apply_save_autostash_oid(stash_oid, 1);
}
static const char *reflog_message(struct replay_opts *opts,
const char *sub_action, const char *fmt, ...)
{
@ -3776,7 +3857,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
return error(_("%s: not a valid OID"), orig_head);
if (run_git_checkout(r, opts, oid_to_hex(onto), action)) {
apply_autostash(opts);
apply_autostash(rebase_path_autostash());
sequencer_remove_state(opts);
return error(_("could not detach HEAD"));
}
@ -4095,7 +4176,7 @@ cleanup_head_ref:
run_command(&hook);
}
}
apply_autostash(opts);
apply_autostash(rebase_path_autostash());
if (!opts->quiet) {
if (!opts->verbose)
@ -4313,7 +4394,8 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
struct strbuf buf = STRBUF_INIT;
struct object_id oid;
if (read_oneliner(&buf, rebase_path_stopped_sha(), 1) &&
if (read_oneliner(&buf, rebase_path_stopped_sha(),
READ_ONELINER_SKIP_IF_EMPTY) &&
!get_oid_committish(buf.buf, &oid))
record_in_rewritten(&oid, peek_command(&todo_list, 0));
strbuf_release(&buf);
@ -5118,7 +5200,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
todo_list_add_exec_commands(todo_list, commands);
if (count_commands(todo_list) == 0) {
apply_autostash(opts);
apply_autostash(rebase_path_autostash());
sequencer_remove_state(opts);
return error(_("nothing to do"));
@ -5129,12 +5211,12 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
if (res == -1)
return -1;
else if (res == -2) {
apply_autostash(opts);
apply_autostash(rebase_path_autostash());
sequencer_remove_state(opts);
return -1;
} else if (res == -3) {
apply_autostash(opts);
apply_autostash(rebase_path_autostash());
sequencer_remove_state(opts);
todo_list_release(&new_todo);

View File

@ -191,6 +191,12 @@ void commit_post_rewrite(struct repository *r,
const struct commit *current_head,
const struct object_id *new_head);
void create_autostash(struct repository *r, const char *path,
const char *default_reflog_action);
int save_autostash(const char *path);
int apply_autostash(const char *path);
int apply_autostash_oid(const char *stash_oid);
#define SUMMARY_INITIAL_COMMIT (1 << 0)
#define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
void print_commit_summary(struct repository *repo,
@ -198,6 +204,20 @@ void print_commit_summary(struct repository *repo,
const struct object_id *oid,
unsigned int flags);
#define READ_ONELINER_SKIP_IF_EMPTY (1 << 0)
#define READ_ONELINER_WARN_MISSING (1 << 1)
/*
* Reads a file that was presumably written by a shell script, i.e. with an
* end-of-line marker that needs to be stripped.
*
* Note that only the last end-of-line marker is stripped, consistent with the
* behavior of "$(cat path)" in a shell script.
*
* Returns 1 if the file was read, 0 if it could not be read or does not exist.
*/
int read_oneliner(struct strbuf *buf,
const char *path, unsigned flags);
int read_author_script(const char *path, char **name, char **email, char **date,
int allow_missing);
void parse_strategy_opts(struct replay_opts *opts, char *raw_opts);

View File

@ -142,6 +142,17 @@ test_expect_success 'refuse two-project merge by default' '
test_must_fail git merge five
'
test_expect_success 'refuse two-project merge by default, quit before --autostash happens' '
t3033_reset &&
git reset --hard four &&
echo change >>one.t &&
git diff >expect &&
test_must_fail git merge --autostash five 2>err &&
test_i18ngrep ! "stash" err &&
git diff >actual &&
test_cmp expect actual
'
test_expect_success 'two-project merge with --allow-unrelated-histories' '
t3033_reset &&
git reset --hard four &&
@ -149,4 +160,15 @@ test_expect_success 'two-project merge with --allow-unrelated-histories' '
git diff --exit-code five
'
test_expect_success 'two-project merge with --allow-unrelated-histories with --autostash' '
t3033_reset &&
git reset --hard four &&
echo change >>one.t &&
git diff one.t >expect &&
git merge --allow-unrelated-histories --autostash five 2>err &&
test_i18ngrep "Applied autostash." err &&
git diff one.t >actual &&
test_cmp expect actual
'
test_done

View File

@ -10,11 +10,13 @@ modify () {
}
test_pull_autostash () {
expect_parent_num="$1" &&
shift &&
git reset --hard before-rebase &&
echo dirty >new_file &&
git add new_file &&
git pull "$@" . copy &&
test_cmp_rev HEAD^ copy &&
test_cmp_rev HEAD^"$expect_parent_num" copy &&
echo dirty >expect &&
test_cmp expect new_file &&
echo "modified again" >expect &&
@ -26,7 +28,7 @@ test_pull_autostash_fail () {
echo dirty >new_file &&
git add new_file &&
test_must_fail git pull "$@" . copy 2>err &&
test_i18ngrep "uncommitted changes." err
test_i18ngrep "\(uncommitted changes.\)\|\(overwritten by merge:\)" err
}
test_expect_success setup '
@ -369,22 +371,22 @@ test_expect_success '--rebase fails with multiple branches' '
test_expect_success 'pull --rebase succeeds with dirty working directory and rebase.autostash set' '
test_config rebase.autostash true &&
test_pull_autostash --rebase
test_pull_autostash 1 --rebase
'
test_expect_success 'pull --rebase --autostash & rebase.autostash=true' '
test_config rebase.autostash true &&
test_pull_autostash --rebase --autostash
test_pull_autostash 1 --rebase --autostash
'
test_expect_success 'pull --rebase --autostash & rebase.autostash=false' '
test_config rebase.autostash false &&
test_pull_autostash --rebase --autostash
test_pull_autostash 1 --rebase --autostash
'
test_expect_success 'pull --rebase --autostash & rebase.autostash unset' '
test_unconfig rebase.autostash &&
test_pull_autostash --rebase --autostash
test_pull_autostash 1 --rebase --autostash
'
test_expect_success 'pull --rebase --no-autostash & rebase.autostash=true' '
@ -402,13 +404,40 @@ test_expect_success 'pull --rebase --no-autostash & rebase.autostash unset' '
test_pull_autostash_fail --rebase --no-autostash
'
for i in --autostash --no-autostash
do
test_expect_success "pull $i (without --rebase) is illegal" '
test_must_fail git pull $i . copy 2>err &&
test_i18ngrep "only valid with --rebase" err
'
done
test_expect_success 'pull succeeds with dirty working directory and merge.autostash set' '
test_config merge.autostash true &&
test_pull_autostash 2
'
test_expect_success 'pull --autostash & merge.autostash=true' '
test_config merge.autostash true &&
test_pull_autostash 2 --autostash
'
test_expect_success 'pull --autostash & merge.autostash=false' '
test_config merge.autostash false &&
test_pull_autostash 2 --autostash
'
test_expect_success 'pull --autostash & merge.autostash unset' '
test_unconfig merge.autostash &&
test_pull_autostash 2 --autostash
'
test_expect_success 'pull --no-autostash & merge.autostash=true' '
test_config merge.autostash true &&
test_pull_autostash_fail --no-autostash
'
test_expect_success 'pull --no-autostash & merge.autostash=false' '
test_config merge.autostash false &&
test_pull_autostash_fail --no-autostash
'
test_expect_success 'pull --no-autostash & merge.autostash unset' '
test_unconfig merge.autostash &&
test_pull_autostash_fail --no-autostash
'
test_expect_success 'pull.rebase' '
git reset --hard before-rebase &&
@ -422,7 +451,7 @@ test_expect_success 'pull.rebase' '
test_expect_success 'pull --autostash & pull.rebase=true' '
test_config pull.rebase true &&
test_pull_autostash --autostash
test_pull_autostash 1 --autostash
'
test_expect_success 'pull --no-autostash & pull.rebase=true' '

View File

@ -29,15 +29,19 @@ Testing basic merge operations/option parsing.
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
printf '%s\n' 1 2 3 4 5 6 7 8 9 >file
printf '%s\n' '1 X' 2 3 4 5 6 7 8 9 >file.1
printf '%s\n' 1 2 3 4 '5 X' 6 7 8 9 >file.5
printf '%s\n' 1 2 3 4 5 6 7 8 '9 X' >file.9
printf '%s\n' 1 2 3 4 5 6 7 8 '9 Y' >file.9y
printf '%s\n' '1 X' 2 3 4 5 6 7 8 9 >result.1
printf '%s\n' '1 X' 2 3 4 '5 X' 6 7 8 9 >result.1-5
printf '%s\n' '1 X' 2 3 4 '5 X' 6 7 8 '9 X' >result.1-5-9
printf '%s\n' 1 2 3 4 5 6 7 8 '9 Z' >result.9z
test_write_lines 1 2 3 4 5 6 7 8 9 >file
cp file file.orig
test_write_lines '1 X' 2 3 4 5 6 7 8 9 >file.1
test_write_lines 1 2 '3 X' 4 5 6 7 8 9 >file.3
test_write_lines 1 2 3 4 '5 X' 6 7 8 9 >file.5
test_write_lines 1 2 3 4 5 6 7 8 '9 X' >file.9
test_write_lines 1 2 3 4 5 6 7 8 '9 Y' >file.9y
test_write_lines '1 X' 2 3 4 5 6 7 8 9 >result.1
test_write_lines '1 X' 2 3 4 '5 X' 6 7 8 9 >result.1-5
test_write_lines '1 X' 2 3 4 5 6 7 8 '9 X' >result.1-9
test_write_lines '1 X' 2 3 4 '5 X' 6 7 8 '9 X' >result.1-5-9
test_write_lines '1 X' 2 '3 X' 4 '5 X' 6 7 8 '9 X' >result.1-3-5-9
test_write_lines 1 2 3 4 5 6 7 8 '9 Z' >result.9z
create_merge_msgs () {
echo "Merge tag 'c2'" >msg.1-5 &&
@ -81,7 +85,7 @@ verify_head () {
}
verify_parents () {
printf '%s\n' "$@" >parents.expected &&
test_write_lines "$@" >parents.expected &&
>parents.actual &&
i=1 &&
while test $i -le $#
@ -95,7 +99,7 @@ verify_parents () {
}
verify_mergeheads () {
printf '%s\n' "$@" >mergehead.expected &&
test_write_lines "$@" >mergehead.expected &&
while read sha1 rest
do
git rev-parse $sha1
@ -675,6 +679,134 @@ test_expect_success 'refresh the index before merging' '
git merge c3
'
test_expect_success 'merge with --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git merge --autostash c2 2>err &&
test_i18ngrep "Applied autostash." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-5 merge-result &&
test_cmp result.1-5-9 file
'
test_expect_success 'merge with merge.autoStash' '
test_config merge.autoStash true &&
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git merge c2 2>err &&
test_i18ngrep "Applied autostash." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-5 merge-result &&
test_cmp result.1-5-9 file
'
test_expect_success 'fast-forward merge with --autostash' '
git reset --hard c0 &&
git merge-file file file.orig file.5 &&
git merge --autostash c1 2>err &&
test_i18ngrep "Applied autostash." err &&
test_cmp result.1-5 file
'
test_expect_success 'octopus merge with --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.3 &&
git merge --autostash c2 c3 2>err &&
test_i18ngrep "Applied autostash." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-5-9 merge-result &&
test_cmp result.1-3-5-9 file
'
test_expect_success 'conflicted merge with --autostash, --abort restores stash' '
git reset --hard c3 &&
cp file.1 file &&
test_must_fail git merge --autostash c7 &&
git merge --abort 2>err &&
test_i18ngrep "Applied autostash." err &&
test_cmp file.1 file
'
test_expect_success 'completed merge (git commit) with --no-commit and --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git diff >expect &&
git merge --no-commit --autostash c2 &&
git stash show -p MERGE_AUTOSTASH >actual &&
test_cmp expect actual &&
git commit 2>err &&
test_i18ngrep "Applied autostash." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-5 merge-result &&
test_cmp result.1-5-9 file
'
test_expect_success 'completed merge (git merge --continue) with --no-commit and --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git diff >expect &&
git merge --no-commit --autostash c2 &&
git stash show -p MERGE_AUTOSTASH >actual &&
test_cmp expect actual &&
git merge --continue 2>err &&
test_i18ngrep "Applied autostash." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-5 merge-result &&
test_cmp result.1-5-9 file
'
test_expect_success 'aborted merge (merge --abort) with --no-commit and --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git diff >expect &&
git merge --no-commit --autostash c2 &&
git stash show -p MERGE_AUTOSTASH >actual &&
test_cmp expect actual &&
git merge --abort 2>err &&
test_i18ngrep "Applied autostash." err &&
git diff >actual &&
test_cmp expect actual
'
test_expect_success 'aborted merge (reset --hard) with --no-commit and --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git diff >expect &&
git merge --no-commit --autostash c2 &&
git stash show -p MERGE_AUTOSTASH >actual &&
test_cmp expect actual &&
git reset --hard 2>err &&
test_i18ngrep "Autostash exists; creating a new stash entry." err &&
git diff --exit-code
'
test_expect_success 'quit merge with --no-commit and --autostash' '
git reset --hard c1 &&
git merge-file file file.orig file.9 &&
git diff >expect &&
git merge --no-commit --autostash c2 &&
git stash show -p MERGE_AUTOSTASH >actual &&
test_cmp expect actual &&
git diff HEAD >expect &&
git merge --quit 2>err &&
test_i18ngrep "Autostash exists; creating a new stash entry." err &&
git diff HEAD >actual &&
test_cmp expect actual
'
test_expect_success 'merge with conflicted --autostash changes' '
git reset --hard c1 &&
git merge-file file file.orig file.9y &&
git diff >expect &&
test_when_finished "test_might_fail git stash drop" &&
git merge --autostash c3 2>err &&
test_i18ngrep "Applying autostash resulted in conflicts." err &&
git show HEAD:file >merge-result &&
test_cmp result.1-9 merge-result &&
git stash show -p >actual &&
test_cmp expect actual
'
cat >expected.branch <<\EOF
Merge branch 'c5-branch' (early part)
EOF