如何git reset --hard子目录?


196

更新²:使用Git 2.23(2019年8月)时,有一个新命令git restore可以执行此操作,请参见接受的答案

更新:从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

3
那一个git stash && git stash drop呢?
2013年

1
那又如何git checkout -- /path/to/subdir/呢?
iberbeu 2013年

3
@CharlesB:git stash不接受路径参数...
krlmlr

@iberbeu:不。还将添加稀疏检出排除的文件。
krlmlr

1
@CharlesBailey:那么为什么有一个单选按钮显示“正在寻找来自可靠和/或官方来源的答案”。在赏金对话框中?我自己没有输入!也可以尝试使用Google搜索“ git reset子目录”(不带引号),并查看前三个位置的内容。当然,发送到kernel.org邮件列表的消息将更难找到。-而且,对我来说,目前还不清楚这种行为是错误还是功能。
krlmlr

Answers:


161

使用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年)

注(如评论丹Fabulich)认为:

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

正如Ajedi32 回答那样,两种签出表格都不会删除在目标版本中删除的文件
如果工作树中有HEAD中不存在的多余文件,git checkout HEAD -- <path>则不会删除它们。

注意:对于git checkout --overlay HEAD -- <path>(Git 2.22,Q1 2019)<tree-ish>将删除出现在索引和工作树中但未出现在其中的文件,以使其<tree-ish>完全匹配。

但是,该签出可以尊重a git update-index --skip-worktree(对于您要忽略的目录),如“ 为什么在我的git sparse签出中为什么排除的文件不断出现? ”中所述。


1
请澄清。之后git checkout HEAD -- .,重新出现稀疏检出排除的文件。什么是git update-index --skip-worktree应该做的?
krlmlr

@krlmlr跳worktree或承担,不变是试图让在指数中的git的条目“看不见”的双向:fallengamer.livejournal.com/93321.htmlstackoverflow.com/q/13630849/6309计算器。 com / a / 6139470/6309
VonC

@krlmlr这些链接只是指针,供您尝试,一旦将它们标记为“跳过工作树”,结帐是否仍会恢复这些条目。
VonC

抱歉,但是对于手头的任务来说太复杂了。我想要一个包容性的重置,而不是一个专有的重置。在Git中真的没有很好的方法吗?
krlmlr

@krlmlr否:最好这样做git checkout HEAD -- <path>,然后删除已还原的目录(但仍在稀疏检出中声明的目录)。
VonC

125

根据Git开发人员Duy Nguyen的友好实现,该功能和兼容性开关表示,自Git 1.8.3起,以下各项按预期工作:

git checkout -- a

a您要硬重置的目录在哪里)。可以通过以下方式访问原始行为

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

5
感谢您为跟进Git开发团队所做的努力,导致了Git的这一变化。
丹·克鲁兹

13
并请注意,在这种情况下,“ a”表示要还原的目录,因此,如果您要还原的目录位于该目录中,则该命令应位于git checkout -- .where .表示当前目录。
TheWestIsThe ...

5
我这边的一则评论是,您必须首先使用以下命令取消登台该文件夹 git reset -- a(其中a是您要重置的目录)
Boyan 2016年

如果您想要git reset --hard整个回购协议,那不是真的吗?
krlmlr

并且,如果您将任何新文件添加到该目录,请执行rm -rf a之前的操作。
Tobias Feil

30

尝试改变

git checkout -- a

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

从1.7.0版开始,Git就采用了skip-worktree标志ls-files

运行您的测试脚本(在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尊重“跳过工作树”吗?
krlmlr

快速浏览checkout.ctree.c没有发现任何跳跃,worktree标志使用。
丹·克鲁兹

即使我必须为此命令设置一个bash别名,这也足够简单以在实践中有用。Duy Nguyen已将我的邮件回复到Git邮件列表,让我们看看是否会弹出一个更加用户友好的替代品。
krlmlr 2013年

18

对于简单地放弃更改的情况,其他答案建议的git checkout -- path/git checkout HEAD -- path/命令效果很好。但是,当您希望将目录重置为HEAD以外的其他版本时,该解决方案存在一个重大问题:它不会删除在目标版本中删除的文件。

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

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

通过找到目标提交和索引之间的差异,然后将该差异反向应用于工作目录和索引,可以起作用。基本上,这意味着它使索引的内容与您指定的修订的内容匹配。git diff带有path参数的事实允许您将这种影响限制为特定的文件或目录。

由于此命令相当长,并且我计划经常使用它,因此我为其设置了一个别名,命名为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 2015年

该选项与git checkout --overlay HEAD -- <path>@VonC在其答案中提到的命令相比如何?
Ehtesh Choudhury

1
@EhteshChoudhury注意git checkout --overlay HEAD -- <path>尚未发布(Git 2.22将在2019年第二季度发布)
VonC

5

我将在这里提供一个糟糕的选择,因为除了add commit和之外,我不知道如何使用git做任何事情push,这就是我“还原”子目录的方式:

我在本地PC上启动了一个新的存储库,将整个内容恢复为我想从中复制代码的提交,然后将这些文件复制到我的工作目录中,add commit push等等。不要恨球员,不要恨托瓦尔兹先生比我们所有人都聪明。


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
乔纳森·雷恩

1
@krlmlr另外,值得注意的是git add不会添加已删除的文件。因此,如果您仅恢复已删除的文件,git add .将添加所有已修改的文件,而不是已删除的文件。
乔纳森·雷恩

3

如果子目录的大小不是特别大,并且您希望远离CLI,可以使用以下快速解决方案手动重置子目录:

  1. 切换到master分支并复制要重置的子目录。
  2. 现在切换回功能分支,并将子目录替换为您在步骤1中刚刚创建的副本。
  3. 提交更改。

干杯。您只需手动将功能分支中的子目录重置为与主分支相同!


我没有看到任何答案,但是有时候这是成功的最简单保证。
Sue Spence

1

Ajedi32答案是我想要的,但是对于某些提交,我遇到了此错误:

error: cannot apply binary patch to 'path/to/directory' without full index line

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

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 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.