All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Artur Bieniek via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Artur Bieniek <abieniek@antmicro.com>, Artur Bieniek <ar2rekb@gmail.com>
Subject: [PATCH] pull: add --hard mode
Date: Tue, 18 Aug 2026 11:34:33 +0000	[thread overview]
Message-ID: <pull.2384.git.git.1787052873141.gitgitgadget@gmail.com> (raw)

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

             reply	other threads:[~2026-08-18 11:34 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-18 11:34 Artur Bieniek via GitGitGadget [this message]
2026-08-18 14:48 ` [PATCH] pull: add --hard mode Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2384.git.git.1787052873141.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=abieniek@antmicro.com \
    --cc=ar2rekb@gmail.com \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.