git cp:没这号命令😂
git mv:这是改名

比如已有 a.txt ,我现在要个 b.txt 。 如何复制出 b.txt 这个文件,并且复制前的历史和 a.txt 保持一致?
还是说 和空文件夹 一样,git 不在乎你死活?
操作尽量简单,要是涉及的非基础知识点太多,还是算了,这历史不要也罢😂

没明白你的需求

你这需求就很妖

什么应用场景这是,我只试过批量修改提交历史的用户名邮箱,因为搞错了

场景一:a.txt 里面有 500 行代码,现在有个新功能 b.txt ,和 a.txt 功能完全一致,就只用改某一行的几个字

场景二:a.txt 里面有 2000 行代码,现在要把里面后半部分 1000 行拆分到 b.txt 里,拆分出来的代码新文件要有原来的历史记录

我这儿有过需求是 mono repo 中两个子项目共用一个配置
解决方式是单独提出来到最外层,然后如果需要放到子项目内就在文件夹内丢硬链接

你确实这是用 git 来做版本管理的么,你确定不是想建立一个分支啥的。非要 copy 的话,你可以考虑用 SVN 。

git-scm.com/docs/git-blame
自行查看

啊这 不知道 OP 听没听说过继承重写

没戏,跟 git 自身的原理相悖了,不支持

假如 a 文件涉及 100 个 commit ,你这新建 b 文件的同时也得新建 100 个 commit ,不能去修改历史记录,我猜 git 自身没有这个功能

filter-branch 每个 commit 复制一遍

git 本事就是保持和维护 commit 历史记录的,你这直接伪造可还行

有没有基础可以用的操作,太复杂的不是说不可以学,要用的时候肯得去翻文档,时间成本太高了,前提是知道翻什么地方,不常用的命令普通人也不大会去记 现用现翻文档

新建分支就行了啊,不同分支不同记录,只是你只关心 a 和 b 文件,其他文件不用关心

“没戏,跟 git 自身的原理相悖了,不支持”
但又存在 git mv 这个命令,mv 本身按字面意思就是删文件+新建文件,要实现复制只是不需要删文件+新建文件

不需要提分支功能,跟这个复制毫无干系

我算是看明白了.
假设 a 文件是 1 年前创建的,有 100 条修改记录.

OP 想的是,根据 a 文件复制出一个 b 文件来.同时在 git 中可以看到 b 文件也是 1 年前创建的,也有这 100 条修改记录.

这不是造假么...

