[PATCH] pull: add --hard mode
"Artur Bieniek via GitGitGadget" <[email protected]>
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <[email protected]> |
From: Artur Bieniek <[email protected]> Add --hard as an explicit alternative to merge and rebase. After fetching, require a single integration candidate and reset the current branch, index, and working tree to it. Preserve quiet and submodule recursion behavior, and reject options that cannot be honored by a hard reset. Document the destructive semantics and cover them in tests. Signed-off-by: Artur Bieniek <[email protected]> --- [RFC] Add git pull --hard mode Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2384%2FArturBieniek4%2Fpull-hard-v1 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2384/ArturBieniek4/pull-hard-v1 Pull-Request: https://github.com/git/git/pull/2384 Documentation/git-pull.adoc | 10 +++++- builtin/pull.c | 68 ++++++++++++++++++++++++++++++++----- t/t5521-pull-options.sh | 31 +++++++++++++++++ t/t5572-pull-submodule.sh | 18 ++++++++++ 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/Documentation/git-pull.adoc b/Documentation/git-pull.adoc index 88f4fd3926..3a103a1630 100644 --- a/Documentation/git-pull.adoc +++ b/Documentation/git-pull.adoc @@ -24,7 +24,7 @@ with no arguments this defaults to the <<UPSTREAM-BRANCHES,upstream>> for the current branch. Then it integrates that branch into the current branch. -There are 4 main options for integrating the remote branch: +There are 5 main options for integrating the remote branch: 1. `git pull --ff-only` will only do "fast-forward" updates: it fails if your local branch has diverged from the remote branch. @@ -32,6 +32,7 @@ There are 4 main options for integrating the remote branch: 2. `git pull --rebase` runs `git rebase` 3. `git pull --no-rebase` runs `git merge`. 4. `git pull --squash` runs `git merge --squash` +5. `git pull --hard` runs `git reset --hard` to the fetched branch. You can also set the configuration options `pull.rebase`, `pull.squash`, or `pull.ff` with your preferred behaviour. @@ -119,6 +120,13 @@ unless you have read linkgit:git-rebase[1] carefully. `--no-rebase`:: This is shorthand for `--rebase=false`. +`--hard`:: + Reset the current branch, index, and working tree to the fetched branch. + The pull must select exactly one branch. Local commits and changes to + tracked files are discarded. Untracked files or directories in the way + of writing tracked files may also be deleted. This option cannot be + combined with merge or rebase options, or with `--append`. + Options related to fetching ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/builtin/pull.c b/builtin/pull.c index db3ee0aab3..8324f06084 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -91,6 +91,7 @@ static char *opt_ff; static const char *opt_verify_signatures; static const char *opt_verify; static int opt_autostash = -1; +static int opt_hard; static int config_rebase_autostash; static int config_pull_autostash = -1; static int check_trust_level = 1; @@ -318,7 +319,9 @@ static void NORETURN die_no_merge_candidates(const char *repo, const char **refs const char *remote = curr_branch ? curr_branch->remote_name : NULL; if (*refspecs) { - if (opt_rebase) + if (opt_hard) + fprintf_ln(stderr, _("There is no candidate for resetting to among the refs that you just fetched.")); + else if (opt_rebase) fprintf_ln(stderr, _("There is no candidate for rebasing against among the refs that you just fetched.")); else fprintf_ln(stderr, _("There are no candidates for merging among the refs that you just fetched.")); @@ -331,7 +334,9 @@ static void NORETURN die_no_merge_candidates(const char *repo, const char **refs repo); } else if (!curr_branch) { fprintf_ln(stderr, _("You are not currently on a branch.")); - if (opt_rebase) + if (opt_hard) + fprintf_ln(stderr, _("Please specify which branch you want to reset to.")); + else if (opt_rebase) fprintf_ln(stderr, _("Please specify which branch you want to rebase against.")); else fprintf_ln(stderr, _("Please specify which branch you want to merge with.")); @@ -346,7 +351,9 @@ static void NORETURN die_no_merge_candidates(const char *repo, const char **refs remote_name = _("<remote>"); fprintf_ln(stderr, _("There is no tracking information for the current branch.")); - if (opt_rebase) + if (opt_hard) + fprintf_ln(stderr, _("Please specify which branch you want to reset to.")); + else if (opt_rebase) fprintf_ln(stderr, _("Please specify which branch you want to rebase against.")); else fprintf_ln(stderr, _("Please specify which branch you want to merge with.")); @@ -358,7 +365,11 @@ static void NORETURN die_no_merge_candidates(const char *repo, const char **refs fprintf(stderr, "\n"); fprintf_ln(stderr, " git branch --set-upstream-to=%s/%s %s\n", remote_name, _("<branch>"), curr_branch->name); - } else + } else if (opt_hard) + fprintf_ln(stderr, _("Your configuration specifies to reset to the ref '%s'\n" + "from the remote, but no such ref was fetched."), + curr_branch->merge[0]->src); + else fprintf_ln(stderr, _("Your configuration specifies to merge with the ref '%s'\n" "from the remote, but no such ref was fetched."), curr_branch->merge[0]->src); @@ -570,6 +581,23 @@ static int run_merge(void) return run_command(&cmd); } +static int run_reset(const struct object_id *oid) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "reset", "--hard", NULL); + if (opt_verbosity < 0) + strvec_push(&cmd.args, "--quiet"); + if (recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) + strvec_push(&cmd.args, "--recurse-submodules"); + else if (recurse_submodules == RECURSE_SUBMODULES_OFF) + strvec_push(&cmd.args, "--no-recurse-submodules"); + strvec_push(&cmd.args, oid_to_hex(oid)); + cmd.git_cmd = 1; + return run_command(&cmd); +} + /** * Returns remote's upstream branch for the current branch. If remote is NULL, * the current branch's configured default remote is used. Returns NULL if @@ -925,6 +953,8 @@ int cmd_pull(int argc, PARSE_OPT_NOARG), OPT_BOOL(0, "autostash", &opt_autostash, N_("automatically stash/stash pop before and after")), + OPT_BOOL(0, "hard", &opt_hard, + N_("reset hard to the fetched branch")), OPT_PASSTHRU_ARGV('s', "strategy", &opt_strategies, N_("strategy"), N_("merge strategy to use"), 0), @@ -1022,6 +1052,16 @@ int cmd_pull(int argc, } argc = parse_options(argc, argv, prefix, pull_options, pull_usage, 0); + if (opt_hard && + (opt_rebase >= 0 || opt_diffstat || opt_log || opt_signoff || + opt_squash || opt_commit || opt_edit || cleanup_arg || opt_ff || + opt_verify_signatures || opt_verify || opt_autostash >= 0 || + opt_strategies.nr || opt_strategy_opts.nr || opt_gpg_sign || + opt_allow_unrelated_histories)) + die(_("--hard cannot be combined with merge or rebase options")); + die_for_incompatible_opt2(opt_hard, "--hard", + opt_append && !strcmp(opt_append, "--append"), + "--append"); if (opt_autostash == -1) opt_autostash = config_pull_autostash; @@ -1037,7 +1077,7 @@ int cmd_pull(int argc, parse_repo_refspecs(argc, argv, &repo, &refspecs); - if (!opt_ff) { + if (!opt_hard && !opt_ff) { opt_ff = xstrdup_or_null(config_get_ff()); /* * A subtle point: opt_ff was set on the line above via @@ -1056,13 +1096,15 @@ int cmd_pull(int argc, } } - if (opt_rebase < 0) + if (opt_hard) + opt_rebase = REBASE_FALSE; + else if (opt_rebase < 0) opt_rebase = config_get_rebase(&rebase_unspecified); - if (repo_read_index_unmerged(the_repository)) + if (!opt_hard && repo_read_index_unmerged(the_repository)) die_resolve_conflict("pull"); - if (file_exists(git_path_merge_head(the_repository))) + if (!opt_hard && file_exists(git_path_merge_head(the_repository))) die_conclude_merge(); if (repo_get_oid(the_repository, "HEAD", &orig_head)) @@ -1090,6 +1132,16 @@ int cmd_pull(int argc, if (opt_dry_run) return 0; + if (opt_hard) { + get_merge_heads(&merge_heads); + if (!merge_heads.nr) + die_no_merge_candidates(repo, refspecs); + if (merge_heads.nr > 1) + die(_("Cannot hard reset to multiple branches.")); + ret = run_reset(merge_heads.oid); + goto cleanup; + } + if (repo_get_oid(the_repository, "HEAD", &curr_head)) oidclr(&curr_head, the_repository->hash_algo); diff --git a/t/t5521-pull-options.sh b/t/t5521-pull-options.sh index 5e420c208c..31bb76b465 100755 --- a/t/t5521-pull-options.sh +++ b/t/t5521-pull-options.sh @@ -117,6 +117,37 @@ test_expect_success 'git pull --force' ' ) ' +test_expect_success 'git pull --hard' ' + test_when_finished "rm -rf hard-parent hard" && + git init hard-parent && + test_commit -C hard-parent base && + git clone hard-parent hard && + test_commit -C hard local && + test_commit -C hard-parent upstream obstruct upstream && + git -C hard-parent branch side && + ( + cd hard && + echo dirty >base.t && + mkdir obstruct && + echo untracked >obstruct/file && + test_must_fail git pull --hard --ff-only 2>err && + test_grep "cannot be combined" err && + test_must_fail git pull --hard -a 2>err && + test_grep "options .*--hard.* and .*--append.*" err && + test_must_fail git pull --hard origin main side 2>err && + test_grep "Cannot hard reset to multiple branches" err && + git pull --hard && + test_cmp_rev HEAD origin/main && + test_path_is_missing local.t && + test_path_is_file obstruct && + git diff --quiet && + git diff --cached --quiet && + git pull -q --hard >out 2>quiet-err && + test_must_be_empty out && + test_must_be_empty quiet-err + ) +' + test_expect_success 'git pull --all' ' mkdir clonedmulti && (cd clonedmulti && git init && diff --git a/t/t5572-pull-submodule.sh b/t/t5572-pull-submodule.sh index 42d14328b6..f2df277b79 100755 --- a/t/t5572-pull-submodule.sh +++ b/t/t5572-pull-submodule.sh @@ -106,6 +106,24 @@ test_expect_success " --[no-]recurse-submodule and submodule.recurse" ' test_path_is_file super/sub/merge_strategy_4.t ' +test_expect_success 'pull --hard honors submodule recursion' ' + test_commit -C child hard_recurse && + git -C parent submodule update --remote && + git -C parent add sub && + git -C parent commit -m "update submodule" && + + git -C super pull --hard --recurse-submodules && + test_path_is_file super/sub/hard_recurse.t && + + test_commit -C child hard_no_recurse && + git -C parent submodule update --remote && + git -C parent add sub && + git -C parent commit -m "update submodule" && + + git -C super -c submodule.recurse=true pull --hard --no-recurse-submodules && + test_path_is_missing super/sub/hard_no_recurse.t +' + test_expect_success "fetch.recurseSubmodules option triggers recursive fetch (but not recursive update)" ' test_commit -C child merge_strategy_5 && # Omit the parent commit, otherwise this passes with the base-commit: 745601a9a94110d74769ab605ccd4f61339758d2 -- gitgitgadget