您想要做的将涉及两个阶段:追溯添加具有合适.gitignore 的新根,并清理您的历史记录以删除不应添加的文件。 git filter-branch 命令可以两者兼得。
设置
考虑你的历史代表。
$ git lola --name-status
* f1af2bf (HEAD, bar-feature) Add bar
| A .gitignore
| A bar.c
| D main.o
| D module.o
| * 71f711a (master) Add foo
|/
| A foo.c
| A foo.o
* 7f1a361 Commit 2
| A module.c
| A module.o
* eb21590 Commit 1
A main.c
A main.o
为了清楚起见,*.c 文件代表 C 源文件,*.o 是应该被忽略的编译目标文件。
在 bar-feature 分支上,您添加了合适的 .gitignore 并删除了不应跟踪的目标文件,但您希望该策略在导入的任何地方都反映出来。
请注意,git lola 是 non-standard 但有用的别名。
git config --global alias.lola \
'log --graph --decorate --pretty=oneline --abbrev-commit --all'
新根提交
如下创建新的根提交。
$ git checkout --orphan new-root
Switched to a new branch 'new-root'
git checkout 文档指出了新的孤立分支可能处于未预料到的状态。
如果你想开始一个断开的历史记录,记录一组与 start_point 完全不同的路径,那么你应该在创建孤立分支后立即清除索引和工作树通过从工作树的顶层运行git rm -rf .。之后,您将准备好准备新文件、重新填充工作树、从其他地方复制它们、提取 tarball 等等。
继续我们的例子:
$ git rm -rf .
rm 'foo.c'
rm 'foo.o'
rm 'main.c'
rm 'main.o'
rm 'module.c'
rm 'module.o'
$ echo '*.o' >.gitignore
$ git add .gitignore
$ git commit -m 'Create .gitignore'
[new-root (root-commit) 00c7780] Create .gitignore
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
现在历史看起来像
$ git lola
* 00c7780 (HEAD, new-root) Create .gitignore
* f1af2bf(bar-feature) Add bar
| * 71f711a (master) Add foo
|/
* 7f1a361 Commit 2
* eb21590 Commit 1
这有点误导,因为它使 new-root 看起来像是 bar-feature 的后代,但实际上它没有父级。
$ git rev-parse HEAD^
HEAD^
fatal: ambiguous argument 'HEAD^': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
记下孤儿的 SHA,因为稍后您将需要它。在这个例子中,它是
$ git rev-parse HEAD
00c778087723ae890e803043493214fb09706ec7
改写历史
我们希望git filter-branch 进行三项广泛的更改。
- 在新的根提交中拼接。
- 删除所有临时文件。
- 使用新根目录下的
.gitignore,除非已经存在。
在命令行上,被咒语为
git filter-branch \
--parent-filter '
test $GIT_COMMIT = eb215900cd15ca2cf9ded74f1a0d9d25f65eb2bf && \
echo "-p 00c778087723ae890e803043493214fb09706ec7" \
|| cat' \
--index-filter '
git rm --cached --ignore-unmatch "*.o"; \
git ls-files --cached --error-unmatch .gitignore >/dev/null 2>&1 ||
git update-index --add --cacheinfo \
100644,$(git rev-parse new-root:.gitignore),.gitignore' \
--tag-name-filter cat \
-- --all
解释:
-
--parent-filter 选项挂钩在您的新根提交中。
-
eb215... 是旧根提交的完整 SHA,cf. git rev-parse eb215
-
--index-filter 选项有两个部分:
- 如上运行
git rm 会从整个树中删除任何匹配 *.o 的内容,因为 glob 模式是由 git 而不是 shell 引用和解释的。
- 使用
git ls-files 检查现有的.gitignore,如果不存在,请指向新根目录中的那个。
- 如果您有任何标签,它们将通过身份操作
cat 进行映射。
- 唯一的
-- 终止选项,--all 是所有引用的简写。
您看到的输出将类似于
Rewrite eb215900cd15ca2cf9ded74f1a0d9d25f65eb2bf (1/5)rm 'main.o'
Rewrite 7f1a361ee918f7062f686e26b57788dd65bb5fe1 (2/5)rm 'main.o'
rm 'module.o'
Rewrite 71f711a15fa1fc60542cc71c9ff4c66b4303e603 (3/5)rm 'foo.o'
rm 'main.o'
rm 'module.o'
Rewrite f1af2bf89ed2236fdaf2a1a75a34c911efbd5982 (5/5)
Ref 'refs/heads/bar-feature' was rewritten
Ref 'refs/heads/master' was rewritten
WARNING: Ref 'refs/heads/new-root' is unchanged
您的原件仍然安全。例如,master 分支现在位于refs/original/refs/heads/master 下。查看新重写的分支中的更改。当您准备好删除备份时,运行
git update-ref -d refs/original/refs/heads/master
您可以编写一个命令以在一个命令中覆盖所有备份引用,但我建议仔细查看每个命令。
结论
最后,新的历史是
$ git lola --name-status
* ab8cb1c (bar-feature) Add bar
| M .gitignore
| A bar.c
| * 43e5658 (master) Add foo
|/
| A foo.c
* 6469dab Commit 2
| A module.c
* 47f9f73 Commit 1
| A main.c
* 00c7780 (HEAD, new-root) Create .gitignore
A .gitignore
观察所有目标文件都消失了。 bar-feature 中对.gitignore 的修改是因为我使用了不同的内容来确保它会被保留。为了完整性:
$ git diff new-root:.gitignore bar-feature:.gitignore
diff --git a/new-root:.gitignore b/bar-feature:.gitignore
index 5761abc..c395c62 100644
--- a/new-root:.gitignore
+++ b/bar-feature:.gitignore
@@ -1 +1,2 @@
*.o
+*.obj
新根引用不再有用,因此将其丢弃
$ git checkout master
$ git branch -d new-root