如何git reset –hard一个子目录?

How to git reset --hard a subdirectory?

UPDATE: This will work more intuitively as of Git 1.8.3, see my own answer.

想象一下以下用例:我想摆脱所有的变化在特定的子目录树中的所有其他工作,离开完整的子目录。

  • 我可以git checkout .的Git,但结帐。用稀疏目录排除增加。

  • 有一个git reset --hard,但它不会让我做任何子目录:

    1
    2
    > git reset --hard .
    fatal: Cannot do hard reset with paths.

    再使用:为什么不能用硬/软resets的路径?

  • 我可以使用的当前状态的反git diff subdir | patch -p1 -R补丁,但这是一个相当奇怪的方式这样做。

什么是git命令的正确操作是这样的吗?

下面示出了剧本的问题。插入在适当的命令,下面的How to make files评论——电流命令将恢复的文件,这是一a/c/ac应该排除由稀疏的校验。请注意,我不想和a/b还原a/a已经明确,"我只想知道"和"a恢复材料。编辑:我也不知道这b",或是其他的在线目录的a相同的水平。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/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


注意(如Dan Fabulich所评论的)以下内容:

  • git checkout -- 不做硬重置:它将工作树内容替换为阶段性内容。
  • git checkout HEAD -- 对路径进行硬重置,用HEAD提交的版本替换索引和工作树。

正如Ajedi32所回答的,两个签出表单都不会删除目标修订版中删除的文件。如果在工作树中有额外的文件,而这些文件在head中不存在,那么git checkout HEAD -- 将不会删除它们。

注:对于git checkout --overlay HEAD -- (git 2.22,q1 2019),删除索引树和工作树中出现但不在中的文件,使其与完全匹配。

但是这个签出可以尊重git update-index --skip-worktree(对于那些您想忽略的目录),如"为什么被排除的文件总是在我的git稀疏签出中重新出现?".


根据Git开发人员Duy Nguyen的说法,他很好地实现了这个特性和一个兼容性开关,从Git 1.8.3开始,以下工作是可以预期的:

1
git checkout -- a

(其中a是要硬重置的目录)。原始行为可以通过

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


尝试改变

1
git checkout -- a

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

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

运行您的测试脚本(通过一些小的调整来更改git commit……到git commit -qgit statusgit status --short输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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变更输出运行测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 -- path/git checkout HEAD -- path/命令效果很好。但是,当您希望将目录重置为head以外的修订时,该解决方案有一个重大问题:它不会删除目标修订中删除的文件。

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

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

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

由于这个命令相当长,我计划经常使用它,所以我为它设置了一个别名,我将其命名为reset-checkout

1
git config --global alias.reset-checkout '!f() { git diff --cached"$@" | git apply -R --index; }; f'

您可以这样使用它:

1
git reset-checkout 451a9a4 -- path/to/directory

或者只是:

1
git reset-checkout 451a9a4


我将在这里提供一个糟糕的选择,因为除了addcommitpush,我不知道如何处理git,下面是我如何"还原"子目录:

我在我的本地PC上启动了一个新的存储库,将整个过程恢复到我想从中复制代码的承诺,然后将这些文件复制到我的工作目录addcommitpushet voila。不要恨球员,恨托瓦尔兹先生比我们都聪明。


重置通常会改变一切,但您可以使用git stash选择要保留的内容。正如您提到的,stash不直接接受一条路径,但它仍然可以用于保留带有--keep-index标志的特定路径。在您的示例中,您将存储B目录,然后重置其他所有内容。

1
2
3
4
5
# 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)

这将使您到达脚本的最后一部分将输出此内容的位置:

1
2
3
4
5
6
7
8
9
10
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不被重新创建)。

这种方法还有一个额外的好处,那就是非常灵活;您可以根据需要在目录中添加特定的文件,而不是添加其他文件,从而获得细粒度。


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

  • 切换到主分支并复制要重置的子目录。
  • 现在切换回您的功能分支,并用您在步骤1中创建的副本替换子目录。
  • 提交更改。
  • 干杯。您只需手动将功能分支中的子目录重置为与主分支相同的目录!!


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

    埃多克斯1〔22〕

    可能是因为目录中的某些文件是二进制文件。将"--binary"选项添加到git diff命令可以修复它:

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