我正在尝试搜索一个包含数十个分支的大型存储库。 具体来说,我正在寻找对特定文件有更改的分支。 我将使用什么 Git 命令来执行此操作?
不是分支对文件进行了更改。 在一个重要的方面,它甚至也不是提交 - 我们稍后会谈到这一点 - 但你必须做的是找到对文件有更改的提交,git log
(和git rev-list
)可以直接做到这一点。
找到所需的提交后,您可以看到哪些分支包含每个提交。 请记住,一个提交可以包含在数十个分支中,因为分支名称仅包含分支中包含的最后一个提交的哈希 ID。 提交本身保存了较早的提交哈希 ID,因此使用如下提交链:
o--o--.... <-- branch1
/
...--o--o
o--o--... <-- branch2
中间行提交位于两个分支上。 如果git log
找到其中一个,它就在两个分支上。
每个提交实际上只存储一个快照,以及一些元数据。 元数据包括提交的父级的提交哈希 ID,或者对于合并提交,包括其所有父级的哈希 ID。git log
能为您做的是:
- 从最后一次提交开始,由一个或多个分支名称找到...
- 对于每个提交:
- 将(单个)父级中的快照与提交中的快照进行比较;
- 报告这两个快照中的不同文件(见下文);和 移动到父
- 级,或者,对于合并提交,按某种顺序移动到一个、部分或所有父级(见下文)。
报告不同步骤的文件采用多种形式,对您最有用的一种形式是 Git 根本不报告提交的形式,如果它没有更改您指定的文件或多个文件中的任何一个:
git log --branches -- path1 path2
这从所有分支提示提交(按某种顺序)开始,访问每个分支上的提交,并对保存的快照进行成对的父子比较。 当父子路径与两个指定路径中的任何一个存在差异时,git log
打印提交。 如果没有,git log
什么也不打印。
这里处理合并提交非常棘手。 没有路径名(上面的path1
和path2
),git log
访问并打印每个提交:当它命中合并时,它会移动到所有父级(按某种顺序,因为它仍然必须一次只处理一个提交)。 但是对于路径名,当git log
命中合并提交时,它默认仅移动到所有指定文件都不会更改的父级。
也就是说,git log
一次将每个父母与孩子进行比较。 这些比较中的每一个要么显示文件都相同,要么显示一个或多个文件已更改。 然后,如果某些父级的所有这些文件都相同,Git 会选择其中一个 - 显然是随机的;你在这里无法获得真正的控制权——并且忽略了所有其他控制权。 这是默认值;这就是 Git 所说的历史简化;根据你想找到的东西,它可能是痛苦和绝望的根源,因为它完全是你不想要的。 更常见的是(可能),它做了一些你想要的事情,这就是 Git 这样做的原因——但无论哪种方式,重要的是要意识到Git 为这种git log
做了历史简化。
如果您不希望简化此历史记录,请使用:
git log --full-history --branches -- path1 path2
因为--full-history
禁用了历史记录简化。
请注意,--branches
告诉git log
从所有分支提示开始。 这将忽略其提交不在任何分支中的任何标签。 提交可能位于零分支中,尤其是在标记提交后。 要从所有标记名称开始,请添加--tags
。 若要从所有引用(包括分支和标记,还包括远程跟踪名称、refs/stash
和其他内部 Git 引用)开始,请使用--all
。 如果您使用不是分支名称的内容,请注意,您以这种方式找到的某些提交可能不在分支中。
同时,如果您真的只想查看特定分支而不是所有分支,则可以一次对一个分支执行git log
,以仅查看可从该分支访问的提交。 对你觉得有趣的(大概非常小的)分支集重复此操作。 同一个提交哈希 ID 可能会多次出现,但在任何一个git log
命令中出现后,您知道该特定提交包含在该特定分支中。 如果在git log
中使用路径名,则历史记录简化仍然适用。
这已经晚了几年,但我将以@torek出色且解释清楚的答案为基础,并给出一个我相信可以提供 OP 想要的结果的 git 咒语。
高级方法是首先获取所有远程分支,然后将每个远程分支传递给git rev-list
/git log
以获取更改相关文件的远程分支上的提交列表,最后将该提交列表传递回git branch -r --contains
,这将显示包含每个找到的提交的远程分支。
完整命令
$ git fetch --all && git branch -r $(git --no-pager rev-list $(git branch -r --format=%(refname:short)) --no-commit-header --format="format:--contains %H" -- path/to/my/file)
包含每个子命令说明的示例
$ git fetch --all # add --prune to drop branches deleted on the remotes.
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 10 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (10/10), 15.84 KiB | 1.98 MiB/s, done.
From <remote-url>
* [new branch] mybranch -> myremote/mybranch
$ git branch -r --format=%(refname:short) # only output short branch name
myremote/main
myremote/mybranch
myremote/myotherbranch
$ git --no-pager log $(git branch -r --format=%(refname:short)) -- path/to/my/file
1411178 (myremote/mybranch) my commit subject [me]
68e5dcf other commit [me]
1bd2894 (myremote/myotherbranch) changing the file [me]
374fcb2 created the file [me]
$ git --no-pager rev-list $(git branch -r --format=%(refname:short)) -- path/to/my/file
1411178f8d9832e99f75646469563e02e62e3475
68e5dcf5d6feb32f2a4b008066e2a134859b32d1
1bd2894b53989555ce875c9570e7ab80949d3d9c
374fcb22e3a9db85c679f456315b84119eed9d4a
$ git --no-pager rev-list $(git branch -r --format=%(refname:short)) --no-commit-header --format="format:--contains %H" -- path/to/my/file
--contains 1411178f8d9832e99f75646469563e02e62e3475
--contains 68e5dcf5d6feb32f2a4b008066e2a134859b32d1
--contains 1bd2894b53989555ce875c9570e7ab80949d3d9c
--contains 374fcb22e3a9db85c679f456315b84119eed9d4a
$ git branch -r $(git --no-pager rev-list $(git branch -r --format=%(refname:short)) --no-commit-header --format="format:--contains %H" -- path/to/my/file)
myremote/mybranch
myremote/myotherbranch