Why git can't do hard/soft resets by path?
Asked Answered
R

9

192

$ git reset -- <file_path> can reset by path.

However, $ git reset (--hard|--soft) <file_path> will report an error like below:

Cannot do hard|soft reset with paths.
Randell answered 26/6, 2012 at 4:36 Comment(2)
This behaviour can now be achieved with git restore. git checkout doesn't do a hard reset.Wickiup
@IanKemp You're free to be as bitter as you want about git, but the only unhelpful thing here is your comment. (And now mine. Let's erase them both :-)Platen
P
196

Because there's no point (other commands provide that functionality already), and it reduces the potential for doing the wrong thing by accident.

A "hard reset" for a path is just done with git checkout HEAD -- <path> (checking out the existing version of the file).

A soft reset for a path doesn't make sense.

A mixed reset for a path is what git reset -- <path> does.

Penuche answered 26/6, 2012 at 4:39 Comment(16)
Personally, I think git checkout -- <path> should be replaced with git reset --hard <path>. It makes so much more sense...Severus
git checkout -- <path> doesn't do a hard reset; it replaces the working tree contents with the staged contents. git checkout HEAD -- <path> does a hard reset for a path, replacing both the index and the working tree with the version from the HEAD commit.Dialytic
Please take a look at the linked question: #15405035. Any idea?Speechless
@Severus git reset --hard <path> is invalid, I get a git error fatal: Cannot do hard reset with paths.. The same issue is true with --soft. Using git checkout HEAD -- <path> described by @Penuche does the trick.Loginov
@EdPlunkett Er, the second sentence in the answer tells you what other command provides the functionality.Penuche
-1: Checkout to said revision will not remove files from the working copy if said revision contains deleted files. reset --hard with a path would provide this missing piece. Git is already so powerful that the "We don't let you do this for your own protection" excuse holds zero water: There are plenty of ways to do the wrong thing "by accident". None of that matters anyway when you have git reflog.Interknit
@Interknit clean is the only git command that will delete untracked files from the working copy. Even reset --hard without paths won't delete untracked files, so adding path support to reset --hard presumably wouldn't provide that piece, either.Dialytic
"Because there's no point", I disagree with this view; it is like saying "There is no point in having for loops if you have goto statements", intuitiveness and clarity matter.Ehrman
as mentioned by @Interknit checkout won't remove files. If you want that behavior then look at this answer. Still, I hope some day we'll get git reset --hard -- <path>. There are legitimate use cases for it.Lactoflavin
Small note to my previous comment. I see there is actually ongoing work to get this feature (maybe even deprecating git reset <tree-ish> -- <pathspec>) :)Lactoflavin
there is a point if you're trying to pull in changes from another branch without staging them. the soft flag seems, intuitively, like one way you could do that. #56030023Clothesline
@Clothesline that's an extremely niche use case and can be accomplished by using git checkout and then git reset to unstage the now-changed paths. It's not really worth adding more (potentially mistaken-prone) functionality to git reset to support it more directly.Penuche
@Mariusz On the contrary: --soft is used to keep snapshots delta IN STAGE (--mixed will make them 'modified/unstaged') So what you suggest doesn't make sense as "without staging"Tabernacle
@Ehrman git config --global alias.reset-h 'checkout --', where do you see a problem??Tabernacle
As others, such as @void.pointer, have stated, git checkout HEAD -- <path> isn't enough, but, this is: https://mcmap.net/q/11242/-why-git-can-39-t-do-hard-soft-resets-by-path. I just wrote up an answer and did a bunch of testing. As far as I can tell, I've got the full and correct solution there.Superincumbent
@MariuszPawelski: I just learned about git restore.Wickiup
B
24

You can accomplishment what you're trying to do using git checkout HEAD <path>.

That said, the provided error message makes no sense to me (as git reset works just fine on subdirectories), and I see no reason why git reset --hard shouldn't do exactly what you're asking of it.

