Git development
 help / color / mirror / Atom feed
* [PATCH] pull: add --hard mode
@ 2026-08-18 11:34 Artur Bieniek via GitGitGadget
  2026-08-18 14:48 ` Junio C Hamano
  0 siblings, 1 reply; 2+ messages in thread
From: Artur Bieniek via GitGitGadget @ 2026-08-18 11:34 UTC (permalink / raw)
  To: git; +Cc: Artur Bieniek, Artur Bieniek

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(-)

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

^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-08-18 14:48 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 11:34 [PATCH] pull: add --hard mode Artur Bieniek via GitGitGadget
2026-08-18 14:48 ` Junio C Hamano

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox