为什么git不能通过路径进行硬重置/软重置?

191

$ git reset -- <file_path>可以按路径重置。

然而,$ git reset (--hard|--soft) <file_path>会报告以下错误:

Cannot do hard|soft reset with paths.

2
现在可以使用git restore实现此行为。git checkout不会进行硬重置。 - Patrick
因为git被设计成尽可能晦涩和不友好。 - undefined
8个回答

194

因为没有必要(其他命令已经提供了该功能),而且这样可以减少意外执行错误操作的可能性。

对于路径的“硬重置”只需使用 git checkout HEAD -- <path> 命令(将文件恢复到现有版本)。

路径的“软重置”无意义。

对于路径的“混合重置”,可以使用 git reset -- <path> 命令实现。


108
个人认为 git checkout -- <path> 应该用 git reset --hard <path> 来替代,这样更加合理... - vergenzt
33
git checkout -- <path> 不会进行硬重置,它会用暂存区的内容替换工作目录中的内容。git checkout HEAD -- <path> 会对指定路径进行硬重置,将索引和工作目录都替换为HEAD提交中的版本。 - Dan Fabulich
25
如果指定版本中有删除的文件,切换到该版本不会从工作副本中删除这些文件。使用带路径参数的 reset --hard 命令可以提供这一缺失的功能。Git 已经非常强大,因此“我们不允许你这样做是为了保护你”的借口已经站不住脚了:有很多方法可以“无意中”做错事情。即使如此,只要你有 git reflog 命令,所有这些都不重要。 - void.pointer
3
如@void.pointer所述,checkout不会删除文件。如果您希望实现该行为,请参考此答案。但我仍然希望有一天我们能够获得git reset --hard -- <path>。因为它确实存在合法的使用场景。 - Mariusz Pawelski
1
@MariuszPawelski:我刚刚了解到git restore - Patrick
显示剩余11条评论

24
你可以使用 git checkout HEAD <path> 来完成你想做的事情。
虽然如此,提供的错误信息对我来说毫无意义(因为git reset在子目录上也能正常工作),我看不出为什么git reset --hard不应该完全满足你的要求。

使用_checkout_阶段更改,这与重置——软件不同。 - worc

16

如何进行硬重置已经有答案,我来解释一下其中的原因。

那么,git reset是什么呢?取决于指定的参数,它可以执行两个不同的操作:

  • 如果您指定了路径,它将用提交(默认为HEAD)中文件替换索引中匹配的文件。此操作完全不影响工作树,通常用作git add的相反操作。

  • 如果您没有指定路径,则将当前分支头移动到指定的提交,并且可选地将索引和工作树重置为该提交的状态。此附加行为由模式参数控制:
    --soft:不要触摸索引和工作树。
    --mixed(默认值):重置索引但不重置工作树。
    --hard:重置索引和工作树。
    还有其他选项,请参见文档以获取完整列表和一些用例。

    当您没有指定提交时,默认为HEAD,因此git reset --soft什么也不会做,因为它是一个将head移动到HEAD(到其当前状态)的命令。另一方面,git reset --hard由于其副作用而是有意义的,它说将head移动到HEAD,并将索引和工作树重置为HEAD。

    我认为现在应该清楚了,为什么这个操作并不针对特定文件 - 它旨在首先移动分支头部,在此过程中重置工作树和索引是次要的功能。


2
很明显,reset 的首要目的是移动分支头,但由于它具有重置整个提交的工作树和索引以及重置特定文件的索引的附加功能,为什么它没有重置特定文件的工作树的功能呢?我相信这就是 OP 所问的。 - Danilo Souza Morães
1
也许是因为这个功能(重置特定文件的工作树)已经作为 git checkout 命令可用了?而将重置执行相同的操作会更加困扰用户。我的回答是,--hard 选项不适用于特定文件,因为它是分支重置模式,而不是索引重置模式。工作树重置被命名为 checkout,你可以在其他答案中看到。我认为这些都只是 Git 用户界面的糟糕设计。 - user
git checkout 的第一个选项进行比较:git reset -- 仅设置索引,而 git checkout -- 仅设置工作树? - seeker_of_bacon

7

请确保在源和实际分支之间加入斜杠:


git reset --hard origin/branch

或者

