现在基本每一个开发人员和很多的非开发人员都用Git。All with Git成了一种时尚。但是对很多人来说Git却是又爱怕,而且一不小心尝试一个新功能却把仓库给弄坏了,还需要求教角落的怪大叔扫地僧出马才能解决。
有什么兼容Git但是使用又很简单的替代呢?
有,这就是Jujutsu(jj),一个零成本兼容VCS的既简单又强大的VCS。一个由谷歌软件工程师用Rust开发的新版本控制系统,它有望取代谷歌现有的版本控制系统(历史上是:Perforce、Piper和Mercurial)。
概述Jujutsu借鉴了所有想取代Git的前辈们的想法:借鉴了Mercurial中修订与修订集来取代Git中的commit,借鉴了Pijul和Darcs的冲突等。Jujutsu目前的核心思想是:
作为Git的新前端。这是到目前为止,这两件事中不太有趣的一个,但实际上,它是当今使用该工具的体验的重要组成部分。与gitix之类的东西处于相同的概念空间。Jujutsu的jj对于日常工作来说比gitix更有gix和ein但到目前为止,它的目标也截然不同。这需要我们:
分布式版本控制的新设计。特别是,Jujutsu借鉴了前面提到的一些关键概念——这些概念本身并不新颖,但它们的组合非常好,在实践中使用起来。
更加友好的用户交互界面。用户界面不仅合理,而且实际上非常好,这个想法借鉴了……实际上除Git之外的所有VCS。
另外背靠大树好乘凉。鉴于谷歌正在积极开发它,以替代其当前的自定义VCS设置,似乎拥有美好的发展前景和未来。
安装Jujutsu安装过程非常轻松。与大多数现代Rust支持的CLI工具一样,Jujutsu开箱即用,具有出色的完成度,只需运行
brew install jj
就可以自动做好一起。
安装后,如果需要在和Git一起协同工作需要调整一下~/.gitignore_global忽略.jj目录(Jujutsu使用的仓库数据库目录)。
在现有的Git项目中使用Jujutsu也非常容易。只需要通过
jj git init --git-repo <path to repo>。
就可以完成所有设置。
之后就可以和git类似的命令一样使用jj存储库上的命令相似,一切都都OK,也支持在不用git存储的情况下初始化Git项目的Jujutsu副本,使用jj git clone即可。
项目初始化后,处理它就相当简单了,但如果有根深蒂固的Git习惯,则需要进行一些重大调整。
变更和提交在git中,默认的工作单元是“commit(提交)”。在jj中,这是一个“Change(变更)”。在实践中,两者是可以互换的,差异只是两者视角不同。
commit是提交到git日志的一个工作单元。做到了这一点,你就提交了。 如果该工作单元只是一部分工作,那么必须在上面在进行commit来提交其他的工作进度。唯一的选择是是否要在rebase的基础上squash。
另一方面,变更只是一个工作单元。 如果愿意,可以认为它是一个提交。但不同之处在于可以随时返回并编辑它。随时。当你完成后,jj自动将所有后续更改重新设置为基础。这太棒了,让感觉自己像一个时间旅行者。
举一个日常工作中的真实例子。假设目前正在进行一次巨大的重构,其中涉及对代码当前的功能进行逆向工程,为该操作创建一个通用接口,将内联代码分解为该接口的实例,然后根据该接口重写原始调用点。经过一天工作后,jj log看起来像这样:
@ ee
│ Rewrite first callsite
◉ dd
│ Give vector implementation
◉ cc
│ Give image implementation
◉ bb
│ Add interface for FileIO
◉ aa
│ (empty) ∅
~
这是jj的版本git log。在左侧,看到一个(线性)ascii 更改树,最新的位于顶部。当前的更改,标记为@有ID ee和描述 Rewrite first callsite。现在准备添加新的更改,可以通过 jj new -m 'Rewrite second callsite':
@ ff
│ Rewrite second callsite
◉ ee
│ Rewrite first callsite
◉ dd
│ Give vector implementation
◉ cc
│ Give image implementation
◉ bb
│ Add interface for FileIO
◉ aa
│ (empty) ∅
~
然后我继续接下来的其他coding。然后,突然出现了一个问题。在继续开发时,刚开始的FileIO 抽象实际上对send callsite没有用,弄错了接口。
在git中,这样的情况很困难。是否只是添加一个新的commit,修改接口,并希望同事不会注意到或者你做一个rebase?或者是否完全放弃该分支,并希望可以挑选中间提交。
在jj中,可以去修复bb Add interface for FileIO 改变通过jj edit bb:
@ ff
│ Rewrite second callsite
◉ ee
│ Rewrite first callsite
◉ dd
│ Give vector implementation
◉ cc
│ Give image implementation
◉ bb
│ Add interface for FileIO
◉ aa
│ (empty) ∅
~
然后跳回之前更新接口(jj edit ff)完成工作。老实说,时间旅行者的东西。
当然,有时这样做会导致冲突,但是jj会保留冲突标记。与git相比影响要小得多。
PR堆积jj中分支的作用大大减弱。更改不需要与任何分支相关联,通常实在相当于git detached head指针状态下工作。可能你在git吃过这厮的亏,但是在jj中这并不是什么大问题。
因为更改不需要与分支相关联,所以这允许git可能认为“不自然”或至少难以操作的工作流程。例如,通常会做一堆工作(一边重写历史一边做),并在事后弄清楚如何将其拆分为PR。一旦距离明显的停止点有大约十个更改,就需要会返回,将其中一个更改标记为分支的头部jj branch create -r ff feat-xxx,然后继续工作。
这标志着改变ff引用指向feat-xxx分支的头,但此操作对于其他方面没有任何影响,变更树没有丝毫改变。
@ ii
| Update ObjectName
◉ hh
| Changes to pubsub
◉ gg
| Fix shape policy
◉ ff feat-xxx
│ Rewrite second callsite
◉ ee
│ Rewrite first callsite
◉ dd
│ Give vector implementation
◉ cc
│ Give image implementation
◉ bb
│ Add interface for FileIO
◉ aa
│ (empty) ∅
~
唯一的区别是线◉ ff feat-xxx。现在,当jj将其发送到git分支feat-xxx将为每个更改进行一次提交aa..ff。如果同事在代码审查期间要求更改,只需将更改添加到更改树中的某个位置,它就会自动向下游传播到下一个PR中的更改。无需在做cherry-picking操作。不再有分支间合并提交。使用相同的工作流程jj如果目前无正在进行的 PR,就可以这样做。
开发分支Dev分支模式的使用和滥用,为特定的git工作流程提供了一个很好的论据,在该工作流程中,所有分支都基于本地dev分支。通过dev分支,可以进行与本地开发人员体验相关的任何更改,在其中更改默认配置选项,或添加额外的日志记录,等等。这个想法是,希望将所有私人更改组织起来,但不必担心这些更改意外地出现在PR中。
在jj存储库这个dev分支可以更加丝滑。如下所示:
◉ wq
╷ reactor: Cleanup singleton usage
╷ ◉ pv
╭─╯ feat: Optimize image rendering
╷ ◉ u
╷ | fix: Fix bug in networking code
╷ | ◉ wo
╷ ╭─╯ feat: Finish porting to FileIO
╷ ◉ ff
╭─╯ feat: Add interface for FileIO
@ dev
│ (empty) ∅
◉ main@origin
│ Remove unused actions (#1074)
在这里可以看到,变化树路劲中wq,pv和ff都是直接从dev分支出来的,对应于目前正在等待审核的PR。u和wo正在堆积变化,等待ff合并。jj的ascii树在跟踪所有更改的位置方面非常有价值。
会注意到dev分支被标记为(empty),也就是说这是一个没有差异的更改。但即便如此,这个提示还是非常有帮助的。因为其他人迁移main时候,只需要rebase dev在新main变化之上,剩下的jj自动会做。比如说rr有冲突。则可以去编辑rr修复冲突,并且该修复将传播到u和wo。
对于所有PR请求,只需解决一次冲突。
修订集在jj,变更集是第一类对象,被称为Revset修订集。修订集是通过一种操作集合的小型纯函数语言以代数方式创建的。任何更改的ID都是单个修订集。可以将两个修订集的并集与|,以及与的交集&。可以通过~操作以获取修订集的补充。通过x::可以获得revset x的后代,及其祖先。
可以获得修订集的子级x通过 x+,
jj rebase -s dev+ -d new-dev。
这样的东西有点简洁,修订集的最佳用途是自定义jj以完全适合方式体验。 例如,堆积了很多的PR,想要在jj log出来。所以默认设置为jj log只显示 “当前PR”中的更改。这有点难以解释,但它的工作原理就像手风琴一样。用分支标记PR,修订集只会显示从最直接的祖先分支到最直接的后代分支的更改,并折叠了不属于当前正在查看的PR的任何更改。
但是,跟踪在更大的变更树中的位置很有帮助,这样默认修订集也会显示PR 与所有其他PR的关系。当改变时@位于其中一个PR的内部,它会立即扩展为提供所有本地上下文:
关于修订集UI最酷的部分是,可以通过将它们添加为别名来创建自己的命名修订集
jj/config.toml:
[revsets]log = "@ | bases | branches | curbranch::@ | @::nextbranch | downstream(@, branchesandheads)"[revset-aliases]'bases' = 'dev''downstream(x,y)' = '(x::y) & y''branches' = 'downstream(trunk(), branches()) & mine()''branchesandheads' = 'branches | (heads(trunk()::) & mine())''curbranch' = 'latest(branches::@- & branches)''nextbranch' = 'roots(@:: & branchesandheads)'可以看到从log总是表现出@(当前),所有命名(当前只是dev,但可能想添加 main) 以及所有指定的分支。然后它显示所有内容curbranch到@,也就是说,分支的变化导致@,以及来自的一切@到下一个(堆叠的)分支的开头。最后,显示了下游的变更树的所有叶子节点@,当还没有完成足够的工作来考虑发送PR时,这很好。
总结Jujutsu在众多准备替代Git的VCS中表现惊艳,过去使用过Mercurial的同学则更应该直接替换古来使用,其他人也值得花费一点时间学习并适应一下,可以让jj作为你心目中git来使用。