如何使用git reset --hard重置子目录。

297
UPDATE²: 2019年8月,随着Git 2.23的发布,新增了一个名为git restore的命令,可以实现此功能,请参见被认可的答案UPDATE: 自Git 1.8.3起,此功能将更加直观易用,请参见我的回答
假设以下使用场景:我想摆脱Git工作树中特定子目录中的所有更改,同时保留所有其他子目录不变。

这个操作的正确 Git 命令是什么?

下面的脚本描述了问题。请在 How to make files 注释下方插入正确的命令 - 当前的命令将还原文件 a/c/ac,而该文件应该被稀疏检出排除。请注意,我不想明确还原 a/aa/b,我只知道 a 并希望恢复其下的所有内容。编辑:我也不知道 b,或者哪些其他目录与 a 在同一级别。

#!/bin/sh

rm -rf repo; git init repo; cd repo
for f in a b; do
  for g in a b c; do
    mkdir -p $f/$g
    touch $f/$g/$f$g
    git add $f/$g
    git commit -m "added $f/$g"
  done
done
git config core.sparsecheckout true
echo a/a > .git/info/sparse-checkout
echo a/b >> .git/info/sparse-checkout
echo b/a >> .git/info/sparse-checkout
git read-tree -m -u HEAD
echo "After read-tree:"
find * -type f

rm a/a/aa
rm a/b/ab
echo >> b/a/ba
echo "After modifying:"
find * -type f
git status

# How to make files a/* reappear without changing b and without recreating a/c?
git checkout -- a

echo "After checkout:"
git status
find * -type f

4
那用git stash && git stash drop怎么样? - CharlesB
1
git checkout -- /path/to/subdir/ 是什么意思? - iberbeu
3
@CharlesB说:git stash命令不接受路径参数... - krlmlr
看起来稀疏检出功能并不是真正想要使用的东西...只需不使用它,并为每个项目设置单独的git存储库。 - user1050755
1
@CharlesBailey:那么为什么赏金对话框中有一个单选按钮写着“寻找来自可信和/或官方来源的答案。”我没有自己输入那个!另外,尝试谷歌搜索“git reset subdirectory”(不带引号),看看前三个位置上是什么。肯定会更难找到发送给kernel.org邮件列表的消息。--此外,对我来说,这种行为是错误还是特性还不清楚。 - krlmlr
显示剩余5条评论
10个回答

324

