Why Git Can't Do Hard/Soft Resets by Path?

$ 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.
2

8 Answers

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.

16

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.

1

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.

3

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`
2

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.

1

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"
Marcus Vance

Marcus Vance

Cybersecurity & Digital Privacy Researcher

Marcus Vance is a cybersecurity auditor and technology writer dedicated to educating the public about online safety, data privacy regulations, enterprise security, and emerging cyber threats.