Boxhaul answered 22/12, 2014 at 21:30 Comment(1)
using checkout stages the changes, which is not the same as a reset --softClothesline
T
16

The question how is already answered, I'll explain the why part.

So, what does git reset do? Depending on the parameters specified, it can do two different things:

  • If you specify a path, it replaces the matched files in the index with the files from a commit (HEAD by default). This action doesn't affect the working tree at all and is usually used as the opposite of git add.

  • If you don't specify a path, it moves the current branch head to a specified commit and, together with that, optionally resets the index and the working tree to the state of that commit. This additional behavior is controlled by the mode parameter:
    --soft: don't touch the index and the working tree.
    --mixed (default): reset the index but not the working tree.
    --hard: reset the index and the working tree.
    There are also other options, see the documentation for the full list and some use cases.

    When you don't specify a commit, it defaults to HEAD, so git reset --soft will do nothing, as it is a command to move the head to HEAD (to its current state). git reset --hard, on the other hand, makes sense due to its side effects, it says move the head to HEAD and reset the index and the working tree to HEAD.

    I think it should be clear by now why this operation is not for specific files by its nature - it is intended to move a branch head in the first place, resetting the working tree and the index is secondary functionality.

Transfinite answered 20/9, 2017 at 18:23 Comment(3)
it's clear that reset is intended to move a branch head in the first place, but since it has the additional functionality of resetting the working tree and the index for entire commits and the functionality of resetting the index for specific files, why doesn't it have the functionality of resetting the working tree for specific files? I believe that's what the OP is asking.Lubricant
Maybe because that functionality (resetting the working tree for specific files) is already available as the git checkout command? And making reset to do the same thing would confuse users further. My answer was that --hard option is not applicable to specific files because it is a mode for branch reset, not index reset. And working tree reset is named checkout, as you can read in other answers. All of that is just a bad design of Git's user interface, IMHO.Transfinite
Comparing first option to git checkout: git reset -- sets index only, while git checkout -- sets working tree only?Splore
C
8

Make sure you put a slash between origin or upstream (source) and the actual branch:

git reset --hard origin/branch

or