使用 Git 2.23(2019年8月),您可以使用新命令git restore(此处也介绍

git restore --source=HEAD --staged --worktree -- aDirectory
# or, shorter
git restore -s@ -SW -- aDirectory

这将用HEAD的内容替换索引和工作树,就像reset --hard一样,但只针对特定路径。


原始答案(2013年)

请注意(如评论Dan Fabulich所述):

  • git checkout -- <path>不执行硬重置:它将工作树内容替换为暂存内容。
  • git checkout HEAD -- <path>对于路径执行硬重置,用HEAD提交的版本替换索引和工作树中的版本。

正如回答Ajedi32所说,两种checkout形式不会删除目标修订中已删除的文件
如果您在工作树中有额外的文件,而这些文件在HEAD中不存在,则git checkout HEAD -- <path>不会将它们删除。

注意:使用 git checkout --overlay HEAD -- <path> (Git 2.22, Q1 2019) 命令,可以删除出现在索引和工作树中但不在 <tree-ish> 中的文件,以使它们与 <tree-ish> 完全匹配。
但是,该命令可以遵循 git update-index --skip-worktree(对于您想要忽略的目录),如 "Why do excluded files keep reappearing in my git sparse checkout?" 中所述。

1
请澄清一下。在执行 git checkout HEAD -- . 命令后,被稀疏检出排除的文件会重新出现。git update-index --skip-worktree 命令的作用是什么? - krlmlr
1
抱歉,但这对于手头的任务来说太复杂了。我想要一个包容性的重置,而不是排他性的。在Git中真的没有一种好的方法吗? - krlmlr
2
git checkout HEAD -- <path> 看起来不像硬重置(hard reset)那样行为一致。硬重置会删除路径中已不存在于给定版本的文件。在我的经验中(git 1.8.3.3),git checkout HEAD -- <path> 并不会这样做。 - Ajedi32
@VonC 很希望看到一个解决方案,它也可以从版本中删除文件。现在我有一个别名来模拟 reset --hard 带路径的行为:reset-path = "!f() { : git checkout ; rm -rf \"$2\" && git checkout \"$1\" -- \"$2\" ; }; f"。然而,这也会在不应该的情况下删除未跟踪的文件,但目前它提供了很大的便利性,这是一个合理的权衡。 - void.pointer
1
这不会删除新文件。 - mvorisek
显示剩余6条评论

145

根据 Git 开发者 Duy Nguyen 的说法,他友善地实现了该功能和兼容开关,在 Git 1.8.3 版本中如预期地工作

git checkout -- a

(其中a是您想要进行硬重置的目录)。可以通过以下方式访问原始行为

git checkout --ignore-skip-worktree-bits -- a

5
感谢你跟进 Git 开发团队,最终使得 Git 发生了这个变化。 - Go Dan
16
请注意,在这种情况下,“a”指的是您想要还原的目录,因此如果您位于要还原的目录中,则命令应为 git checkout -- .,其中“.”表示当前目录。 - TheWestIsThe...
5
我想提出一点建议,就是您必须先使用以下命令取消暂存文件夹: git reset -- a (其中a是您想要重置的目录)。 - Boyan
如果您想要git reset --hard整个存储库,这也是正确的吗? - krlmlr
3
如果您向该目录添加了任何新文件,请在执行操作前使用 rm -rf a 命令。 - Tobias Feil

35

试着进行更改。

git checkout -- a

to

git checkout -- `git ls-files -m -- a`

自 Git 1.7.0 版本起,Git 的 ls-files 命令 支持跳过工作树标记(skip-worktree flag)
运行测试脚本(稍做修改将 git commit... 更改为 git commit -q 并将 git status 更改为 git status --short),输出如下:
Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/c/ac
a/b/ab
b/a/ba

使用建议的checkout更改运行测试脚本会输出:

Initialized empty Git repository in /home/user/repo/.git/
After read-tree:
a/a/aa
a/b/ab
b/a/ba
After modifying:
b/a/ba
 D a/a/aa
 D a/b/ab
 M b/a/ba
After checkout:
 M b/a/ba
a/a/aa
a/b/ab
b/a/ba

听起来不错。但是 git checkout 本应该首先尊重 "skip-worktree" 标志,不是吗? - krlmlr
快速查看checkout.ctree.c并没有发现使用skip-worktree标志。 - Go Dan
这个很简单,实际应用中非常有用,即使我必须为此命令设置一个bash别名。Duy Nguyen已经回复了我的Git邮件列表消息,让我们看看是否会很快出现更加用户友好的替代品。 - krlmlr

19

如果只是想简单放弃更改,其他答案建议使用git checkout -- path/git checkout HEAD -- path/命令。但是,当您希望将目录重置为HEAD以外的修订版本时,该解决方案存在一个重要问题:它不会删除目标修订版中已删除的文件。

因此,我开始使用以下命令:

git diff --cached commit -- subdir | git apply -R --index

这个命令的作用是找出目标提交和索引之间的差异,然后将其反向应用于工作目录和索引。基本上,这意味着它使索引的内容与您指定的修订版本的内容相匹配。git diff 命令接受路径参数,因此您可以将此效果限制为特定文件或目录。

由于这个命令相当长,而且我计划经常使用它,因此我为它设置了一个别名,命名为reset-checkout:

git config --global alias.reset-checkout '!f() { git diff --cached "$@" | git apply -R --index; }; f'
您可以像这样使用它:
git reset-checkout 451a9a4 -- path/to/directory

或者只需:

git reset-checkout 451a9a4

我昨天看到了你的评论,今天进行了实验。你的别名很有帮助。+1 - VonC
这个选项与@VonC在他的回答中提到的git checkout --overlay HEAD -- <path>命令相比如何? - Ehtesh Choudhury
1
请注意,git checkout --overlay HEAD -- <path>尚未发布(Git 2.22将于2019年第二季度发布)。 - VonC

7

所有GIT版本

对于我而言,我将使用checkout:

首先,我会删除我想要重置的文件夹。

然后,我会简单地使用checkout:

git checkout <branch> <path>

该文件夹将被正确重置到我所需的分支。


6

我要提供一个可怕的选项,因为我不知道如何在Git中做任何事情,除了add commitpush。以下是我“还原”子目录的方法:

我在本地PC上启动了一个新存储库,将整个存储库还原到我想要复制代码的提交,然后将这些文件复制到我的工作目录中,add commit push,完成。别恨玩家;恨Linus Torvalds先生比我们都聪明。


5
如果子目录的大小不是特别大,并且您希望避免使用CLI,那么这里有一个快速的解决方案可以手动重置子目录:
  1. 切换到主分支并复制要重置的子目录。
  2. 现在切换回您的功能分支并用步骤1中创建的副本替换子目录。
  3. 提交更改。
干杯。 您刚刚手动重置了功能分支中的子目录,使其与主分支相同!

我没有看到这个答案的任何投票,但有时这是成功的最简单保证路线。 - Sue Spence

4

重置通常会改变所有内容,但您可以使用git stash选择要保留的内容。如您所述,stash不能直接接受路径,但仍可使用--keep-index标志来保留特定路径。在您的示例中,您将存储 b 目录,然后重置其他所有内容。

# How to make files a/* reappear without changing b and without recreating a/c?
git add b               #add the directory you want to keep
git stash --keep-index  #stash anything that isn't added
git reset               #unstage the b directory
git stash drop          #clean up the stash (optional)

这会使得你的脚本最后输出以下内容:
After checkout:
# On branch master
# Changes not staged for commit:
#
#   modified:   b/a/ba
#
no changes added to commit (use "git add" and/or "git commit -a")
a/a/aa
a/b/ab
b/a/ba

我相信这是预期的结果(b保持修改,a/*文件已恢复,a/c未重建)。
采用这种方法的额外好处是非常灵活的,您可以将其细化到特定文件,但不会影响目录中的其他文件。

很好,但我需要 git add 除了 a 之外的所有内容,对吗? 在实践中听起来很困难。 - krlmlr
1
@krlmlr 不完全是这样。你可以使用 git add . 然后 git reset a 来添加除了 a 之外的所有内容。 - Jonathan Wren
1
@krlmlr 另外值得注意的是,git add 不会添加已删除的文件。因此,如果您只想恢复已删除的文件,则 git add . 将添加所有修改过的文件,但不包括已删除的文件。 - Jonathan Wren

1

Ajedi32答案就是我在寻找的,但是在一些提交中我遇到了这个错误:

错误:无法对“路径/到/目录”的二进制补丁进行应用,因为没有完整的索引行

可能是因为该目录中的一些文件是二进制文件。将'--binary'选项添加到git diff命令中即可解决问题:

git diff --binary --cached commit -- path/to/directory | git apply -R --index

0

使用:

subdir=thesubdir
for fn in $(find $subdir); do
  git ls-files --error-unmatch $fn 2>/dev/null >/dev/null;
  if [ "$?" = "1" ]; then
    continue;
  fi
  echo "Restoring $fn";
  git show HEAD:$fn > $fn;
done

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