简单, a 文件保留, 然后 b 文件开头加一行注释, 历史见 a 文件(

你想让「文件 b 」和「文件 a 」具备一样的历史记录,不仅仅是内容的拷贝。我觉得git应该无法满足你这个奇怪的需求,这相当于是“伪造”了「文件 b 」的历史记录(仿佛开启了月报宝盒)。

如楼上的几个答案所说,这和“版本控制”的理念是矛盾的。

“伪造” “造假”
比如:新文件 b.txt 提交的时候会有新建文件的记录,显示是从 a.txt 复制。就像 mv 一样 显示从 a.txt 改名 新文件名可以看到以前的老文件名记录

维护一下这个记录很难的嘛🙏

做不到,这个要看对应的 git 客户端能不能识别出他是文件改名。

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.'

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

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

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

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

  1. 合并两个修改

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

  1. 回归原分支

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

  1. 删除中间分支

git branch -d copy copy1 copy2

把 a.txt 相关的所有记录导出成文本,贴在创建 b.txt 的 commit message 里

#9
#10
#19
见 #22

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

#21 模糊识别重命名确实有这个问题,但是 Git 的原理保证:如果一个 commit 里面只发生不同内容文件的重命名(没有内容修改、没有多个同内容文件重命名),那么历史可以正确用 blame 追溯。(不满足此条件则会有模糊匹配的问题,所以我给文件改名的时候都是一个 commit 只做改名一件事的。)

本身 git 的哲学就是“保持历史真实性”,你这个需求就是“篡改历史”

相违背了。

6

想看历史记录就去看 a.txt 就好了

你需要这个东西: github.com/newren/git-filter-repo
筛选出某一个目录/文件的提交记录 和 filter-branch 不一样,更好用

我不需要“理念”
我不需要“哲学”

我只想要我复制出的文件能简单的做到历史记录追踪😐

#15 ,mv 是重命名啊,怎么到你这边就变成删文件+新建文件了……
基于“文件”进行操作,给这个 mv 操作添加到对应文件的 commit 历史中。

git-mv - Move or rename a file, a directory, or a symlink
Git - git-mv Documentation

如果按照 OP 你的意思,其实复制出来一个 b 文件。但对于你来说是知道这个文件是基于 a 文件复制出来的,但是对于 git 来说 b 文件单纯只是没有被追踪过的一个新文件。
所以你的实际操作是新增一个 b 文件,同时去按照 a 文件的编辑历史造 b 文件的整个历史,而不是共享整个历史。

“想看历史记录就去看 a.txt 就好了”,目前就是这样看历史记录的,总感觉差点意思

#29 所以你也不需要"git"

这种需求应该做成子仓库, txt 文件保留两份 blame 指向同一个记录, 必须由分支实现. 主仓库分别引用子仓库的两个分支.
你要求的这个用法就不符合 git 思想和哲学, 要么就用 24l 的方案, 要么就用最符合 git 哲学的办法达成近似效果.

git mv 等价于 mv+git rm+git add ,git 识别文件移动完全靠猜测,并没有特别记录这个信息

11 楼老哥都给你答案了,再给你个详细的
git filter-branch --tree-filter 'cp -f dir/a1 dir/a3' --tag-name-filter cat --prune-empty -- --all

#30 ,不管是使用分支冲突的方式,还是通过 filter-branch/repo 去创造 b 文件的历史,都会去修改仓库的提交历史。在多人协作的项目里面操作 git 历史一个非常危险的行为。

#22 感谢,前几天 deepseek 也给出了类似的方案,我就是嫌操作起来太复杂了,要是有 git cp 一条命令解决,这个世界就安静了,这记录如果没有简单操作实现,其实不要历史也没什么关系,提交的时候 commit 里面手动备注是从 a.txt 复制来的,到了这个位置就手动切换到 a.txt 查看历史

#34 ,识别到 rename 是猜。但是如果猜到了,那么提交历史里面是会有标注是 rename ,而不是 add 。

diff --git a/test.xt b/test2.xt
similarity index 100%
rename from test.xt
rename to testTT.xt

#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 算出来的。

#38 ,随便建了一个 test 文件,rename 的时候顺手加了个 TT 。回复的时候感觉好像改成 test2 更合适。就手动改成了 2 ,但直接发出来了,后面的 to 还没改掉。

😂 将就看吧。

这个 diff 完全是根据两个状态对比实时计算出来的,git 并没有记录任何 diff 信息

git 原生的概念是基于行操作的,甚至没有整个源文件 mv 的识别。
mv 的识别都是各大 git 第三方客户端自己实现的,为了用户的交互友好。
我曾经就遇到过,同一个提交,在某个客户端上被识别为 mv ,在另一个客户端上被识别为 del+add 。没有任何理由。
整个原理基本上是基于识别同名文件,对比文件中的内容。
你这样,文件名变了,内容也变了,那鬼才知道你的意图。

#30 git mv 和删除文件再新建没有任何区别。你在 #38 也说了,Git 是猜的,但

但是如果猜到了,那么提交历史里面是会有标注是 rename

这是错误的,commits 里不会标注为 rename ,只有计算 diff/blame 的时候 Git 才会去(根据 commits 的内容)识别重命名关系。

并没有基于行,是单纯整个文件对比,之所以可以提交 hunk ,也是前端的处理方式,内部生成虚拟文件提交到树里,这个意义上二进制文件和文本文件的唯一区别只在前端展示方面(后端影响的只有可压缩性,二进制一般不太容易压缩)

啊?这个很简单啊
首先你复制一下文件,复制以后立马就 add 并 commit (这其中不能修改,否则 git 会认为是另一个文件)
然后就只要
git log --follow -- 对应的文件
就好了呀

git mv 重命名文件 ,提交之后,回滚,到上一条记录,再重新添加旧的文件, merge 两个分支

GIT_AUTHOR_DATE=$(date -d'2012-12-12 01:00:01') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git commit -m '2012 init'

git 本身倒是确实支持随意设置提交时间(和提交人),但 git log 的顺序还是按实际顺序的,除非 merge

试试 git filter-repo

lrainner.github.io/posts/2025/05/22/note.html#%E8%BF%81%E7%A7%BB%E4%BB%93%E5%BA%93%E6%97%B6%E4%BF%9D%E7%95%99%E9%83%A8%E5%88%86commit%E5%8E%86%E5%8F%B2

#29 并不会因为你不在乎重力,你就可以直接飞起来。现在的问题是其他人推荐让你可以飞的工具,你说我一定要靠肉身,然后别人说重力是这个世界的规则,你说我不需要“规则”,只需要飞起来。
那别人就只能建议你睡一觉或者去天台了

跟修改邮箱一个样。历史记录是不变,但是,所有的 commit id 都变了。