如何列出 git 中包含未合并提交的所有 'active' 分支
how to list all 'active' branches in git containing unmerged commits
我正在努力理解几个非常大的存储库的历史,这些存储库有数百个(旧的)分支,这些分支从未被删除(即使这些分支中的大部分工作是 'done' ).
我正在尝试找到一种方法来生成分支列表
- 包含创建分支后的提交 ('not empty')
- 尚未合并到另一个分支
如果我的假设是正确的,这应该 return 包含 unmerged/active 代码的分支列表 - 其他所有内容都可以安全删除。
一个不错的噱头是通过 git log --graph
将其可视化 - 仅显示 'current working tree',仅返回到所有 'currently active branches' 中存在的第一个提交。
非常感谢suggestions/help!
TL;DR:git branch --no-merged HEAD
可能是您想要的答案。您可能想要添加 -r
或 -a
,或者使用 HEAD
以外的内容。您可能想要 运行 这个(调整后的)命令多次,每个分支名称一次(尽管在这种情况下有一些方法可以更有效地执行此操作,在 this possible cost)。
长
重要的是要意识到 Git 实际上并没有合并 分支 。或者更准确地说,我们必须首先定义 branch 的含义(参见 What exactly do we mean by "branch"?);根据我们使用的定义,Git 没有 有 分支,或者没有 merge 分支,或者合并分支但是然后他们有时 un-merge 后来;或者还有其他可能性,这取决于 you 所说的“分支”是什么意思。 Git 做的 合并——可能对你的问题有用的方式,即——是提交。分支名称可以帮助您 Git find 提交,这些提交在 commit graph 中独立存在,这就是您将如何使用上面的答案。
A Git repository 实际上主要是 commits 的集合。 Git 与文件无关——尽管提交 包含 文件——与分支无关,或者至少与分支名称无关(well-defined,与“分支”不同"),尽管分支名称可以帮助我们找到提交。这实际上只是关于提交,所以你需要能够可视化提交:
A nice gimmick would be to to visualize this via git log --graph
您可以这样做,但是:
only displaying the 'current working tree', going back only to the first commit that's present in all of the 'currently active branches'.
工作树实际上在Git中,考虑到分支 =648=] 是首先定义的,再加上单词 active 完全是 un 定义的事实,我们可能永远不会知道“当前活跃的分支机构”甚至意味着什么。所以我们不可能那样做。
在Git中的是提交。提交:
被编号:每个都有一个唯一的编号,或哈希ID,表示在hexadecimal中。一旦某个哈希 ID 分配给某个特定的提交,就意味着 那个 提交,永远,在每个Git存储库。换句话说,这些提交哈希 ID 是普遍唯一的。1 Git 遵循这个原则做了很多事情:例如,我们将两个 Git 存储库挂钩到彼此,使用 git fetch
或 git push
,他们只交换原始哈希 ID,并立即知道哪个 提交 (因此文件)另一个 Git 需要得到。
是不可变的:任何提交的任何部分都不能改变。 (这适用于所有 Git 的内部 objects,所有这些都使用 UUID 散列方案。散列仅在 objects 不能更改时才有效。)
存储两件事:所有文件的快照(采用特殊的内部 read-only de-duplicated 格式)和一些元数据。元数据包括诸如谁做出提交以及何时提交之类的内容,而且对于 Git 的内部工作至关重要,previous 或 [=447= 的哈希 ID 列表]parent,提交。
通常每个提交中的 parent 列表只有一个元素长,这给了我们一个简单的线性 backwards-looking 提交链:
... <-F <-G <-H
这里的 H
代表链中 last 提交的实际哈希 ID。提交 H
存储所有文件的快照(截至他们在有人制作 H
时的状态)和一些元数据。 H
中的元数据包含 H
的 parent 提交 G
的哈希 ID,它存储快照和一些元数据; G
的元数据存储 F
的哈希 ID,后者存储快照和元数据;依此类推,直到永远——或者至少,直到我们回到有史以来的第一次提交,它不能有 parent,所以就没有:
A--B--C--D--E--F--G--H <-- latest
我们说提交 H
向后指向 G
,后者向后指向 F
,依此类推。提交 A
,作为第一个提交,不指向任何地方,因此允许 git log
停止。
为了找到H
,不过,我们必须告诉Git它的hash ID。为了避免自己必须记住哈希 ID,我们 Git 将此哈希 ID 保存在名称中,例如分支名称,latest
。然后该名称指向 H
,让我们开始。
1我们可以通过pigeonhole principle证明这实际上行不通。最终它 将 失败。哈希 ID 的大小决定了失败在多长时间内成为一个明显的概率;通过让它足够大,我们将失败推到我们不关心的未来,因为在凯恩斯的长期 运行 中,我们都死了。
现在我们可以看到分支名称是如何工作的
假设我们有一系列以 H
结尾的提交加上一个分支名称像 main
:
...--G--H <-- main
我们现在添加一个 second 名称,也指向 H
,这样所有提交现在都在 两个分支:
...--G--H <-- dev, main
我们需要一种方法来找出我们实际上使用的名称。为此,我们将 Git 将特殊名称 HEAD
附加到分支名称之一:
...--G--H <-- dev, main (HEAD)
这意味着我们“在”main
,已经完成 git checkout main
或 git switch main
,或者已经开始 main
。与此同时,我们 使用 提交 H
。如果我们想改用名称 dev
,我们 运行:
git switch dev
并得到:
...--G--H <-- dev (HEAD), main
我们仍在使用 commit H
,但我们通过 name dev
现在
关于 Git 的索引/staging-area 和你的工作树的简要说明
任何 Git 提交快照中的所有文件都是不可变的。但是我们希望能够改变文件:如果我们不能更改 文件,我们就无法完成任何实际的新工作。 Git 像大多数版本控制系统一样解决了这个问题:当我们 签出 一些提交时,Git 将文件从提交中复制出来进入工作区。这个工作区就是我们的工作树或者work-tree.
重要的是要认识到这些文件 不在 Git 中。它们来自 Git 的 ,但在 Git 内部,它们处于特殊的 read-only 压缩(有时高度压缩)和 de-duplicated 形式,只有 Git 本身可以读取,实际上什么都不能写。所以Git把它们复制出来,复制的不在Git里。这些副本是普通的日常文件,每个程序都可以以通常的方式读写。
当程序执行此操作时,Git 不知道它们正在执行此操作。2 这就是为什么你必须告诉 Git——用 git add
——某个文件已更新。
历史上,其他版本控制系统只是扫描更改。也就是说,你 运行 他们相当于检出,他们检出一些提交或文件。然后你 运行 他们相当于签入/提交,他们扫描所有内容,然后你出去吃午饭,因为这一步至少需要 5 分钟,可能需要一个小时或更长时间。 Git 不这样做:相反,Git 保留每个文件 的额外副本,但以压缩和de-duplicated 的形式。由于这些额外的副本刚刚来自提交,根据定义它们是重复的,因此不带 space.3 这构成了 Git 调用的大部分内容它的 index 或 staging area.
当你 运行 git add
处理某些文件时,你实际上是在告诉 Git: 读取工作树副本,并将其压缩到内部 de-duplicated表格。如果结果是重复的,de-duplicate 现在它,以便为下一次提交做好准备。否则现在就为下一次提交做好准备。 无论哪种方式,在 git add
之后,索引 / staging-area 副本现在与 working-tree 复制,并“暂存提交”。如果它与 already-committed 副本相匹配,那么当您 运行 git status
时,Git 不会 说 任何有关它的信息。如果不是,git status
说 staged for commit
。但实际上 Git 的索引 中的每个文件都是 准备提交的:这就是为什么这是 暂存区 的原因。如果 Git 说 updated in proposed next commit
,那可能会更好,但是 Git 只是说 staged for commit
。
2为了提高效率,有时使用 OS 的 file-monitoring 设施很好,而且 Git 有一些原始的能力在某些 OSes 上执行此操作。但在大多数情况下 Git 仍然没有意识到这一点。 Git有一个不同的效率技巧(如果Git可以说是有袖子的话)。
3这些索引条目还带space记录他们的名字和一堆相关数据,粗略的每个文件大约 100 个字节的顺序。
进行新提交
假设我们处于这种状态:
...--G--H <-- dev (HEAD), main
也就是说,我们在分支 dev
上并使用提交 H
。同时我们已经更新了一些文件和 运行 git add
,所以 staged-for-commit 副本与 in commit H
。我们现在 运行 git commit
和 Git 按某种顺序执行以下步骤:
- Git 收集它需要的任何额外元数据,例如我们的姓名和电子邮件地址以及当前 date-and-time 和日志消息。
- Git 将当前提交解析为原始哈希 ID(
H
的哈希 ID)以作为 parent 提交列表放入。
- Git 快照出现在索引中时会一直冻结。
- Git 将所有这些合并到一个新的提交中,该提交将获得一个新的唯一哈希 ID;我们称之为
I
。请注意,新提交I
指向现有提交 H
.
- 这是棘手的部分:Git 将新提交的哈希 ID 写入当前 分支名称。
所以现在我们有:
I <-- dev (HEAD)
/
...--G--H <-- main
注意git branch
没有创建分支; git commit
创建了分支。 至少,只要“分支”意味着提交 I
,现在完全在 dev
,“分支关闭”,就会发生这种情况" 来自 main
.
随着我们进行更多的提交,它们会添加到 I
:
I--J <-- dev (HEAD)
/
...--G--H <-- main
直到我们 git switch
回到 main
:
I--J <-- dev
/
...--G--H <-- main (HEAD)
当我们做切换提交时,Git从工作树(及其索引/staging-area)中删除提交J
,并改为放入来自提交 H
的文件。这里还有很多技巧,但我们会忽略它。
如果我们创建第三个名称并切换到该名称,再添加两次提交,我们会遇到这种情况:
I--J <-- dev
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
这里有两点很重要:
- 通过并包括
H
的提交在 所有分支上 。
- 名称
main
在某种意义上不再需要:它的目的是定位提交H
。它仍然服务于此目的,但提交 J
和 L
也是如此。通过从 dev
(J
) 开始并向后工作,我们将到达并因此找到提交 H
。这同样适用于提交 L
。但是,我们确实需要名称dev
和feature
,因为这些名称是仅查找提交的方法I-J
和 K-L
分别。4
4如果你搞砸了——这在 Git 中很容易做到——Git 提供了多种再次查找提交的方法,一段时间。最终,那些名为 reflogs 的“从错误中恢复”条目将过期。删除分支名称会删除分支的引用日志,这可能是一个错误,在 15 年多的时间里都没有得到纠正,因此至少应该对 branch-name 删除保持谨慎。如果 Git 保留这些 reflogs,并且正在进行的工作可能会导致这种情况,您可以“un-delete”一个分支名称。
真正的合并
一旦我们有了一个 branch-y 提交结构——其中有一个分支的提交图——就像这样:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
我们经常发现使用 git merge
很有趣也很有用。 git merge
对这些所做的,表示为 high-level 目标,是 合并工作 。在这种情况下,“工作”是根据 变化 来定义的。 Git 不存储更改:Git 存储提交。所以要获得更改,Git必须比较提交。
我们每天都在 git show
或 git log -p
中看到这一点。当我们使用这些命令时,Git 找到一个提交并使用该提交的元数据找到该提交的 parent 提交:
...--o--o--P--C--o--...
为了“显示”提交C
,Git找到它的parentP
,提取两个快照,并进行比较他们。对于每个相同的文件,Git 什么都不说,对于每个不同的文件,Git 计算出一个配方,该配方将更改 P
中该文件的副本以匹配复制 C
并生成该食谱。
如果 work 是 changes,并且如果我们有:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
然后很明显5 如果我们将 H
中的快照与 J
中的快照进行比较,我们将找出发生了什么工作br1
。如果我们将 H
中的快照与 L
中的快照进行比较,我们将了解 br2
上发生了什么工作。此外,这会产生两个 更改配方 ,如果应用于 H
,则分别在 J
和 L
中生成快照。如果我们合并这两个食谱,我们将合并工作。
也就是说,假设一个食谱说要修改某个文件,而另一个根本没有提到该文件。组合是采取变化。如果两个食谱都说要更改 shared 文件,我们只需将两个更改组合起来:只要它们针对文件的不同 regions,我们可能可以做到这一点。我们将在这里跳过整个机制,并假设 Git 可以 组合更改并正确执行。6 Git 将组合更改应用到 H
的 common-starting-point 快照,并进行新的 merge commit M
:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
提交 M
像往常一样有一个快照:快照是通过将组合更改应用于 H
的快照而构建的。提交 M
像往常一样有元数据:你是 author-and-committer,它的 date-and-time 是“现在”,它的默认日志消息是相当无用的7 merge branch br2 into br1
。 only 关于 M
的不同和特殊之处在于,而不是 onparentJ
,它有两个:J
和L
。因此,当 git log
查看什么提交“在”分支 br1
上时,Git 将遵循 两个链接 ,并提交 L
`K 现在会在树枝上,即使他们刚才不在树枝上。
如果我们不再需要快速找到提交 K-L
,我们现在可以 删除 名称 br2
:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L
我们仍然可以通过回到 M
的 second parent 找到提交 L
,从那里我们可以找到 K
。所以我们可能会删除名称 br1
。 如果我们不这样做,我们就会得到您首先写的 post 的问题。
5数学家用这句话来表示我不想证明,这样说你就太尴尬了让我这样做。
6和Git一样愚蠢——它不知道文件的内容;它只是在这里应用简单的 line-by-line 文本规则——这实际上经常出奇地有效。但这对于 XML 或 JSON 数据就不那么正确了;不要让 Git 合并 XML 或其他未经仔细检查或测试的结构化文本。
7这并不总是完全没用,但任何auto-generated文字很少能像某些东西一样好还真有人想过。不过,大多数人通常不会编写好的合并消息;您可以通过查看两个 parent 链来得出有用的数据。
不合并的东西
假设我们有一个更简单的图表而不是上面的 branch-y 图:
I--J <-- dev
/
...--G--H <-- main (HEAD)
假设我们现在 运行 git merge dev
将在 main
上完成的工作与在 dev
上完成的工作相结合。我们在 main
上所做的“工作”将是:与提交 H
中的文件相比,提交 H
中的任何内容。但是提交 H
中的文件 根据定义 将匹配提交 H
中的文件。因此,main
上尚未完成的工作尚未在 main
上完成。为此,我们想要 添加 在 dev
上完成的工作,如果我们比较 H
与 [=97=,这就是我们将看到的配方].
Git 可以作为常规合并来执行此操作:
I--J <-- dev
/ \
...--G--H------M <-- main (HEAD)
但如果 Git 使用标准合并代码执行此操作,则 M
中的 快照 将与 J
中的快照完全匹配。提交 M
在某种意义上不是 必需的 。我们确实需要它,如果我们想知道某些特征被合并,但我们不需要如果我们只想跟踪提交和所有 work.
就需要它
默认情况下,Git 不会在这里进行完全合并。相反,git merge dev
只是执行 git checkout
或 git switch
来提交 J
,同时 向前拖动分支名称 ,像这样:
I--J <-- dev, main (HEAD)
/
...--G--H
然后没有理由不把所有东西画在一条线上:
...--G--H--I--J <-- dev, main (HEAD)
我们现在可以像以前一样安全地删除名称 dev
,不留下任何合并操作的痕迹。但是,如果我们 不 ,并在 main
上进行更多提交或以其他方式推进名称 main
,我们将得到:
...--G--H--I--J <-- dev
\
K <-- main (HEAD)
正如 br2
会在真正合并后留在 br1
后面。
现在我们可以理解git branch --merged
和git branch --no-merged
这些命令需要一个输入:提交。我们选择一些提交,比如 J
或 K
或 H
或其他。然后它查看所有 分支名称 ,或 -r
,所有 remote-tracking 名称(我将在片刻)。对于每个这样的名称:
- 名称select一些提交;
- 该提交是在我们选择的提交“之前”还是“之后”?
注意可以两者都,如:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
在这里,提交 L
,通过名称 br2
找到,落后于 br1
或提交 J
因为提交 I
和 J
仅在 br1
上。但它也 领先于 br1
,因为提交 K
和 L
仅在 br2
上。随着:
...--o--P--C--o--...
commit P
比 C
落后一步,C
比 P
领先一步,并且没有并发症,但是当出现“branch-y”图结构,有这些并发症。
--no-merged
所做的是查找任何 names 以找到任何 commits “领先于”selected 提交。因此,如果我们 select 提交 H
,那么 git branch --no-merged
将向我们显示名称 br1
和 br2
,因为这两个名称都在 H
之前。但是如果我们 select 提交 J
,git branch --no-merged
将只显示名称 br2
,因为 br1
selects J
,这并不领先于 J
.
--merged
所做的是相似的,除了它向我们显示任何名称,其中名称 select 是 不在 我们之前提交的提交挑选。让我们再次使用此图,但添加指向 H
的名称 main
,并切换到 main
:
I--J <-- br1
/
...--G--H <-- main (HEAD)
\
K--L <-- br2
如果我们选择 main
/ HEAD
作为提交,git branch --merged
命令将只显示 main
,因为 br1
和 br2
都 领先于 提交 H
。请注意,--merged
将“偶数”分支计为合并,并且由于 main
selects H
,git branch --merged main
打印 main
.
如果我们选择提交 J
,但是,它将向我们显示名称 main
和 br1
,因为 那些 名称选择一个不在提交 J
之前的提交。或者,如果我们选择提交 L
,它会显示名称 main
和 br2
.
Remote-tracking 名字
Git 不仅仅是一个版本控制系统。它是一个 分布式 (实际上更重要的是,复制的)版本控制系统。我们使用 git clone
制作存储库的副本。每个存储库都包含提交,但每个存储库 还 包含这些分支名称,可帮助我们 找到 提交。
当我们克隆一个存储库时,我们复制它的所有提交8和none它的分支名称。也就是说,我们复制的存储库中的名称 对该特定存储库 是私有的。但是,我们可以 看到 他们,而我们的 Git 在我们的存储库上工作,连接到他们的 Git 软件,该软件正在读取 他们的 资料库。所以我们的 Git 将它们的 对存储在 我们的 存储库中,但首先它 更改名称 .
我们为他们的存储库命名。我们用于“那个”另一个存储库(当只有一个这样的存储库时)的标准名称是 origin
。即我们运行:
git clone -o origin <url>
而我们的 Git 将 URL 保存在名称 origin
下。如果我们不使用-o
,无论如何默认名称都是origin
,所以我们大多不使用-o
。在任何情况下,这个名称——几乎总是 origin
,尽管你可以更改它——是 Git 称为 远程 的东西。它主要是一个简短的名称,我们可以通过它来引用他们的存储库,而不是重复输入 URL。9 我喜欢将其称为“他们的 Git”:他们的 Git 软件在这个 URL 上回答,它将他们的 Git 软件连接到他们的存储库,或“他们的 Git”。
要构建它将用于保存 他们的 分支名称的名称,我们的 Git 将我们的远程名称放在前面他们 Git 的分支名称:例如,他们的 main
变成了我们的 origin/main
,而他们的 dev
变成了我们的 origin/dev
。所以在 git clone
之后,我们有一个包含所有提交的存储库,并且它们所有的 分支 名称都变成了这些有趣的 origin
前缀名称。这些名称对应于它们的分支名称,但它们实际上不是 branch 名称:如果您 git checkout origin/dev
您的 Git 告诉您它已进入“分离 HEAD”模式.
完成所有这些复制后,git clone
的最后一步是我们的 Git 将 create one 分店名称。我们选择带有 -b
的分支名称:例如 git clone -b dev <em>url</em>
。如果我们不取一个带-b
的名字,我们的Git会问他们Git他们推荐什么,通常是master
或 main
,然后我们的 Git 创建该名称。
这意味着我们最终得到一个包含 所有提交(但请参阅脚注 8)和 一个分支 的存储库.他们的分支已经成为我们的remote-tracking名字。我们的一个分支,git clone
作为其最后一步创建,指向相同的 commit 作为他们的分支名称之一,这就是我们现在签出的分支。
要更新我们的remote-tracking名字,我们运行git fetch
:
git fetch origin
这告诉我们的 Git 查找名称 origin
,将其转换为 URL,联系那里的 Git 软件,并让他们列出他们的名字分支名称和哈希 ID。我们的 Git 可以立即从哈希 ID 判断我们是否拥有他们的所有提交,或者是否需要从他们那里获得一些提交。如果我们需要提交,我们的 Git 会与他们的 Git 交谈以生成更完整的列表,然后获取他们的新提交并将其填充到我们的存储库中:因为这些 相同 提交,它们具有 相同的哈希 ID。现在我们有他们所有的提交,加上我们之前有但他们没有的任何提交。
从他们那里获得我们需要的任何新提交后,我们的 Git 现在更新我们的 remote-tracking 名称以记住哪些提交他们的 分支 名称记住。然后我们完成了获取,我们的 Git 与他们的 Git.
断开了连接
(如果我们想发送他们承诺我们有那个不要,我们用git push
。这几乎是 git fetch
的镜像,有一个非常大的例外:他们没有任何 us 的 remote-tracking 名字。在我们向他们发送新提交后,我们要求他们创建或设置他们的 分支 名称之一。但我们将在这里跳过所有这些。)
8这有点夸大其词:我们复制了 reachable 提交,我们可以有意限制其中的数量抄也。但是默认的是复制所有可达的commit,大家一般不会担心nominally-removed、still-findable-by-reflog的commit,所以说“all commits”是个好主意怎么想,只要记得有脚注就可以了。
9在原始的Git中,你确实每次都必须输入URL。这很漂亮 error-prone 并且 Git 的人发明了一堆不同的 hack 来绕过它。最后,真正卡住的是 远程 、origin
.
的想法
结论
git branch
命令是user-facing(或porcelain)命令,它遍历分支名称,或者看起来像分支名称的东西,例如remote-tracking名字。它还允许我们创建和删除分支名称,尽管这不是我们在这里关心的。
使用--merged
或--no-merged
,我们可以在我们的存储库中挑选出一个提交,并询问哪些名称-分支and/or remote-tracking 名称——在我们的存储库中指向特定的提交,这些提交要么 不领先于 (--merged
) 要么 领先于 (--no-merged
) 我们挑选的一个提交。由于提交图的性质和分支名称的工作方式,这通常会让我们在这里得到我们想要的。
(请注意,我们上面没有涉及的 so-called squash 合并 根本不是合并,因此如果有人已经使用壁球合并。)
除了 I stumbled upon this - 弄清楚 'obsolete' 分支确实分解为
git branch -r | xargs -t -n 1 git branch -r --contains
我想出了一个包含 git branch --merged
的 PowerShell 片段,可以集成到基于豪华的环境中:
[CmdletBinding()]
param (
# path to local git repository
[Parameter(Mandatory)]
[string]
$Path,
# the branches (regex) in this list will be ignored
[Parameter(Mandatory = $False)]
[string[]]
$IgnoreBranches = @('main', 'master', 'dev', 'develop', '^.+_Maintenance$')
)
$ErrorActionPreference = 'Stop'
function Exec {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[scriptblock]
$ScriptBlock
)
$LASTEXITCODE = 0
try {
& $ScriptBlock
$theEc = $LASTEXITCODE
}
finally {
if ($theEc -ne 0) {
Write-Error "expected 0 exit code, got $theEc"
Write-Error $ScriptBlock.ToString()
throw "command exited with $theEc"
}
}
}
Push-Location $Path
try {
# ensure we're not analyzing a shallow checkout, prune branches deleted on remote
Exec { git fetch --prune }
# get list of all branches that exist on remote
$remoteBranches = (Exec { git branch -r }).Trim()
# map used to store wich branches are fully contained in other branches (b -> fully contained in (a,c,d))
# string -> string[]
$fullyContainedBranches = @{}
# foreach branch, figure out what other branches are "fully contained" -> candidates for deletion
foreach ($b in $remoteBranches) {
$b = $b.Split(' ')[0]
Write-Verbose " checking fully integrated branches of '$b'"
$fullyContained = Exec { git branch -r --merged $b }
if (-not $fullyContained) {
Write-Verbose " '$b' is already gone for good!"
$fullyContainedBranches[$b] = @()
continue
}
foreach ($f in $fullyContained) {
$f = $f.Replace('* ', '').Trim()
Write-Verbose " -> '$f' is in '$b'"
if ($f -eq $b) {
continue
}
if (-not $fullyContainedBranches[$f]) {
$fullyContainedBranches[$f] = @()
}
$fullyContainedBranches[$f] += $b
}
}
$fullyContainedBranches.Keys | Foreach-Object {
$branchName = $_
if ($IgnoreBranches) {
if (($IgnoreBranches | ForEach-Object { $branchName -match $_ }) -contains $true) {
Write-Verbose "ignoring $branchName"
return
}
}
# put output objects to pipeline
@{
branch = $branchName
contained_in = $fullyContainedBranches[$branchName]
}
}
}
finally {
Pop-Location
}
我正在努力理解几个非常大的存储库的历史,这些存储库有数百个(旧的)分支,这些分支从未被删除(即使这些分支中的大部分工作是 'done' ).
我正在尝试找到一种方法来生成分支列表
- 包含创建分支后的提交 ('not empty')
- 尚未合并到另一个分支
如果我的假设是正确的,这应该 return 包含 unmerged/active 代码的分支列表 - 其他所有内容都可以安全删除。
一个不错的噱头是通过 git log --graph
将其可视化 - 仅显示 'current working tree',仅返回到所有 'currently active branches' 中存在的第一个提交。
非常感谢suggestions/help!
TL;DR:git branch --no-merged HEAD
可能是您想要的答案。您可能想要添加 -r
或 -a
,或者使用 HEAD
以外的内容。您可能想要 运行 这个(调整后的)命令多次,每个分支名称一次(尽管在这种情况下有一些方法可以更有效地执行此操作,在 this possible cost)。
长
重要的是要意识到 Git 实际上并没有合并 分支 。或者更准确地说,我们必须首先定义 branch 的含义(参见 What exactly do we mean by "branch"?);根据我们使用的定义,Git 没有 有 分支,或者没有 merge 分支,或者合并分支但是然后他们有时 un-merge 后来;或者还有其他可能性,这取决于 you 所说的“分支”是什么意思。 Git 做的 合并——可能对你的问题有用的方式,即——是提交。分支名称可以帮助您 Git find 提交,这些提交在 commit graph 中独立存在,这就是您将如何使用上面的答案。
A Git repository 实际上主要是 commits 的集合。 Git 与文件无关——尽管提交 包含 文件——与分支无关,或者至少与分支名称无关(well-defined,与“分支”不同"),尽管分支名称可以帮助我们找到提交。这实际上只是关于提交,所以你需要能够可视化提交:
A nice gimmick would be to to visualize this via
git log --graph
您可以这样做,但是:
only displaying the 'current working tree', going back only to the first commit that's present in all of the 'currently active branches'.
工作树实际上在Git中,考虑到分支 =648=] 是首先定义的,再加上单词 active 完全是 un 定义的事实,我们可能永远不会知道“当前活跃的分支机构”甚至意味着什么。所以我们不可能那样做。
在Git中的是提交。提交:
被编号:每个都有一个唯一的编号,或哈希ID,表示在hexadecimal中。一旦某个哈希 ID 分配给某个特定的提交,就意味着 那个 提交,永远,在每个Git存储库。换句话说,这些提交哈希 ID 是普遍唯一的。1 Git 遵循这个原则做了很多事情:例如,我们将两个 Git 存储库挂钩到彼此,使用
git fetch
或git push
,他们只交换原始哈希 ID,并立即知道哪个 提交 (因此文件)另一个 Git 需要得到。是不可变的:任何提交的任何部分都不能改变。 (这适用于所有 Git 的内部 objects,所有这些都使用 UUID 散列方案。散列仅在 objects 不能更改时才有效。)
存储两件事:所有文件的快照(采用特殊的内部 read-only de-duplicated 格式)和一些元数据。元数据包括诸如谁做出提交以及何时提交之类的内容,而且对于 Git 的内部工作至关重要,previous 或 [=447= 的哈希 ID 列表]parent,提交。
通常每个提交中的 parent 列表只有一个元素长,这给了我们一个简单的线性 backwards-looking 提交链:
... <-F <-G <-H
这里的 H
代表链中 last 提交的实际哈希 ID。提交 H
存储所有文件的快照(截至他们在有人制作 H
时的状态)和一些元数据。 H
中的元数据包含 H
的 parent 提交 G
的哈希 ID,它存储快照和一些元数据; G
的元数据存储 F
的哈希 ID,后者存储快照和元数据;依此类推,直到永远——或者至少,直到我们回到有史以来的第一次提交,它不能有 parent,所以就没有:
A--B--C--D--E--F--G--H <-- latest
我们说提交 H
向后指向 G
,后者向后指向 F
,依此类推。提交 A
,作为第一个提交,不指向任何地方,因此允许 git log
停止。
为了找到H
,不过,我们必须告诉Git它的hash ID。为了避免自己必须记住哈希 ID,我们 Git 将此哈希 ID 保存在名称中,例如分支名称,latest
。然后该名称指向 H
,让我们开始。
1我们可以通过pigeonhole principle证明这实际上行不通。最终它 将 失败。哈希 ID 的大小决定了失败在多长时间内成为一个明显的概率;通过让它足够大,我们将失败推到我们不关心的未来,因为在凯恩斯的长期 运行 中,我们都死了。
现在我们可以看到分支名称是如何工作的
假设我们有一系列以 H
结尾的提交加上一个分支名称像 main
:
...--G--H <-- main
我们现在添加一个 second 名称,也指向 H
,这样所有提交现在都在 两个分支:
...--G--H <-- dev, main
我们需要一种方法来找出我们实际上使用的名称。为此,我们将 Git 将特殊名称 HEAD
附加到分支名称之一:
...--G--H <-- dev, main (HEAD)
这意味着我们“在”main
,已经完成 git checkout main
或 git switch main
,或者已经开始 main
。与此同时,我们 使用 提交 H
。如果我们想改用名称 dev
,我们 运行:
git switch dev
并得到:
...--G--H <-- dev (HEAD), main
我们仍在使用 commit H
,但我们通过 name dev
现在
关于 Git 的索引/staging-area 和你的工作树的简要说明
任何 Git 提交快照中的所有文件都是不可变的。但是我们希望能够改变文件:如果我们不能更改 文件,我们就无法完成任何实际的新工作。 Git 像大多数版本控制系统一样解决了这个问题:当我们 签出 一些提交时,Git 将文件从提交中复制出来进入工作区。这个工作区就是我们的工作树或者work-tree.
重要的是要认识到这些文件 不在 Git 中。它们来自 Git 的 ,但在 Git 内部,它们处于特殊的 read-only 压缩(有时高度压缩)和 de-duplicated 形式,只有 Git 本身可以读取,实际上什么都不能写。所以Git把它们复制出来,复制的不在Git里。这些副本是普通的日常文件,每个程序都可以以通常的方式读写。
当程序执行此操作时,Git 不知道它们正在执行此操作。2 这就是为什么你必须告诉 Git——用 git add
——某个文件已更新。
历史上,其他版本控制系统只是扫描更改。也就是说,你 运行 他们相当于检出,他们检出一些提交或文件。然后你 运行 他们相当于签入/提交,他们扫描所有内容,然后你出去吃午饭,因为这一步至少需要 5 分钟,可能需要一个小时或更长时间。 Git 不这样做:相反,Git 保留每个文件 的额外副本,但以压缩和de-duplicated 的形式。由于这些额外的副本刚刚来自提交,根据定义它们是重复的,因此不带 space.3 这构成了 Git 调用的大部分内容它的 index 或 staging area.
当你 运行 git add
处理某些文件时,你实际上是在告诉 Git: 读取工作树副本,并将其压缩到内部 de-duplicated表格。如果结果是重复的,de-duplicate 现在它,以便为下一次提交做好准备。否则现在就为下一次提交做好准备。 无论哪种方式,在 git add
之后,索引 / staging-area 副本现在与 working-tree 复制,并“暂存提交”。如果它与 already-committed 副本相匹配,那么当您 运行 git status
时,Git 不会 说 任何有关它的信息。如果不是,git status
说 staged for commit
。但实际上 Git 的索引 中的每个文件都是 准备提交的:这就是为什么这是 暂存区 的原因。如果 Git 说 updated in proposed next commit
,那可能会更好,但是 Git 只是说 staged for commit
。
2为了提高效率,有时使用 OS 的 file-monitoring 设施很好,而且 Git 有一些原始的能力在某些 OSes 上执行此操作。但在大多数情况下 Git 仍然没有意识到这一点。 Git有一个不同的效率技巧(如果Git可以说是有袖子的话)。
3这些索引条目还带space记录他们的名字和一堆相关数据,粗略的每个文件大约 100 个字节的顺序。
进行新提交
假设我们处于这种状态:
...--G--H <-- dev (HEAD), main
也就是说,我们在分支 dev
上并使用提交 H
。同时我们已经更新了一些文件和 运行 git add
,所以 staged-for-commit 副本与 in commit H
。我们现在 运行 git commit
和 Git 按某种顺序执行以下步骤:
- Git 收集它需要的任何额外元数据,例如我们的姓名和电子邮件地址以及当前 date-and-time 和日志消息。
- Git 将当前提交解析为原始哈希 ID(
H
的哈希 ID)以作为 parent 提交列表放入。 - Git 快照出现在索引中时会一直冻结。
- Git 将所有这些合并到一个新的提交中,该提交将获得一个新的唯一哈希 ID;我们称之为
I
。请注意,新提交I
指向现有提交H
. - 这是棘手的部分:Git 将新提交的哈希 ID 写入当前 分支名称。
所以现在我们有:
I <-- dev (HEAD)
/
...--G--H <-- main
注意git branch
没有创建分支; git commit
创建了分支。 至少,只要“分支”意味着提交 I
,现在完全在 dev
,“分支关闭”,就会发生这种情况" 来自 main
.
随着我们进行更多的提交,它们会添加到 I
:
I--J <-- dev (HEAD)
/
...--G--H <-- main
直到我们 git switch
回到 main
:
I--J <-- dev
/
...--G--H <-- main (HEAD)
当我们做切换提交时,Git从工作树(及其索引/staging-area)中删除提交J
,并改为放入来自提交 H
的文件。这里还有很多技巧,但我们会忽略它。
如果我们创建第三个名称并切换到该名称,再添加两次提交,我们会遇到这种情况:
I--J <-- dev
/
...--G--H <-- main
\
K--L <-- feature (HEAD)
这里有两点很重要:
- 通过并包括
H
的提交在 所有分支上 。 - 名称
main
在某种意义上不再需要:它的目的是定位提交H
。它仍然服务于此目的,但提交J
和L
也是如此。通过从dev
(J
) 开始并向后工作,我们将到达并因此找到提交H
。这同样适用于提交L
。但是,我们确实需要名称dev
和feature
,因为这些名称是仅查找提交的方法I-J
和K-L
分别。4
4如果你搞砸了——这在 Git 中很容易做到——Git 提供了多种再次查找提交的方法,一段时间。最终,那些名为 reflogs 的“从错误中恢复”条目将过期。删除分支名称会删除分支的引用日志,这可能是一个错误,在 15 年多的时间里都没有得到纠正,因此至少应该对 branch-name 删除保持谨慎。如果 Git 保留这些 reflogs,并且正在进行的工作可能会导致这种情况,您可以“un-delete”一个分支名称。
真正的合并
一旦我们有了一个 branch-y 提交结构——其中有一个分支的提交图——就像这样:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
我们经常发现使用 git merge
很有趣也很有用。 git merge
对这些所做的,表示为 high-level 目标,是 合并工作 。在这种情况下,“工作”是根据 变化 来定义的。 Git 不存储更改:Git 存储提交。所以要获得更改,Git必须比较提交。
我们每天都在 git show
或 git log -p
中看到这一点。当我们使用这些命令时,Git 找到一个提交并使用该提交的元数据找到该提交的 parent 提交:
...--o--o--P--C--o--...
为了“显示”提交C
,Git找到它的parentP
,提取两个快照,并进行比较他们。对于每个相同的文件,Git 什么都不说,对于每个不同的文件,Git 计算出一个配方,该配方将更改 P
中该文件的副本以匹配复制 C
并生成该食谱。
如果 work 是 changes,并且如果我们有:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
然后很明显5 如果我们将 H
中的快照与 J
中的快照进行比较,我们将找出发生了什么工作br1
。如果我们将 H
中的快照与 L
中的快照进行比较,我们将了解 br2
上发生了什么工作。此外,这会产生两个 更改配方 ,如果应用于 H
,则分别在 J
和 L
中生成快照。如果我们合并这两个食谱,我们将合并工作。
也就是说,假设一个食谱说要修改某个文件,而另一个根本没有提到该文件。组合是采取变化。如果两个食谱都说要更改 shared 文件,我们只需将两个更改组合起来:只要它们针对文件的不同 regions,我们可能可以做到这一点。我们将在这里跳过整个机制,并假设 Git 可以 组合更改并正确执行。6 Git 将组合更改应用到 H
的 common-starting-point 快照,并进行新的 merge commit M
:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
提交 M
像往常一样有一个快照:快照是通过将组合更改应用于 H
的快照而构建的。提交 M
像往常一样有元数据:你是 author-and-committer,它的 date-and-time 是“现在”,它的默认日志消息是相当无用的7 merge branch br2 into br1
。 only 关于 M
的不同和特殊之处在于,而不是 onparentJ
,它有两个:J
和L
。因此,当 git log
查看什么提交“在”分支 br1
上时,Git 将遵循 两个链接 ,并提交 L
`K 现在会在树枝上,即使他们刚才不在树枝上。
如果我们不再需要快速找到提交 K-L
,我们现在可以 删除 名称 br2
:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L
我们仍然可以通过回到 M
的 second parent 找到提交 L
,从那里我们可以找到 K
。所以我们可能会删除名称 br1
。 如果我们不这样做,我们就会得到您首先写的 post 的问题。
5数学家用这句话来表示我不想证明,这样说你就太尴尬了让我这样做。
6和Git一样愚蠢——它不知道文件的内容;它只是在这里应用简单的 line-by-line 文本规则——这实际上经常出奇地有效。但这对于 XML 或 JSON 数据就不那么正确了;不要让 Git 合并 XML 或其他未经仔细检查或测试的结构化文本。
7这并不总是完全没用,但任何auto-generated文字很少能像某些东西一样好还真有人想过。不过,大多数人通常不会编写好的合并消息;您可以通过查看两个 parent 链来得出有用的数据。
不合并的东西
假设我们有一个更简单的图表而不是上面的 branch-y 图:
I--J <-- dev
/
...--G--H <-- main (HEAD)
假设我们现在 运行 git merge dev
将在 main
上完成的工作与在 dev
上完成的工作相结合。我们在 main
上所做的“工作”将是:与提交 H
中的文件相比,提交 H
中的任何内容。但是提交 H
中的文件 根据定义 将匹配提交 H
中的文件。因此,main
上尚未完成的工作尚未在 main
上完成。为此,我们想要 添加 在 dev
上完成的工作,如果我们比较 H
与 [=97=,这就是我们将看到的配方].
Git 可以作为常规合并来执行此操作:
I--J <-- dev
/ \
...--G--H------M <-- main (HEAD)
但如果 Git 使用标准合并代码执行此操作,则 M
中的 快照 将与 J
中的快照完全匹配。提交 M
在某种意义上不是 必需的 。我们确实需要它,如果我们想知道某些特征被合并,但我们不需要如果我们只想跟踪提交和所有 work.
默认情况下,Git 不会在这里进行完全合并。相反,git merge dev
只是执行 git checkout
或 git switch
来提交 J
,同时 向前拖动分支名称 ,像这样:
I--J <-- dev, main (HEAD)
/
...--G--H
然后没有理由不把所有东西画在一条线上:
...--G--H--I--J <-- dev, main (HEAD)
我们现在可以像以前一样安全地删除名称 dev
,不留下任何合并操作的痕迹。但是,如果我们 不 ,并在 main
上进行更多提交或以其他方式推进名称 main
,我们将得到:
...--G--H--I--J <-- dev
\
K <-- main (HEAD)
正如 br2
会在真正合并后留在 br1
后面。
现在我们可以理解git branch --merged
和git branch --no-merged
这些命令需要一个输入:提交。我们选择一些提交,比如 J
或 K
或 H
或其他。然后它查看所有 分支名称 ,或 -r
,所有 remote-tracking 名称(我将在片刻)。对于每个这样的名称:
- 名称select一些提交;
- 该提交是在我们选择的提交“之前”还是“之后”?
注意可以两者都,如:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
在这里,提交 L
,通过名称 br2
找到,落后于 br1
或提交 J
因为提交 I
和 J
仅在 br1
上。但它也 领先于 br1
,因为提交 K
和 L
仅在 br2
上。随着:
...--o--P--C--o--...
commit P
比 C
落后一步,C
比 P
领先一步,并且没有并发症,但是当出现“branch-y”图结构,有这些并发症。
--no-merged
所做的是查找任何 names 以找到任何 commits “领先于”selected 提交。因此,如果我们 select 提交 H
,那么 git branch --no-merged
将向我们显示名称 br1
和 br2
,因为这两个名称都在 H
之前。但是如果我们 select 提交 J
,git branch --no-merged
将只显示名称 br2
,因为 br1
selects J
,这并不领先于 J
.
--merged
所做的是相似的,除了它向我们显示任何名称,其中名称 select 是 不在 我们之前提交的提交挑选。让我们再次使用此图,但添加指向 H
的名称 main
,并切换到 main
:
I--J <-- br1
/
...--G--H <-- main (HEAD)
\
K--L <-- br2
如果我们选择 main
/ HEAD
作为提交,git branch --merged
命令将只显示 main
,因为 br1
和 br2
都 领先于 提交 H
。请注意,--merged
将“偶数”分支计为合并,并且由于 main
selects H
,git branch --merged main
打印 main
.
如果我们选择提交 J
,但是,它将向我们显示名称 main
和 br1
,因为 那些 名称选择一个不在提交 J
之前的提交。或者,如果我们选择提交 L
,它会显示名称 main
和 br2
.
Remote-tracking 名字
Git 不仅仅是一个版本控制系统。它是一个 分布式 (实际上更重要的是,复制的)版本控制系统。我们使用 git clone
制作存储库的副本。每个存储库都包含提交,但每个存储库 还 包含这些分支名称,可帮助我们 找到 提交。
当我们克隆一个存储库时,我们复制它的所有提交8和none它的分支名称。也就是说,我们复制的存储库中的名称 对该特定存储库 是私有的。但是,我们可以 看到 他们,而我们的 Git 在我们的存储库上工作,连接到他们的 Git 软件,该软件正在读取 他们的 资料库。所以我们的 Git 将它们的
我们为他们的存储库命名。我们用于“那个”另一个存储库(当只有一个这样的存储库时)的标准名称是 origin
。即我们运行:
git clone -o origin <url>
而我们的 Git 将 URL 保存在名称 origin
下。如果我们不使用-o
,无论如何默认名称都是origin
,所以我们大多不使用-o
。在任何情况下,这个名称——几乎总是 origin
,尽管你可以更改它——是 Git 称为 远程 的东西。它主要是一个简短的名称,我们可以通过它来引用他们的存储库,而不是重复输入 URL。9 我喜欢将其称为“他们的 Git”:他们的 Git 软件在这个 URL 上回答,它将他们的 Git 软件连接到他们的存储库,或“他们的 Git”。
要构建它将用于保存 他们的 分支名称的名称,我们的 Git 将我们的远程名称放在前面他们 Git 的分支名称:例如,他们的 main
变成了我们的 origin/main
,而他们的 dev
变成了我们的 origin/dev
。所以在 git clone
之后,我们有一个包含所有提交的存储库,并且它们所有的 分支 名称都变成了这些有趣的 origin
前缀名称。这些名称对应于它们的分支名称,但它们实际上不是 branch 名称:如果您 git checkout origin/dev
您的 Git 告诉您它已进入“分离 HEAD”模式.
完成所有这些复制后,git clone
的最后一步是我们的 Git 将 create one 分店名称。我们选择带有 -b
的分支名称:例如 git clone -b dev <em>url</em>
。如果我们不取一个带-b
的名字,我们的Git会问他们Git他们推荐什么,通常是master
或 main
,然后我们的 Git 创建该名称。
这意味着我们最终得到一个包含 所有提交(但请参阅脚注 8)和 一个分支 的存储库.他们的分支已经成为我们的remote-tracking名字。我们的一个分支,git clone
作为其最后一步创建,指向相同的 commit 作为他们的分支名称之一,这就是我们现在签出的分支。
要更新我们的remote-tracking名字,我们运行git fetch
:
git fetch origin
这告诉我们的 Git 查找名称 origin
,将其转换为 URL,联系那里的 Git 软件,并让他们列出他们的名字分支名称和哈希 ID。我们的 Git 可以立即从哈希 ID 判断我们是否拥有他们的所有提交,或者是否需要从他们那里获得一些提交。如果我们需要提交,我们的 Git 会与他们的 Git 交谈以生成更完整的列表,然后获取他们的新提交并将其填充到我们的存储库中:因为这些 相同 提交,它们具有 相同的哈希 ID。现在我们有他们所有的提交,加上我们之前有但他们没有的任何提交。
从他们那里获得我们需要的任何新提交后,我们的 Git 现在更新我们的 remote-tracking 名称以记住哪些提交他们的 分支 名称记住。然后我们完成了获取,我们的 Git 与他们的 Git.
断开了连接(如果我们想发送他们承诺我们有那个不要,我们用git push
。这几乎是 git fetch
的镜像,有一个非常大的例外:他们没有任何 us 的 remote-tracking 名字。在我们向他们发送新提交后,我们要求他们创建或设置他们的 分支 名称之一。但我们将在这里跳过所有这些。)
8这有点夸大其词:我们复制了 reachable 提交,我们可以有意限制其中的数量抄也。但是默认的是复制所有可达的commit,大家一般不会担心nominally-removed、still-findable-by-reflog的commit,所以说“all commits”是个好主意怎么想,只要记得有脚注就可以了。
9在原始的Git中,你确实每次都必须输入URL。这很漂亮 error-prone 并且 Git 的人发明了一堆不同的 hack 来绕过它。最后,真正卡住的是 远程 、origin
.
结论
git branch
命令是user-facing(或porcelain)命令,它遍历分支名称,或者看起来像分支名称的东西,例如remote-tracking名字。它还允许我们创建和删除分支名称,尽管这不是我们在这里关心的。
使用--merged
或--no-merged
,我们可以在我们的存储库中挑选出一个提交,并询问哪些名称-分支and/or remote-tracking 名称——在我们的存储库中指向特定的提交,这些提交要么 不领先于 (--merged
) 要么 领先于 (--no-merged
) 我们挑选的一个提交。由于提交图的性质和分支名称的工作方式,这通常会让我们在这里得到我们想要的。
(请注意,我们上面没有涉及的 so-called squash 合并 根本不是合并,因此如果有人已经使用壁球合并。)
除了
git branch -r | xargs -t -n 1 git branch -r --contains
我想出了一个包含 git branch --merged
的 PowerShell 片段,可以集成到基于豪华的环境中:
[CmdletBinding()]
param (
# path to local git repository
[Parameter(Mandatory)]
[string]
$Path,
# the branches (regex) in this list will be ignored
[Parameter(Mandatory = $False)]
[string[]]
$IgnoreBranches = @('main', 'master', 'dev', 'develop', '^.+_Maintenance$')
)
$ErrorActionPreference = 'Stop'
function Exec {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[scriptblock]
$ScriptBlock
)
$LASTEXITCODE = 0
try {
& $ScriptBlock
$theEc = $LASTEXITCODE
}
finally {
if ($theEc -ne 0) {
Write-Error "expected 0 exit code, got $theEc"
Write-Error $ScriptBlock.ToString()
throw "command exited with $theEc"
}
}
}
Push-Location $Path
try {
# ensure we're not analyzing a shallow checkout, prune branches deleted on remote
Exec { git fetch --prune }
# get list of all branches that exist on remote
$remoteBranches = (Exec { git branch -r }).Trim()
# map used to store wich branches are fully contained in other branches (b -> fully contained in (a,c,d))
# string -> string[]
$fullyContainedBranches = @{}
# foreach branch, figure out what other branches are "fully contained" -> candidates for deletion
foreach ($b in $remoteBranches) {
$b = $b.Split(' ')[0]
Write-Verbose " checking fully integrated branches of '$b'"
$fullyContained = Exec { git branch -r --merged $b }
if (-not $fullyContained) {
Write-Verbose " '$b' is already gone for good!"
$fullyContainedBranches[$b] = @()
continue
}
foreach ($f in $fullyContained) {
$f = $f.Replace('* ', '').Trim()
Write-Verbose " -> '$f' is in '$b'"
if ($f -eq $b) {
continue
}
if (-not $fullyContainedBranches[$f]) {
$fullyContainedBranches[$f] = @()
}
$fullyContainedBranches[$f] += $b
}
}
$fullyContainedBranches.Keys | Foreach-Object {
$branchName = $_
if ($IgnoreBranches) {
if (($IgnoreBranches | ForEach-Object { $branchName -match $_ }) -contains $true) {
Write-Verbose "ignoring $branchName"
return
}
}
# put output objects to pipeline
@{
branch = $branchName
contained_in = $fullyContainedBranches[$branchName]
}
}
}
finally {
Pop-Location
}