git reset --hard upstream/branch`
Chewy answered 27/7, 2019 at 23:33 Comment(3)
This doesn't answer the question at all.Exceptional
Just found this issue via Google and this was indeed my mistake. So it's a helpful answer anyways.Zoubek
Some people (like me) may be getting the OP's error due to a command like git reset --hard origin my/branchLandwehr
D
6

There's a very important reason behind that: the principles of checkout and reset.

In Git terms, checkout means "bring into the current working tree". And with git checkout we can fill the working tree with data from any area, being it from a commit in the repository or individual files from a commit or the staging area (which is the even the default).

In turn, git reset doesn't have this role. As the name suggests, it will reset the current ref but always having the repository as a source, independently of the "reach" (--soft, --mixed or --hard).

Recap:

  • checkout: From anywhere (index / repo commit) -> working tree
  • reset: Repo commit -> Overwrite HEAD (and optionally index and working tree)

Therefore what can be a bit confusing is the existence of git reset COMMIT -- files since "overwriting HEAD" with only some files doesn't make sense!

In the absence of an official explanation, I can only speculate that the git developers found that reset was still the best name of a command to discard changes made to the staging area and, given the only data source was the repository, then "let's extend the functionality" instead of creating a new command.

So somehow git reset -- <files> is already a bit exceptional: it won't overwrite the HEAD. IMHO all such variations would be exceptions. Even if we can conceive a --hard version, others (for example --soft) wouldn't make sense.

Dwarfism answered 21/1, 2019 at 1:50 Comment(1)
I like this answer. Really, git reset -- <files> fell like it was added because this is useful feature but no one was sure in which command it should be put. Luckily now we have much more sane git restore which have functionality of git checkout -- <path> git checkout <commit> -- <path> and git reset [<commit>] -- <path> with much saner defaults and even more features you couldn't do before (Contrary to what accepted answer says. Now you can finally easily restore just working tree, without touching index).Lactoflavin
S
4

This answer is now referenced by my broader answer here: All about checking out files or directories in git.

Why git can't do hard/soft resets by path?

It can. It just requires several commands is all, instead of just one. Here is how:

Summary

WARNING: git status should be TOTALLY CLEAN before beginning this process! Otherwise, you risk PERMANENTLY LOSING any uncommitted changes shown by git status, since git clean -fd 'f'orce deletes ALL files and 'd'irectories which are in your current working tree (file system), but which are not in the path you specify below in commit or branch commit_hash. Therefore, anything NOT already committed gets permanently lost as though you had used rm on it!

1. How to do a --soft reset by path:

# How to "soft reset" "path/to/some/file_or_dir" to its state exactly as it was
# at commit or branch `commit_hash`.
#
# SEE WARNING ABOVE!

git reset commit_hash -- path/to/some/file_or_dir
git checkout-index -fa
git clean -fd  # SEE WARNING ABOVE!

2. How to do a --hard reset by path:

# How to "hard reset" "path/to/some/file_or_dir" to its state exactly as it was
# at commit or branch `commit_hash`.
#
# SEE WARNING ABOVE!

git reset commit_hash -- path/to/some/file_or_dir
git checkout-index -fa
git clean -fd  # SEE WARNING ABOVE!
git commit -m "hard reset path/to/some/file_or_dir to its state \
as it was at commit_hash"

Full answer:

Tested in git version 2.17.1 (check yours with git --version).

Why git can't do hard/soft resets by path?

I don't know why exactly, but I'd guess because git either made a development decision which both you and I disagree with, or because git simply is incomplete, and still needs to implement this. See also the additional insight on this provided below the "--hard reset by path" section below. A true --hard reset on a single path can't be done in the same way as a --hard reset for an entire branch.

BUT, we can accomplish the desired behavior manually with a few commands. Note that git checkout commit_hash -- path/to/some/file_or_dir alone is NOT one of them, due to the reasons explained below.

Before continuing, you should understand what git reset does, what is a working tree, index, and what --soft and --hard normally do with git reset. If you have any questions about these topics, read the "Background knowledge" section below first.

How to do a --soft or --hard git reset by path

AKA: How to accomplish the equivalent of either of these invalid commands manually:

# hypothetical commands not allowed in git, since `git` does NOT 
# allow `--soft` or `--hard` resets on paths

git reset --soft commit_hash -- path/to/some/file_or_dir
git reset --hard commit_hash -- path/to/some/file_or_dir

Since the above commands are NOT allowed, and since this checkout command does NOT do the same as what those hypothetical commands above would do, since this checkout command does NOT also delete files or folders existing locally which are not in commit_hash:

git checkout commit_hash -- path/to/some/file_or_dir

...then, you can accomplish what the hypothetical commands above would do with these several commands below, together.

1. --soft reset by path

Description: Make your local path/to/some/file_or_dir identical to what that file_or_dir looks like at commit_hash, while also deleting files in your local path directory (if path/to/some/file_or_dir is a directory) that do NOT exist in the directory at commit_hash. Leave all changes "staged" (added but not committed) in the end.

git reset commit_hash -- path/to/some/file_or_dir
git checkout-index -fa
git clean -fd

The above results are exactly what I'd expect from a --soft reset on a path if such a command were allowed.

For more information on the git checkout-index -fa and git clean -fd parts, see my other answer here: Using git, how do you reset the working tree (local file system state) to the state of the index ("staged" files)?.

Note that you should run a git status after each of the individual commands to see what each command is doing as you go. Here are the explanations of the individual commands:

# Stage some changes in path/to/some/file_or_dir, by adding them to the index,
# to show how your local path/to/some/file_or_dir SHOULD look in order to
# match what it looks like at `commit_hash`, but do NOT actually _make_ 
# those changes in yourlocal file system. Rather, simply "untrack" files 
# which should be deleted, and do NOT stage for commit changes which should 
# NOT have been made in order to match what's in `commit_hash`.
git reset commit_hash -- path/to/some/file_or_dir
git status

# Now, delete and discard all your unstaged changes.

# First, copy your index (staged/added changes) to your working file 
# tree (local file system). See this answer for these details:
# https://mcmap.net/q/11276/-using-git-how-do-you-reset-the-working-tree-local-file-system-state-to-the-state-of-the-index-quot-staged-quot-files
# and https://mcmap.net/q/11279/-how-do-i-discard-unstaged-changes-in-git
git checkout-index -fa
git status

# 'f'orce clean, including 'd'irectories. This means to **delete** 
# untracked local files and folders. The `git reset` command above
# is what made these untracked. `git clean -fd` is what actually 
# removes them.
git clean -fd
git status

2. --hard reset by path

Description: Do the --soft reset steps above, then also commit the changes:

git reset commit_hash -- path/to/some/file_or_dir
git checkout-index -fa
git clean -fd
git commit -m "hard reset path/to/some/file_or_dir to its state \
as it was at commit_hash"

Now, for good measure and as a final check, you can run git reset commit_hash -- path/to/some/file_or_dir, followed by git status. You will see that git status shows no changes whatsoever, because the --hard reset by path was successful above, so this call to git reset commit_hash -- path/to/some/file_or_dir did nothing. Excellent; it worked!

These results aren't quite the same as a true --hard reset, because a true --hard reset does not add a new commit with git commit. Rather, it simply forces your currently-checked-out branch to point to that other commit_hash. But, when "hard resetting" only a few files or paths like this, you can't just move your branch pointer to point to that other commit_hash, so there's really no other way to implement a reasonable behavior for this command than to also add a new commit with these "un-added", or "reset", changes, as done above.

This insight may also be the reason git doesn't natively support the --hard reset option by path; perhaps it's because a --hard reset by path would require adding a new commit, which deviates slightly from the normal --hard behavior of NOT adding new commits, but rather just "resetting" to (moving a branch pointer to) a given commit.

That doesn't natively explain why git won't allow at least --soft git resets by path, however, as that seems more standard to me.

Background knowledge

1. basic git terminology

As you're reading through the man git reset pages, you need to understand a few git terms:

  1. working tree = the local file system; this refers to the files and folders in the state you see them when you navigate your file system in the terminal or in a GUI file manager like nemo, nautilus or thunar.
  2. <tree-ish> = a commit hash or branch name
  3. index = what you see in green when you run git status. These are all the changes which are git added ("staged"), but NOT yet commited. When you run git add some_file you are "staging" some_file by moving its changes to the index. You can now say some_file is "added", "staged", or "in the index" (all the same thing).

2. man git reset pages

As you're reading through these solutions, it is insightful and helpful to note that man git reset states (emphasis added):

git reset <paths> is the opposite of git add <paths>.

In other words, git reset commit_hash -- some_file_or_dir can "un-add", or add the opposite changes of (thereby undoing those changes) some_file_or_dir, as contained in commit or branch commit_hash, while also setting HEAD to point to commit_hash, or to be as though it pointed to commit_hash for a specified file or directory (again, by adding the necessary changes to make some_file_or_dir in the working tree look like some_file_or_dir at commit_hash.

Also, in git lingo, "working tree" means "your local file system" (as your computer normally sees files and folders in a folder manager or when navigating in a terminal), and "index" or "index file" means "the place where files go when you git add, or 'stage' them." When you run git status, all files shown in green are "staged", or in the "index" or "index file" (same thing). (Source: What's the difference between HEAD, working tree and index, in Git?).

Now, with that in mind, here are some important parts from man git reset:

git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]

In the third form [the form shown above], set the current branch head (HEAD) to <commit>, optionally modifying index and working tree to match. The <tree-ish>/<commit> defaults to HEAD in all forms.

and:

git reset [-q] [<tree-ish>] [--] <paths>...
    This form resets the index entries for all <paths> to
    their state at <tree-ish>. (It does not affect the working
    tree or the current branch.)
    
    **This means that `git reset <paths>` is the opposite of `git
    add <paths>`.**
      
    After running git reset <paths> to update the index entry,
    you can use git-checkout(1) to check the contents out of
    the index to the working tree. Alternatively, using git-
    checkout(1) and specifying a commit, you can copy the
    contents of a path out of a commit to the index and to the
    working tree in one go.

and:

git reset [<mode>] [<commit>]
    This form resets the current branch head to <commit> and
    possibly updates the index (resetting it to the tree of
    <commit>) and the working tree depending on <mode>. If
    <mode> is omitted, defaults to "--mixed". The <mode> must
    be one of the following:
     
    --soft
        Does not touch the index file or the working tree at
        all (but resets the head to <commit>, just like all
        modes do). This leaves all your changed files "Changes
        to be committed", as git status would put it.
     
    --mixed
        Resets the index but not the working tree (i.e., the
        changed files are preserved but not marked for commit)
        and reports what has not been updated. This is the
        default action.
     
        If -N is specified, removed paths are marked as
        intent-to-add (see git-add(1)).
     
    --hard
        Resets the index and working tree. Any changes to
        tracked files in the working tree since <commit> are
        discarded.

3. You should also get familiar with the man git checkout-index page.

Remember, the "index" contains all added or "staged" files (shown in green when you run git status), and the "working tree" refers to your actual, local file system (containing also the changes shown in red when you run git status).

At the most basic level, here's what it does:

From man git checkout-index:

NAME
       git-checkout-index - Copy files from the index to the working tree

and:

-f, --force
    forces overwrite of existing files

-a, --all
    checks out all files in the index. Cannot be used together with
    explicit filenames.

References:

  1. [my answer--directly applicable, and the precursor answer I needed to be able to write this entire answer above!] Using git, how do you reset the working tree (local file system state) to the state of the index ("staged" files)?
  2. How to remove local (untracked) files from the current Git working tree
  3. How do I discard unstaged changes in Git?
  4. What's the difference between HEAD, working tree and index, in Git?

Related:

  1. [my answer] How to get just one file from another branch?
Superincumbent answered 9/3, 2021 at 2:3 Comment(0)
H
0

Explanation

The git reset manual lists 3 ways of invocation:

  • 2 are file-wise: These do not affect the working tree, but operate only on the files in the index specified by <paths>:

    • git reset [-q] [<tree-ish>] [--] <paths>..
    • git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
  • 1 is commit-wise: Operates on all files in the referenced <commit>, and may affect the working tree:

    • git reset [<mode>] [<commit>]

There's no mode of invocation that operates only on specified files and affects the working tree.

Workaround

If you want to both:

  • Reset the index/cache version of a file(s)
  • Checkout the file(s) (ie, make the working tree match the index and commit version)

You can use this alias in your git config file:

[alias]
  reco   = !"cd \"${GIT_PREFIX:-.}\" && git reset \"$@\" && git checkout \"$@\" && git status --short #"  # Avoid: "fatal: Cannot do hard reset with paths."

You can then do one of:

$ git reco <paths>

$ git reco <branch/commit> <paths>

$ git reco -- <paths>

(Mnenonic for reco: reset && checkout)

Heterogenetic answered 13/9, 2018 at 12:7 Comment(0)
N
0

As mentioned in the comments, the top answer is somewhat outdated, because we now have git restore which replaces the (strange)

git checkout HEAD -- path/to/file

with

git restore path/to/file

This is similar to how lots of checkout's large functionality space is getting split up to different sub-commands.

Nihi answered 18/3 at 11:17 Comment(0)
N
-3

git reset --soft HEAD~1 filename undo the commit but changes remain in local. filename could be -- for all commited files

Northnortheast answered 25/8, 2015 at 21:21 Comment(1)
fatal Cannot do soft reset with paths.Yoicks

© 2022 - 2024 — McMap. All rights reserved.