Volume XXII, number 279Tuesday, October 6, 2026Latest message 9 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patchpull: add --hard mode

9 messages between Aug 18, 2026 and Aug 24, 2026, from Artur Bieniek via GitGitGadget, Junio C Hamano, Phillip Wood, Artur Bieniek, Kristoffer Haugsbakk.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Artur Bieniek via GitGitGadgetAug 18, 2026, 11:34 UTC on lore
From: Artur Bieniek <ar2rekb@gmail.com>

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 <ar2rekb@gmail.com>
---
    [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(-)
Show changes to 4 files +118 −9

Documentation/git-pull.adoc, builtin/pull.c, t/t5521-pull-options.sh, t/t5572-pull-submodule.sh

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
Junio C HamanoAug 18, 2026, 14:48 UTC in reply to Artur Bieniek via GitGitGadget on lore

Re: [PATCH] pull: add --hard mode

"Artur Bieniek via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 5 quoted lines
> From: Artur Bieniek <ar2rekb@gmail.com>
>
> 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.

There may be a population of users who *never* make changes to their history or working tree, and always want to "hard reset to the updated upstream". Doing so would be safe for them because they create nothing in their tree whose loss matters.

Giving them a convenient and safe way to do so might be worth considering, but the behavior is already safely and explicitly achieved by running 'git fetch' followed by 'git reset --hard @{u}', so I am not sure whether it is worth adding another way to do so.

More importantly, throwing it into 'git pull' feels very wrong.

The core purpose of 'git pull' is history integration. The command is designed to help those who make their own changes and advance history. Adding a destructive option to the command makes it easier for them to trigger it by accident, and unlike the main target of this new feature, they have things in their tree that they cannot afford to lose to accidents or mistakes.

So, I am mildly against adding anything of this sort to 'git pull'. For that matter, I am generally against making it convenient to discard or destroy history. I prefer to keep these destructive operations explicit, e.g., "fetch + reset --hard".

Thanks.
Phillip WoodAug 20, 2026, 09:58 UTC in reply to Junio C Hamano on lore

Re: [PATCH] pull: add --hard mode

On 18/08/2026 15:48, Junio C Hamano wrote:
Show 17 quoted lines
> "Artur Bieniek via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 
>> From: Artur Bieniek <ar2rekb@gmail.com>
>>
>> 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.
> 
> There may be a population of users who *never* make changes to their
> history or working tree, and always want to "hard reset to the
> updated upstream".  Doing so would be safe for them because they
> create nothing in their tree whose loss matters.
> 
> Giving them a convenient and safe way to do so might be worth
> considering, but the behavior is already safely and explicitly
> achieved by running 'git fetch' followed by 'git reset --hard @{u}',
> so I am not sure whether it is worth adding another way to do so.

I think if the design was slightly different so that it errored out by default if there were uncommitted changes then that would make it worth while as it is safer than "git fetch; git reset --hard @{u}" and would allow the user to carry over those changes with "--autostash". So to me something like

	git pull --reset [--discard-changes | --autostash]
would be a more convincing design.
Show 8 quoted lines
> More importantly, throwing it into 'git pull' feels very wrong.
> 
> The core purpose of 'git pull' is history integration.  The command
> is designed to help those who make their own changes and advance
> history.  Adding a destructive option to the command makes it easier
> for them to trigger it by accident, and unlike the main target of
> this new feature, they have things in their tree that they cannot
> afford to lose to accidents or mistakes.

If it refused to reset by default when there were uncommitted changes would that be safe enough? Uncommitted changes would be protected and any local commits that become unreachable after the reset can still be retrieved from the reflog. It's not quite the same as integrating remote and local changes, but more like updating the working copy.

Thanks
Phillip
Show 7 quoted lines
> So, I am mildly against adding anything of this sort to 'git pull'.
> For that matter, I am generally against making it convenient to
> discard or destroy history.  I prefer to keep these destructive
> operations explicit, e.g., "fetch + reset --hard".
> 
> Thanks.
> 
Junio C HamanoAug 20, 2026, 15:59 UTC in reply to Phillip Wood on lore

Re: [PATCH] pull: add --hard mode

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 15 quoted lines
> I think if the design was slightly different so that it errored out by 
> default if there were uncommitted changes then that would make it worth 
> while as it is safer than "git fetch; git reset --hard @{u}" and would 
> allow the user to carry over those changes with "--autostash". So to me 
> something like
>
> 	git pull --reset [--discard-changes | --autostash]
>
> would be a more convincing design.
> ...
> If it refused to reset by default when there were uncommitted changes 
> would that be safe enough? Uncommitted changes would be protected and 
> any local commits that become unreachable after the reset can still be 
> retrieved from the reflog. It's not quite the same as integrating remote 
> and local changes, but more like updating the working copy.

Yup, but git pull --ff-only serves the "No development is done in this repository; it is merely to keep the latest sources here" audience just fine.

What you are suggesting may be *useful* for those who agree with this statement:

    I do value my local changes because I haven't committed them,
    but I am willing to discard these changes and replace them with
    whatever the upstream did.

but I am not sure of the use case for a repository/working tree that is managed in such a way.

Artur BieniekAug 20, 2026, 16:43 UTC in reply to Junio C Hamano on lore

Re: [PATCH] pull: add --hard mode

One case where --ff-only does not seem to cover that audience is when the upstream branch itself is rewritten.

For example, a checkout may contain no local development at all and only be used to track the latest state of an upstream branch, but if that branch is rebased or otherwise force-updated, git pull --ff-only will refuse to update it because the histories have diverged.

That seems like a reasonably natural use case for the behavior Phillip described: git pull --reset on a clean working tree would mean "make this checkout match the fetched upstream", while still refusing by default to discard uncommitted changes.

I also like that distinction better than my original --hard proposal, since the destructive working-tree behavior would no longer be implicit in the primary option.

Thanks, Artur

On 8/20/26 5:59 PM, Junio C Hamano wrote:
Show 31 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>> I think if the design was slightly different so that it errored out by
>> default if there were uncommitted changes then that would make it worth
>> while as it is safer than "git fetch; git reset --hard @{u}" and would
>> allow the user to carry over those changes with "--autostash". So to me
>> something like
>>
>> 	git pull --reset [--discard-changes | --autostash]
>>
>> would be a more convincing design.
>> ...
>> If it refused to reset by default when there were uncommitted changes
>> would that be safe enough? Uncommitted changes would be protected and
>> any local commits that become unreachable after the reset can still be
>> retrieved from the reflog. It's not quite the same as integrating remote
>> and local changes, but more like updating the working copy.
> 
> Yup, but git pull --ff-only serves the "No development is done in
> this repository; it is merely to keep the latest sources here"
> audience just fine.
> 
> What you are suggesting may be *useful* for those who agree with
> this statement:
> 
>      I do value my local changes because I haven't committed them,
>      but I am willing to discard these changes and replace them with
>      whatever the upstream did.
> 
> but I am not sure of the use case for a repository/working tree
> that is managed in such a way.
Kristoffer HaugsbakkAug 20, 2026, 17:29 UTC in reply to Artur Bieniek on lore

Re: [PATCH] pull: add --hard mode

On Thu, Aug 20, 2026, at 18:43, Artur Bieniek wrote:
Show 7 quoted lines
> One case where --ff-only does not seem to cover that audience is when
> the upstream branch itself is rewritten.
>
> For example, a checkout may contain no local development at all and only
> be used to track the latest state of an upstream branch, but if that
> branch is rebased or otherwise force-updated, git pull --ff-only will
> refuse to update it because the histories have diverged.

I don’t understand what you need a branch for in that case. I just use the remote-tracking branch in that case (`origin/main` e.g.).

PS: Bottom posting is strongly preferred on this list.
> ...
Junio C HamanoAug 21, 2026, 01:09 UTC in reply to Artur Bieniek on lore

Re: [PATCH] pull: add --hard mode

Artur Bieniek <abieniek@antmicro.com> writes:
> One case where --ff-only does not seem to cover that audience is when 
> the upstream branch itself is rewritten.
It does, doesn't it?  

It is not like you want to always blindly follow them. Rewound upstream is w warning-worthy event that the user should be notified rather loudly, I would expect.

Artur BieniekAug 24, 2026, 10:55 UTC in reply to Kristoffer Haugsbakk on lore

Re: [PATCH] pull: add --hard mode

On 8/20/26 7:29 PM, Kristoffer Haugsbakk wrote:
Show 15 quoted lines
> On Thu, Aug 20, 2026, at 18:43, Artur Bieniek wrote:
>> One case where --ff-only does not seem to cover that audience is when
>> the upstream branch itself is rewritten.
>>
>> For example, a checkout may contain no local development at all and only
>> be used to track the latest state of an upstream branch, but if that
>> branch is rebased or otherwise force-updated, git pull --ff-only will
>> refuse to update it because the histories have diverged.
> 
> I don’t understand what you need a branch for in that case. I just use
> the remote-tracking branch in that case (`origin/main` e.g.).
> 
> PS: Bottom posting is strongly preferred on this list.
> 
>> ...

The branch itself is not essential; the point is to update the checked-out working tree as well. git fetch updates origin/main, but leaves the checkout at the old commit.

If resetting does not belong under pull, would the inverse design be more appropriate, e.g. git reset --pull --hard, meaning “fetch the configured upstream and reset to it”? Or would making reset perform network I/O be undesirable for the same reason?

Thanks, Artur

Junio C HamanoAug 24, 2026, 14:11 UTC in reply to Artur Bieniek on lore

Re: [PATCH] pull: add --hard mode

Artur Bieniek <abieniek@antmicro.com> writes:
> If resetting does not belong under pull, would the inverse design be 
> more appropriate, e.g. git reset --pull --hard, meaning “fetch the 
> configured upstream and reset to it”? Or would making reset perform 
> network I/O be undesirable for the same reason?

Very true. I wonder if the feature of git you should be looking at for doing things like this is not "pull", "fetch", or "reset" but is "alias"?

Back to recent threads