git reset --hard upstream/branch`

这完全没有回答问题。 - Hashim Aziz
刚通过谷歌发现了这个问题,原来是我的错误。所以这个回答还是很有帮助的。 - Markus Waas

6
这背后有一个非常重要的原因:checkout和reset的原则
在Git术语中,checkout的意思是“将其带入当前工作树”。通过使用git checkout,我们可以从任何区域(无论是来自存储库中的提交还是来自提交或暂存区的单个文件)填充工作树(甚至是默认值)。
相反,git reset没有这个角色。正如其名称所示,它将重置当前引用,但始终将存储库作为源,独立于“reach”(--soft,--mixed或--hard)。
总结一下:
  • checkout:从任何地方(索引/存储库提交)- > 工作树
  • reset:存储库提交 - > 覆盖HEAD(并可选覆盖索引和工作树)
因此,可能会有点令人困惑的是存在git reset COMMIT -- files,因为仅使用一些文件覆盖HEAD是没有意义的!
在没有官方解释的情况下,我只能推测git开发人员发现reset仍然是丢弃对暂存区所做更改的最佳命令名称,并且考虑到唯一的数据源是存储库,那么就“扩展功能”而不是创建新命令。
因此,git reset -- <files>已经有点特殊:它不会覆盖HEAD。在我看来,所有这些变化都将是例外。即使我们可以构想一个--hard版本,其他版本(例如--soft)也没有意义。

我喜欢这个答案。实际上,git reset -- <files> 感觉就像是因为这是一个有用的功能,但是没有人确定应该放在哪个命令中而添加进来的。幸运的是,现在我们有了更加理智的 git restore,它具有 git checkout -- <path>git checkout <commit> -- <path>git reset [<commit>] -- <path> 的功能,其默认值更加合理,并且还有更多以前无法完成的功能(与被接受的答案相反。现在你终于可以轻松地恢复工作树,而不会影响索引)。 - Mariusz Pawelski

3

本答案现已被我更广泛的回答所引用:所有关于在git中检查文件或目录的问题

为什么git无法按路径进行硬重置/软重置?

实际上是可以的,只需使用几个命令即可,而不是一个。以下是具体步骤:

总结

警告:在开始此过程之前,git status 应该是完全干净的!否则,您将面临永久丢失 git status 显示的任何未提交更改的风险,因为 git clean -fd 'f'orce 删除当前工作树(文件系统)中不在提交或分支commit_hash 中指定的路径下的所有文件和目录。因此,任何尚未提交的内容都会永久丢失,就像您使用了rm一样!

1.如何按路径进行--soft重置:

# 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. 如何通过路径执行 --hard 重置:

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

完整答案:

在git版本2.17.1中进行了测试(使用git --version检查您的版本)。

为什么git无法通过路径进行硬/软重置?

我不知道确切的原因,但我猜测是因为git要么做出了一项开发决策,你和我都不同意,要么是因为git还不完整,仍然需要实现这个功能。请参见下面“按路径进行--hard重置”部分下提供的其他见解。对于单个路径的真正的--hard重置无法像对整个分支进行--hard重置那样完成。

但是,我们可以通过几个命令手动实现所需的行为。请注意,由于下面解释的原因,git checkout commit_hash -- path/to/some/file_or_dir本身不是其中之一。

在继续之前,您应该了解 git reset 的作用,什么是 工作树索引,以及 --soft--hard 通常如何使用 git reset。如果您对这些主题有任何疑问,请先阅读下面的“背景知识”部分。

如何通过路径进行 --soft--hard git reset

又称:如何手动完成以下任一无效命令的等效操作:

# 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

由于上述命令是不允许的,并且由于此“checkout”命令并不执行与上述假设命令所执行的相同操作,因为此“checkout”命令也不会删除本地存在但不在“commit_hash”中的文件或文件夹:
git checkout commit_hash -- path/to/some/file_or_dir

...然后,你可以通过以下几个命令一起完成上面的假设命令所做的事情。

1. --soft路径重置

描述:使本地path/to/some/file_or_dircommit_hash中的file_or_dir相同,同时删除本地路径目录中不存在于commit_hash目录中的文件(如果path/to/some/file_or_dir是一个目录)。最后将所有更改“暂存”(已添加但未提交)。

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

上述结果正是我期望从路径上的 --soft 重置命令中得到的结果。
有关 git checkout-index -fa 和 git clean -fd 的更多信息,请参见我的另一个答案:使用 git,如何将工作树(本地文件系统状态)重置为索引(“暂存”文件)的状态?
请注意,在执行每个单独的命令后,应运行 git status 命令以查看每个命令的操作。以下是各个命令的说明:
# 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://dev59.com/qnA75IYBdhLWcg3wxsNn#66589020
# and https://dev59.com/IXVD5IYBdhLWcg3wNIzc#12184274
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 按路径重置

描述: 执行上述--soft重置步骤,然后提交更改:

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"

现在,为了保险起见并进行最终检查,您可以运行git reset commit_hash -- path/to/some/file_or_dir,然后是git status。您会发现git status完全没有任何更改,因为上面的路径--hard重置成功,所以对git reset commit_hash -- path/to/some/file_or_dir的调用没有做任何事情。太棒了;它奏效了!
这些结果与真正的--hard重置不完全相同,因为真正的--hard重置不会使用git commit添加新提交。相反,它只是强制您当前检出的分支指向那个其他的commit_hash。但是,当像这样“硬重置”仅几个文件或路径时,您不能仅将分支指针移动到该其他commit_hash,因此实现此命令的合理行为的其他方法就是像上面所做的那样添加这些“未添加”或“重置”的更改的新提交。
这个见解可能也是git不支持原生的按路径--hard重置选项的原因;也许是因为按路径进行--hard重置需要添加一个新提交,这与不添加新提交的正常--hard行为略有不同,而只是“重置”到(将分支指针移动到)给定提交。

然而,这并不能自然地解释为什么git不允许至少按路径进行--soft git重置,因为这对我来说似乎更标准。

背景知识

1. 基本的git术语

当您阅读man git reset页面时,您需要了解一些git术语:

  1. 工作树 = 本地文件系统; 这指的是当您在终端或GUI文件管理器(如nemonautilusthunar)中浏览文件系统时看到的文件和文件夹的状态。
  2. <tree-ish> = 提交哈希值或分支名称
  3. 索引 = 运行git status时看到绿色部分。这些都是已经git add(“暂存”)但尚未提交的更改。当您运行git add some_file时,通过将其更改移动到索引中,“暂存”了some_file。现在可以说some_file已被“添加”,“暂存”或“在索引中”(都是同一件事)。

2. man git reset页面

阅读这些解决方案时,有洞察力和帮助的是注意到man git reset声明(重点添加):

git reset <paths>git add <paths> 的相反操作。

换句话说,git reset commit_hash -- some_file_or_dir 可以“取消添加”或者添加与(从而撤销这些更改)some_file_or_dir 相反的更改,这些更改包含在提交或分支 commit_hash 中,同时也将 HEAD 设置为指向 commit_hash,或者对于指定的文件或目录,就像它指向 commit_hash 一样(通过添加必要的更改来使工作树中的 some_file_or_dir 看起来像 commit_hash 中的 some_file_or_dir)。

此外,在git术语中,"working tree"的意思是"您的本地文件系统"(就像计算机通常在文件夹管理器中或在终端中导航时看到文件和文件夹一样),而"index""index file"的意思是"当您git add或' stage '文件时,它们所在的位置。"当您运行git status时,所有以绿色显示的文件均为“已暂存”,或者位于“索引”或“索引文件”中(同一件事)。 (来源:Git中HEAD、工作树和索引之间有什么区别?)。
现在,考虑到这一点,以下是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.

并且:

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.

并且:

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. 你还应该熟悉 man git checkout-index 页面。

记住,“索引”包含所有已添加或“暂存”的文件(在运行 git status 时显示为绿色),而“工作树”是指您实际的本地文件系统(也包含在运行 git status 时显示为红色的更改)。

最基本的级别,它的作用如下:

来自 man git checkout-index

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

并且:

-f, --force
    forces overwrite of existing files

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

参考资料:

  1. [我的答案-直接适用,也是我能够写出上述整个答案所需的前置答案!]使用git,如何将工作树(本地文件系统状态)重置为索引(“staged”文件)的状态?
  2. 如何从当前Git工作树中删除本地(未跟踪)文件
  3. 如何舍弃Git中未暂存的更改?
  4. 在Git中,HEAD、工作树和索引之间有什么区别?

相关内容:

  1. [我的回答] 如何从另一个分支获取单个文件?

0

解释

git reset手册列出了三种调用方式:

  • 其中2种是基于文件的:这些不会影响工作树,而只对由<paths>指定的索引中的文件进行操作:

    • git reset [-q] [<tree-ish>] [--] <paths>..
    • git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
  • 1种是基于提交的:对所引用的<commit>中的所有文件进行操作,并且可能会影响工作树:

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

没有一种调用模式仅对指定的文件进行操作并影响工作树。

解决方法

如果您想同时:

  • 重置文件的索引/缓存版本
  • 检出文件(即使工作树匹配索引和提交版本)

您可以在git配置文件中使用此别名:

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

然后你可以执行以下操作之一:

$ git reco <paths>

$ git reco <branch/commit> <paths>

$ git reco -- <paths>

(reco的助记符:重置并检查out)


-3

git reset --soft HEAD~1 文件名 撤销提交但更改保留在本地。文件名 可以是 -- 表示所有已提交的文件。


7
致命错误: 无法使用路径执行软重置。 - alt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接