Skip to contentSkip to conversation

pull: add --hard mode

PATCH In review Artur Bieniek via GitGitGadget 5 participants 9 messages Aug 18 – Aug 24 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 pull: add --hard mode

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.

---

    [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(-)
Documentation/git-pull.adoc +9 -1
index 88f4fd3926..3a103a1630 100644
@@ -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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
builtin/pull.c +60 -8

138 lines not on this page · raw message

t/t5521-pull-options.sh +31 -0
index 5e420c208c..31bb76b465 100755
@@ -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 &&
t/t5572-pull-submodule.sh +18 -0
index 42d14328b6..f2df277b79 100755
@@ -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

Signature · 1 line
gitgitgadget
#1 lore raw
Quoted 5 lines · Artur Bieniek via GitGitGadget

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.

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.

#2 lore raw
Quoted 13 lines · Junio C Hamano
Quoted 5 lines · Artur Bieniek via GitGitGadget

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.

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.

Quoted 4 lines

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.

Quoted 8 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

Quoted 7 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.

#3 lore raw
Quoted 8 lines · Phillip Wood

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]
Quoted 7 lines

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.

#4 lore raw

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

Quoted 31 lines · Junio C Hamano
Quoted 8 lines · Phillip Wood

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]
Quoted 7 lines

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.

#5 lore raw
Quoted 7 lines · Artur Bieniek

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.

Quoted 1 line

...

#6 lore raw
Quoted 9 lines · Kristoffer Haugsbakk
Quoted 7 lines · Artur Bieniek

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.

Quoted 6 lines

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.

Quoted 1 line

...

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

#7 lore raw
Quoted 4 lines · Artur Bieniek

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"?

#8 lore raw
In reply to Artur Bieniek
Quoted 2 lines · Artur Bieniek

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.