事实是,如果您了解它的实际用途,就会git rebase发现它是一个非常优雅且简单的工具,可以在 Git 中实现许多不同的事情。
在之前的文章中,您了解了Git 差异是什么、合并是什么以及Git 如何解决合并冲突。在这篇文章中,您将了解 Git rebase 是什么,为什么它与 merge 不同,以及如何自信地进行 rebase ??
(更|多优质内|容:java567 点 c0m)
开始之前的注意事项
-
我还制作了一个涵盖这篇文章内容的视频。如果您想在阅读的同时观看,可以在这里找到。
-
如果您想使用我使用的存储库并亲自尝试命令,您可以在此处获取存储库。
-
我正在写一本关于 Git 的书!您有兴趣阅读初始版本并提供反馈吗?
好的,你准备好了吗?
简短回顾 - 什么是 Git Merge??
在幕后,git rebase和git merge是非常非常不同的东西。那么为什么人们总是比较他们呢?
原因在于它们的用途。使用 Git 时,我们通常在不同的分支中工作并对这些分支引入更改。
在之前的教程中,我举了一个例子,约翰和保罗(披头士乐队的成员)正在共同创作一首新歌。他们从main分支开始,然后各自发散,修改歌词并提交自己的更改。
然后,两人想要集成他们的更改,这是使用 Git 时经常发生的事情。
一段不同的历史——paul_branch并且john_branch背离了main(来源:Brief)
有两种主要方法可以集成 Git 中不同分支中引入的更改,或者换句话说,不同的提交和提交历史记录。这些是合并和变基。
在之前的教程中,我们已经git merge非常了解了。我们看到,在执行合并时,我们创建一个合并提交- 该提交的内容是两个分支的组合,并且它还有两个父级,每个分支一个。
因此,假设您位于分支上john_branch(假设上图中描述的历史记录),然后运行git merge paul_branch. 您将进入这种状态 – 在 上john_branch,有两个父母的新提交。第一个是执行合并之前指向的john_branch分支上的提交,在本例中为“Commit 6”。HEAD第二个是paul_branch“Commit 9”指向的提交。
运行结果git merge paul_branch:有两个父级的新合并提交(来源:Brief)
再看看历史图表:您创建了一个分歧的历史。您实际上可以看到它在哪里分支以及在哪里再次合并。
因此,在使用时git merge,您不会重写历史记录 - 而是向现有历史记录添加提交。具体来说,是创建分歧历史记录的提交。
与 有何git rebase不同git merge??
使用时git rebase,会发生不同的情况。?
让我们从大局开始:如果您在 上paul_branch,并使用git rebase john_branch,Git 将转到 John 分支和 Paul 分支的共同祖先。然后,它采用 Paul 分支上的提交中引入的补丁,并将这些更改应用到 John 的分支。
因此,在这里,您通常rebase会获取在一个分支(Paul 的分支)上提交的更改,然后在另一个分支上重播它们john_branch。
运行结果:上面git rebase john_branch的提交被“重放” (来源:Brief)paul_branch``john_branch
等等,这是什么意思??
我们现在将一点一点地了解这一点,以确保您完全了解幕后发生的事情?
cherry-pick作为 Rebase 的基础
将变基视为执行是有用的git cherry-pick- 一个命令接受一次提交,通过计算父级提交和提交本身之间的差异来计算此提交引入的补丁,然后cherry-pick“重放”此差异。
让我们手动执行此操作。
如果我们通过执行以下命令来查看“Commit 5”引入的差异git diff main <SHA_OF_COMMIT_5>:
运行git diff观察“Commit 5”引入的补丁(来源:Brief)
(如果您想使用我使用的存储库并亲自尝试命令,您可以在此处获取存储库)。
您可以看到,在此提交中,约翰开始创作一首名为“Lucy in the Sky with Diamonds”的歌曲:
git diff“Commit 5”引入的补丁的输出(来源: Brief)
提醒一下,您还可以使用以下命令git show获得相同的输出:
git show <SHA_OF_COMMIT_5>
现在,如果您cherry-pick进行此提交,您将在活动分支上专门引入此更改。切换到main第一个:
git checkout main(或git switch main)
并创建另一个分支,只是为了清楚起见:
git checkout -b my_branch(或git switch -c my_branch)
创建my_branch分支main(来源:Brief)
而cherry-pick这个提交:
git cherry-pick <SHA_OF_COMMIT_5>
用于cherry-pick将“Commit 5”中引入的更改应用到main(来源:Brief)
考虑日志( 的输出git lol):
的输出git lol(来源:Brief)
(git lol是我添加到 Git 中的别名,以便以图形方式直观地查看历史记录。您可以在此处找到它)。
看来您复制粘贴了“Commit 5”。请记住,即使它具有相同的提交消息,并引入相同的更改,甚至在本例中指向与原始“Commit 5”相同的树对象 - 它仍然是一个不同的提交对象,因为它是使用不同的时间戳。
查看更改,使用git show HEAD:
的输出git show HEAD(来源:Brief)
它们与“Commit 5”相同。
当然,如果您查看该文件(例如,通过使用nano lucy_in_the_sky_with_diamonds.md),它将处于与原始“Commit 5”之后相同的状态。
凉爽的!?
好的,您现在可以删除新分支,这样它就不会每次都出现在您的历史记录中:
git checkout main
git branch -D my_branch
Beyond cherry-pick– 如何使用git rebase
您可以将git rebase其视为一种依次执行多个cherry-pick操作的方法,即“重播”多个提交。这不是您可以做的唯一事情rebase,但它是我们解释的一个很好的起点。
是时候一起玩了git rebase!????
之前,你paul_branch并入john_branch. 如果您基于重新建立 paul_branch基础, 会发生什么john_branch?你会得到一段非常不同的历史。
从本质上讲,我们似乎采用了 上的提交中引入的更改paul_branch,并在 上重播了它们john_branch。结果将是一个线性历史。
为了理解这个过程,我将提供高层次的视图,然后深入研究每个步骤。将一个分支变基到另一分支之上的过程如下:
-
寻找共同祖先。
-
确定要“重播”的提交。
-
对于每次提交X,计算diff(parent(X), X)并将其存储为patch(X).
-
迁往HEAD新基地。
-
将生成的补丁按顺序应用到目标分支上。每次,创建一个具有新状态的新提交对象。
使用与现有变更集相同的变更集进行新提交的过程也称为“重放”这些提交,这是我们已经使用过的术语。
是时候实践 Rebase 了??
从Paul的分支开始:
git checkout paul_branch
这是历史:
执行前提交历史记录git rebase(来源:Brief)
现在,到了令人兴奋的部分:
git rebase john_branch
并观察历史:
rebase后的历史(来源:Brief)
(是我在视频中gg介绍的外部工具的别名)。
因此,随着git merge你被添加到历史中,随着git rebase你重写历史。您创建新的提交对象。此外,结果是线性历史图,而不是发散图。
rebase后的历史(来源:Brief)
本质上,我们“复制”了“Commit 4”之后引入的提交paul_branch,并将它们“粘贴”到john_branch.
该命令称为“rebase”,因为它更改了运行它的分支的基本提交。也就是说,在您的情况下,在运行之前git rebase, 的基础paul_branch是“Commit 4” - 因为这是分支“诞生”的地方(来自main)。通过rebase,您要求 Git 给它另一个基础 - 也就是说,假装它是从“Commit 6”诞生的。
为此,Git 采用了以前的“Commit 7”,并将此提交中引入的更改“重播”到“Commit 6”上,然后创建了一个新的提交对象。这个对象与原来的“Commit 7”有三个方面的不同:
-
它有不同的时间戳。
-
它有一个不同的父提交 - “Commit 6”而不是“Commit 4”。
-
它指向的树对象不同 - 因为更改是引入到“Commit 6”指向的树,而不是“Commit 4”指向的树。
请注意此处的最后一次提交“Commit 9'”。它所代表的快照(即它指向的树)与通过合并两个分支得到的树完全相同。Git 存储库中文件的状态与您使用git merge. 只是历史不同,当然还有提交对象。
现在,您可以简单地使用:
git checkout main
git merge paul_branch
嗯...如果您运行最后一条命令会发生什么?? 检查后再次考虑提交历史记录main:
变基和检出后的历史main(来源:Brief)
main合并和意味着什么paul_branch?
事实上,Git 可以简单地执行快进合并,因为历史记录是完全线性的(如果您需要有关快进合并的提醒,请查看这篇文章)。结果,main现在paul_branch指向相同的提交:
快进合并的结果(来源:Brief)
Git 中的高级变基??
现在您已经了解了 rebase 的基础知识,是时候考虑更高级的情况了,在这些情况下,命令的附加开关和参数rebase将派上用场。
在前面的示例中,当您仅表示rebase(没有附加开关)时,Git 会重播从共同祖先到当前分支尖端的所有提交。
但 rebase 是一种超级力量,它是一个全能的命令,能够……嗯,重写历史。如果您想修改历史记录以使其成为您自己的历史记录,它会派上用场。
通过再次指向“Commit 4”来撤消上次合并main:
git reset -–hard <ORIGINAL_COMMIT 4>
“撤消”最后一次合并操作(来源:Brief)
并使用以下命令撤消变基:
git checkout paul_branch
git reset -–hard <ORIGINAL_COMMIT 9>
“撤消”变基操作(来源:Brief)
请注意,您获得的历史记录与以前的历史记录完全相同:
可视化“撤消”变基操作后的历史记录(来源:Brief)
再次需要明确的是,当无法从当前 .commit 9 访问时,“Commit 9”并不会消失HEAD。相反,它仍然存储在对象数据库中。当您git reset现在更改HEAD为指向此提交时,您能够检索它及其父提交,因为它们也存储在数据库中。很酷吧??
好的,快速查看 Paul 引入的更改:
git show HEAD
git show HEAD显示“Commit 9”引入的补丁(来源:Brief)
在提交图中继续向后移动:
git show HEAD~
git show HEAD~(同git show HEAD~1)显示“Commit 8”引入的补丁(来源:Brief)
并进一步承诺:
git show HEAD~2
git show HEAD~2显示“Commit 7”引入的补丁(来源:Brief)
所以,这些改变很好,但也许保罗不想要这样的历史。相反,他希望看起来好像他将“Commit 7”和“Commit 8”中的更改作为单个提交引入。
为此,您可以使用交互式变基。为此,我们将-i(或--interactive) 开关添加到rebase命令中:
git rebase -i <SHA_OF_COMMIT_4>
或者,由于main指向“Commit 4”,我们可以简单地运行:
git rebase -i main
通过运行此命令,您可以告诉 Git 使用新的基础“Commit 4”。因此,您要求 Git 返回“Commit 4”之后引入的所有提交,并且可以从 current 访问这些提交HEAD,并重播这些提交。
对于重播的每个提交,Git 都会询问我们想用它做什么:
git rebase -i main提示您选择每次提交要执行的操作(来源:Brief)
在这种情况下,将提交视为补丁是很有用的。也就是说,“Commit 7”如““Commit 7”在其父级之上引入的补丁”一样。
一种选择是使用pick. 这是默认行为,它告诉 Git 重放此提交中引入的更改。在这种情况下,如果您保持原样 - 以及pick所有提交 - 您将获得相同的历史记录,并且 Git 甚至不会创建新的提交对象。
另一种选择是squash。压缩的提交会将其内容“折叠”到其前面的提交的内容中。因此,在我们的例子中,Paul 希望将“Commit 8”压缩为“Commit 7”:
将“Commit 8”压缩为“Commit 7”(来源:Brief)
如您所见,git rebase -i提供了其他选项,但我们不会在本文中讨论所有选项。如果您允许运行变基,系统将提示您为新创建的提交选择一条提交消息(即引入“Commit 7”和“Commit 8”更改的消息):
提供提交消息:(Commits 7+8来源:Brief)
看看历史:
交互式rebase之后的历史(来源:Brief)
正是我们想要的!我们有paul_branch“Commit 9”(当然,它是与原始“Commit 9”不同的对象)。这里指向“Commits 7+8”,这是一个单一的提交,同时引入了原始“Commit 7”和原始“Commit 8”的更改。该提交的父级是“Commit 4”,main指向哪里。你有john_branch。
交互式变基后的历史 - 可视化(来源:Brief)
哦,哇,这不是很酷吗??
git rebase让您可以无限制地控制任何分支的形状。您可以使用它来重新排序提交,或删除不正确的更改,或回顾修改更改。或者,您也许可以将分支的基础移动到另一个提交(您希望的任何提交)。
如何使用--onto开关git rebase
让我们再考虑一个例子。再次进入main:
git checkout main
并删除指向的指针paul_branch,john_branch这样您就不会再在提交图中看到它们:
git branch -D paul_branch
git branch -D john_branch
现在分支main到一个新分支:
git checkout -b new_branch
创造new_branch不同于main(来源:Brief)
new_branch与此不同的干净历史main(来源:Brief)
现在,在此处添加一些更改并提交它们:
nano code.py
new_branch添加该功能code.py(来源:Brief)
git add code.py
git commit -m "Commit 10"
回到main:
git checkout main
并引入另一个变化:
在文件开头添加了文档字符串(来源:Brief)
是时候准备并提交这些更改了:
git add code.py
git commit -m "Commit 11"
还有另一个变化:
添加@Author到文档字符串(来源:Brief)
也提交此更改:
git add code.py
git commit -m "Commit 12"
哦等等,现在我意识到我希望您将“Commit 11”中引入的更改作为new_branch. 啊。你能做什么??
回顾一下历史:
引入“Commit 12”后的历史(来源:Brief)
我想要的是,我不希望“Commit 10”仅驻留在分支上main,而是希望它同时位于main分支和new_branch. 从视觉上看,我想将它移到图表中:
从视觉上看,我希望你“推动”“Commit 10”(来源:Brief)
你能看到我要去哪里吗??
嗯,正如我们所理解的,rebase 允许我们基本上重放在“Commit 10”中引入的更改new_branch,就好像它们最初是在“Commit 11”而不是“Commit 4”上进行的。
为此,您可以使用 的其他参数git rebase。您会告诉 Git,您想要获取main和的共同祖先new_branch(即“Commit 4”)之间引入的所有历史记录,并将该历史记录的新基础设为“Commit 11”。为此,请使用:
git rebase -–onto <SHA_OF_COMMIT_11> main new_branch
rebase前后的历史,“Commit 10”已被“推送”(来源:Brief)
看看我们美丽的历史!?
rebase前后的历史,“Commit 10”已被“推送”(来源:Brief)
让我们考虑另一个案例。
假设我开始在一个分支上工作,并且错误地从 开始工作feature_branch_1,而不是从 开始工作main。
因此,要模拟这一点,请创建feature_branch_1:
git checkout main
git checkout -b feature_branch_1
并擦除,new_branch这样您就不会再在图表中看到它:
git branch -D new_branch
创建一个简单的 Python 文件,名为1.py:
一个新文件,1.py,包含print('Hello world!')(来源:Brief)
暂存并提交此文件:
git add 1.py
git commit -m "Commit 13"
现在(错误地)从以下分支出来feature_branch_1:
git checkout -b feature_branch_2
并创建另一个文件2.py:
创建2.py(来源:Brief)
也暂存并提交该文件:
git add 2.py
git commit -m "Commit 14"
并引入更多代码2.py:
修改2.py(来源:Brief)
也暂存并提交这些更改:
git add 2.py
git commit -m "Commit 15"
到目前为止你应该有这样的历史:
引入“Commit 15”后的历史(来源:Brief)
返回feature_branch_1并编辑1.py:
git checkout feature_branch_1
修改1.py(来源:Brief)
现在暂存并提交:
git add 1.py
git commit -m "Commit 16"
您的历史记录应该如下所示:
引入“Commit 16”后的历史(来源:Brief)
说现在你意识到你犯了一个错误。你实际上想feature_branch_2从树枝中诞生main,而不是从……中诞生feature_branch_1。
你怎样才能做到这一点??
--onto尝试根据历史图以及您对命令标志的了解来思考它rebase。
好吧,您想要将 上的第一个提交feature_branch_2(即“Commit 14”)的父级“替换”到main分支顶部(在本例中为“Commit 12”),而不是在分支顶部feature_branch_1(在本例中为“”)提交 13 英寸。同样,您将创建一个新的基础,这次 是为了第一次提交feature_branch_2.
您想要移动“Commit 14”和“Commit 15”(来源:Brief)
你会怎么做?
首先,切换到feature_branch_2:
git checkout feature_branch_2
现在您可以使用:
git rebase -–onto main <SHA_OF_COMMIT_13>
因此,您feature_branch_2基于main而不是feature_branch_1:
执行rebase后的提交历史(来源:Brief)
该命令的语法是:
git rebase --onto <new_parent> <old_parent>
如何在单个分支上变基
git rebase您还可以在查看单个分支的历史记录时使用。
让我们看看你是否可以在这里帮助我。
假设我工作feature_branch_2并专门编辑了该文件code.py。我首先将所有字符串更改为用双引号而不是单引号括起来:
更改'为"in code.py(来源:Brief)
然后,我上演并承诺:
git add code.py
git commit -m "Commit 17"
然后我决定在文件的开头添加一个新函数:
添加功能another_feature(来源:Brief)
我再次上演并承诺:
git add code.py
git commit -m "Commit 18"
现在我意识到我实际上忘记将单引号更改为双引号main(正如您可能已经注意到的那样),所以我也这样做了:
改成(来源'main':简报)"main"
当然,我策划并承诺了这一改变:
git add code.py
git commit -m "Commit 19"
现在,回顾一下历史:
引入“Commit 19”后的提交历史(来源:Brief)
这不太好,不是吗?我的意思是,我有两个彼此相关的提交,“Commit 17”和“Commit 19”(将's 变成"s),但它们被不相关的“Commit 18”(我在其中添加了一个新函数)分开。我们可以做什么?? 你能帮我吗?
直觉上,我想在这里编辑历史记录:
这些是我要编辑的提交(来源:Brief)
那么,你会怎么做?
你是对的!??
我可以在“Commit 15”之上将历史记录从“Commit 17”重新设置为“Commit 19”。要做到这一点:
git rebase --interactive --onto <SHA_OF_COMMIT_15> <SHA_OF_COMMIT_15>
请注意,我指定“Commit 15”作为提交范围的开头,不包括此提交。而且我不需要明确指定HEAD为最后一个参数。
rebase --onto在单个分支上使用(来源: Brief)
按照您的建议并运行rebase命令后(谢谢!?)我得到以下屏幕:
交互式变基(来源:Brief)
那么我该怎么办呢?我想将“Commit 19”放在“Commit 18”之前,因此它位于“Commit 17”之后。我可以更进一步,将它们压在一起,如下所示:
交互式变基 - 更改提交和压缩的顺序(来源:Brief)
现在,当我收到提交消息提示时,我可以提供消息“Commit 17+19”:
提供提交消息(来源:Brief)
现在,看看我们美丽的历史:
由此产生的历史(来源:Brief)
再次感谢!??
更多变基用例+更多实践
到目前为止,我希望您对 rebase 的语法感到满意。真正理解它的最好方法是考虑各种情况并自己找出解决方法。
对于即将到来的用例,我强烈建议您在介绍完每个用例后停止阅读,然后尝试自己解决它。
如何排除提交
假设您在另一个存储库上有此历史记录:
另一个提交历史(来源:Brief)
在使用它之前,将标签存储到“Commit F”,以便稍后可以返回:
git tag original_commit_f
现在,您实际上不希望包含“Commit C”和“Commit D”中的更改。您可以像以前一样使用交互式变基并删除它们的更改。或者,可以再次使用git rebase -–onto。您将如何使用--onto来“删除”这两个提交?
您可以HEAD在“Commit B”之上进行变基,其中旧的父级实际上是“Commit D”,现在它应该是“Commit B”。再回顾一下历史:
再次回顾历史(来源:Brief)
变基使“Commit B”成为“Commit E”的基础,意味着“移动”“Commit E”和“Commit F”,并给它们另一个基础—— “ Commit B”。你能自己想出这个命令吗?
git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D> HEAD
请注意,使用上面的语法不会移动main到指向新的提交,因此结果是“分离” HEAD。如果您使用gg或其他显示可从分支访问的历史记录的工具,它可能会让您感到困惑:
变基并--onto产生分离结果HEAD(来源:Brief)
但如果您只是使用git log(或我的别名git lol),您将看到所需的历史记录:
由此产生的历史(来源:Brief)
我不了解你,但这些事情让我真的很高兴。??
顺便说一句,您可以省略HEAD上一个命令,因为这是第三个参数的默认值。所以只需使用:
git rebase --onto <SHA_OF_COMMIT_B> <SHA_OF_COMMIT_D>
会有同样的效果。最后一个参数实际上告诉 Git 当前的 rebase 提交序列的结尾在哪里。git rebase --onto所以带有三个参数的语法是:
git rebase --onto <new_parent> <old_parent> <until>
如何跨分支移动提交
假设我们得到了与之前相同的历史记录:
git checkout original_commit_f
现在我只想要“提交 E”位于基于“提交 B”的分支上。也就是说,我想要一个新分支,从“Commit B”分支,只有“Commit E”。
当前的历史,考虑“Commit E”(来源:Brief)
那么,这对于 rebase 来说意味着什么呢?考虑上面的图片。我应该重新设定哪些提交(或哪些提交),以及哪个提交将成为新的基础?
我知道我可以在这里依靠你?
我想要的是仅采用“Commit E”,并且仅此提交,并将其基础更改为“Commit B”。换句话说,将“提交 E”中引入的更改重播到“提交 B”上。
你能将该逻辑应用到 的语法中吗git rebase?
这是(为了简洁,这次我写<COMMIT_B>的是<SHA_OF_COMMIT_B>):
git rebase –-onto <COMMIT_B> <COMMIT_D> <COMMIT_E>
现在历史看起来是这样的:
rebase后的历史(来源:Brief)
惊人的!
关于冲突的注意事项
请注意,执行变基时,您可能会像合并时一样遇到冲突。您可能会遇到冲突,因为在变基时,您试图在不同的基础上应用补丁,也许补丁不适用。
例如,再次考虑以前的存储库,具体来说,考虑“Commit 12”中引入的更改,由 指向main:
git show main
“Commit 12”中引入的补丁(来源:Brief)
我已经在上一篇文章git diff中详细介绍了 的格式,但作为一个快速提醒,此提交指示 Git 在两行上下文之后添加一行:
```
This is a sample file
在这三行上下文之前:
```
def new_feature():
print('new feature')
假设您正在尝试将“Commit 12”重新设置为另一个提交。如果由于某种原因,这些上下文行并不像您要变基到的提交的补丁中那样存在,那么您将遇到冲突。要了解有关冲突以及如何解决冲突的更多信息,请参阅本指南。
缩小大局
比较变基和合并(来源:Brief)
git merge在本指南的开头,我首先提到和 之间的相似之处git rebase:两者都用于整合不同历史中引入的变化。
但是,正如您现在所知,它们的运作方式非常不同。合并会产生发散的历史记录,而变基则会产生线性历史记录。这两种情况都可能发生冲突。上表中还有一列需要密切关注。
现在您知道什么是“Git rebase”,以及如何使用交互式 rebase,或者rebase --onto,正如我希望您同意的那样,git rebase它是一个超级强大的工具。然而,与合并相比,它有一个巨大的缺点。
Git rebase 改变了历史。
这意味着您不应该对存储库本地副本之外存在的提交进行变基,而其他人可能已经基于这些提交进行了提交。
换句话说,如果唯一有问题的提交是您在本地创建的提交 - 继续,使用 rebase,尽情发挥。
但是,如果提交已被推送,这可能会导致一个巨大的问题 - 因为其他人可能会依赖这些提交,而您稍后会覆盖这些提交,然后您和他们将拥有不同版本的存储库。
正如我们所看到的,这与merge不修改历史不同。
例如,考虑我们重新设置基础并导致此历史记录的最后一个案例:
rebase后的历史(来源:Brief)
现在,假设我已经将此分支推送到远程。在我推送分支后,另一位开发人员将其拉出并从“Commit C”分支出来。另一位开发人员不知道与此同时,我正在本地重新调整我的分支,并稍后再次推送它。
这会导致不一致:其他开发人员所使用的提交在我的存储库副本上不再可用。
我不会在本指南中详细说明这到底是什么原因,因为我的主要信息是您绝对应该避免这种情况。如果您对实际发生的情况感兴趣,我将在下面留下一个有用资源的链接。现在,让我们总结一下我们所涵盖的内容。
回顾
在本教程中,您了解了git rebase,一个重写 Git 历史记录的超级强大工具。您考虑了一些git rebase有用的用例,以及如何使用一个、两个或三个参数(带或不带开关)来使用它--onto。
我希望我能够让您相信这git rebase很强大,而且一旦您掌握了要点,它就非常简单。它是一个“复制粘贴”提交(或更准确地说,补丁)的工具。它是一个值得拥有的有用工具。