问个 Git 基操:怎么样复制一个文件,能保持历史记录?

123 天前
 xiangyuecn
git cp:没这号命令😂
git mv:这是改名

比如已有 a.txt ,我现在要个 b.txt 。 如何复制出 b.txt 这个文件,并且复制前的历史和 a.txt 保持一致?

还是说 和空文件夹 一样,git 不在乎你死活?

操作尽量简单,要是涉及的非基础知识点太多,还是算了,这历史不要也罢😂

8035 次点击
所在节点    git
99 条回复
Trim21
123 天前
做不到,这个要看对应的 git 客户端能不能识别出他是文件改名。
geelaw
123 天前
https://devblogs.microsoft.com/oldnewthing/20190919-00/?p=102904

基本原理是:

- blame 在 merge commit 处会追踪文件来自各个 parents 的部份。
- 让文件 a 和 b 来自 merge 的两个 parents 。
- 让文件 a 来自更早的 a 。
- 仍文件 b 来自更早的 a 。

我的做法和 Raymond Chen 不一样,假设当前在 branch "current":

1. 重命名当前文件

git checkout -b copy
git mv a _
git commit -m 'Prepare to copy file.'

2. 把当前文件在第一个分支上恢复为原来的名字

git checkout -b copy1
git mv _ a
git commit -m 'Restore name of file.'

3. 把当前文件在另一个分支上重命名为副本的名字

git checkout -b copy2 copy
git mv _ b
git commit -m 'Copy file.'

4. 合并两个修改

git checkout copy
git merge --no-ff -m 'Finish copying file.' copy1 copy2

5. 回归原分支

git checkout current
git merge --no-ff -m 'Copy `a` to `b`.' copy

6. 删除中间分支

git branch -d copy copy1 copy2
sunbeams001
123 天前
把 a.txt 相关的所有记录导出成文本,贴在创建 b.txt 的 commit message 里
geelaw
123 天前
@magggia #9
@mercury233 #10
@mangoDB #19
见 #22

@xubingok #17 这当然不是造假,考虑各种变种需求:文件拆分成两个,希望各自保留历史;两个文件合并,希望保留各自的历史。

@Trim21 #21 模糊识别重命名确实有这个问题,但是 Git 的原理保证:如果一个 commit 里面只发生不同内容文件的重命名(没有内容修改、没有多个同内容文件重命名),那么历史可以正确用 blame 追溯。(不满足此条件则会有模糊匹配的问题,所以我给文件改名的时候都是一个 commit 只做改名一件事的。)
gesse
123 天前
本身 git 的哲学就是“保持历史真实性”,你这个需求就是“篡改历史”

相违背了。
dsggnbsp
123 天前
WhiskerSpark
123 天前
想看历史记录就去看 a.txt 就好了
suhu
123 天前
你需要这个东西: https://github.com/newren/git-filter-repo
筛选出某一个目录/文件的提交记录 和 filter-branch 不一样,更好用
xiangyuecn
123 天前
@mangoDB 我不需要“理念”
@gesse 我不需要“哲学”

我只想要我复制出的文件能简单的做到历史记录追踪😐
94
123 天前
@xiangyuecn #15 ,mv 是重命名啊,怎么到你这边就变成删文件+新建文件了……
基于“文件”进行操作,给这个 mv 操作添加到对应文件的 commit 历史中。

git-mv - Move or rename a file, a directory, or a symlink
[Git - git-mv Documentation]( https://git-scm.com/docs/git-mv)

如果按照 OP 你的意思,其实复制出来一个 b 文件。但对于你来说是知道这个文件是基于 a 文件复制出来的,但是对于 git 来说 b 文件单纯只是没有被追踪过的一个新文件。
所以你的实际操作是新增一个 b 文件,同时去按照 a 文件的编辑历史造 b 文件的整个历史,而不是共享整个历史。
xiangyuecn
123 天前
@WhiskerSpark “想看历史记录就去看 a.txt 就好了”,目前就是这样看历史记录的,总感觉差点意思
lzgshsj
123 天前
@xiangyuecn #29 所以你也不需要"git"
loon98
123 天前
这种需求应该做成子仓库, txt 文件保留两份 blame 指向同一个记录, 必须由分支实现. 主仓库分别引用子仓库的两个分支.
你要求的这个用法就不符合 git 思想和哲学, 要么就用 24l 的方案, 要么就用最符合 git 哲学的办法达成近似效果.
codehz
123 天前
@dfkjgklfdjg git mv 等价于 mv+git rm+git add ,git 识别文件移动完全靠猜测,并没有特别记录这个信息
feedcode
123 天前
11 楼老哥都给你答案了,再给你个详细的
git filter-branch --tree-filter 'cp -f dir/a1 dir/a3' --tag-name-filter cat --prune-empty -- --all
94
123 天前
@dfkjgklfdjg #30 ,不管是使用分支冲突的方式,还是通过 filter-branch/repo 去创造 b 文件的历史,都会去修改仓库的提交历史。在多人协作的项目里面操作 git 历史一个非常危险的行为。
xiangyuecn
123 天前
@geelaw #22 感谢,前几天 deepseek 也给出了类似的方案,我就是嫌操作起来太复杂了,要是有 git cp 一条命令解决,这个世界就安静了,这记录如果没有简单操作实现,其实不要历史也没什么关系,提交的时候 commit 里面手动备注是从 a.txt 复制来的,到了这个位置就手动切换到 a.txt 查看历史
94
123 天前
@codehz #34 ,识别到 rename 是猜。但是如果猜到了,那么提交历史里面是会有标注是 rename ,而不是 add 。

```
diff --git a/test.xt b/test2.xt
similarity index 100%
rename from test.xt
rename to testTT.xt
```
geelaw
123 天前
@xiangyuecn #37 git cp 很难成立,原因:git mv 和 git rm 都不产生 commit ,而是修改 index 等,因此基于一致性,git cp 也应该不产生 commit 而只是修改 index ;但是 git 数据库里面每个 commit 存的是状态,而不是“怎么来的”,因此要让 git blame 认识到一个文件是另一个的副本,要么 git blame 自动检测(见下一段),要么这个信息必须存在于 commits 的历史中,用多分支的做法就是把这个信息存放在历史中,这必然要创建多个 commits ,因此 git cp 不会这样做。

目前的实现里,如果一个 commit 的惟一变化是有一个新文件 B ,并且新文件 B 和某个已经存在的文件 A 内容一样,那么 Git 会认为新文件是“手工重新写出来的”,不会认为 B 来自于 A 。我觉得这样设计的原因是很多配置文件可能确实会是内容上一样的,但更可能是基于不同的原因分别写出来的,所以不宜合并历史。

git mv 也并不保证 git blame 会认为移动前后的文件有关联性,这种信息依然是 Git 通过对比两个 commits 算出来的。
94
123 天前
@dfkjgklfdjg #38 ,随便建了一个 test 文件,rename 的时候顺手加了个 TT 。回复的时候感觉好像改成 test2 更合适。就手动改成了 2 ,但直接发出来了,后面的 to 还没改掉。

😂 将就看吧。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1133693

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX