jj 多远程(Multiple Remotes)实战指南:GitHub 式 Fork 协作与上游集成工作流配置
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
导读:本指南基于 Jujutsu(jj)官方文档 docs/guides/multiple-remotes.md 编写,系统讲解在一个仓库中配置多个 Git 远程(remote)的两种典型工作流——向上游项目贡献代码(GitHub 式 fork)与维护一个独立仓库并周期性地从上游集成变更。你将掌握
git.fetch、git.push的默认远程配置、jj bookmark track/untrack的远程书签跟踪机制,以及如何通过覆盖trunk()revset 别名来正确设定"主干"(immutable 边界),并理解这些配置背后的源码实现原理。
Jujutsu 与 Git 完全兼容,可以对接 GitHub、GitLab、Codeberg 等所有主流 Git 托管平台(参见 glossary.md 中对 Remote 的定义)。当仓库同时存在多个远程时,如何配置取决于你的工作流以及每个远程扮演的角色:是向上游项目提交贡献,还是从另一个仓库集成变更。两种场景下,jj git fetch、jj git push、书签跟踪与trunk()别名的配置策略截然不同。
术语约定(Nomenclature)
在开始配置之前,先统一文档中的命名约定:
origin:你拥有写权限的远程,通常是你推送变更的地方(例如你自己的 fork)。upstream:更知名的权威上游仓库,你可能没有推送权限,只能拉取。- 每个仓库的主干(trunk)假定为
main,因此对应的远程书签是main@origin和main@upstream。
远程书签(remote bookmark)的表示法:Jujutsu 会记录每个书签在远程上的"最后看到的位置"(类似 Git 的 remote-tracking branches),用
<书签名>@<远程名>表示,例如jj new main@origin。该记录在每次jj git fetch/jj git push时更新,且 jj 不提供手动编辑这些记录的方式(详见 docs/bookmarks.md 的 "Remotes and tracked bookmarks" 小节)。
相关源码佐证
trunk()等 revset 别名与配置项的定义见默认配置文件 cli/src/config/revsets.toml。其中内置的trunk()默认值已经涵盖origin和upstream上的常见主干名:
[revset-aliases] 'trunk()' = ''' latest( remote_bookmarks(exact:"main", exact:"origin") | remote_bookmarks(exact:"master", exact:"origin") | remote_bookmarks(exact:"trunk", exact:"origin") | remote_bookmarks(exact:"main", exact:"upstream") | remote_bookmarks(exact:"master", exact:"upstream") | remote_bookmarks(exact:"trunk", exact:"upstream") | root() ) '''注意文件中的注释:"trunk() can be overridden as ' @ '",这正是本指南中两种工作流都要覆盖trunk()别名的原因——用main@origin或main@upstream明确指定哪个远程书签定义了你仓库的主干。
工作流一:GitHub 式 fork 向上游贡献代码
这是最经典的场景:upstream是权威上游仓库,origin是你自己的 fork,用来推送贡献(通常是发起 Pull Request 的入口)。
典型操作
- 从
upstreamfetch 获取最新变更。 - 将
mainpush 到origin以保持 fork 与上游同步。 - 将
my-featurepush 到origin,然后向上游仓库发起 Pull Request。
配置步骤
为此场景,你需要:
- 跟踪
main@upstream:这样每次从upstreamfetch 时,本地main书签都会被更新。 - 跟踪
main@origin:这样执行jj git push时,fork 的main书签会被更新。 - 将
main@upstream设为trunk()别名:使其成为不可变(immutable)主干。
# 默认同时从两个远程 fetch $ jj config set --repo git.fetch '["upstream", "origin"]' # 默认只向 fork push $ jj config set --repo git.push origin # 跟踪两个远程的 main 书签 $ jj bookmark track main # 上游仓库定义主干 $ jj config set --repo 'revset-aliases."trunk()"' main@upstream配置参数深度解析
git.fetch:配置jj git fetch(不带--remote参数时)默认拉取哪些远程。取值可以是一个远程名,也可以是一个字符串数组(一次拉取多个远程)。fetch.rs 源码 中的文档说明:"If no remotes are specified, fetches the remotes specified by thegit.fetchsetting. If that is not configured and there are multiple remotes, the remote named 'origin' will be used.",其解析实现位于 fetch.rs 的resolve_default_fetch_remotes附近,通过settings.get::<Vec<String>>("git.fetch")读取并用parse_union_name_patterns解析。git.push:配置jj git push(不带--remote参数时)默认推送的目标。与git.fetch不同,git.push目前只能是一个单一远程,不能是数组(docs/config.md 中明确说明)。push.rs 源码 显示其回退逻辑:先读git.push设置,未配置时若仓库只有一个远程则使用该远程。- 字符串模式:
git.fetch和git.push的值默认按 glob 语法匹配远程名,也支持其他 string pattern 语法,例如jj config set --repo git.fetch "regex:'^(remote|upstream)'"或jj config set --repo git.fetch '["remote*", "upstream*"]'(详见 docs/config.md)。
书签跟踪命令说明
jj bookmark track main会跟踪所有匹配main的远程书签(即同时跟踪main@origin和main@upstream)。track.rs 源码 显示该命令接受BOOKMARK[@REMOTE]形式的参数:
BOOKMARK默认按 glob 语法匹配书签名。BOOKMARK@REMOTE精确解析为一个远程书签。--remote <REMOTE>选项可以限定只在指定远程上跟踪。
此外,源码还规定了一些使用约束:不能混用<bookmark>模式与<bookmark>@<remote>符号,--remote也不能与<bookmark>@<remote>符号同时使用(track.rs 中的校验逻辑)。
工作流二:维护独立仓库,周期集成上游变更
这是另一种常见场景:仓库最初从上游克隆而来,但现在main分支上含有不属于上游、未来也可能不会回馈上游的本地变更。
角色划分
origin是你正在工作的仓库(拥有写权限)。upstream是你周期性集成变更的来源。
典型操作
- 从
originfetch 获取最新变更。 - 将书签 push 到
origin。 - 将 Pull Request 合并进
main@origin。 - 周期性 fetch
main@upstream,并将其变更 merge、rebase 或 duplicate 进main@origin。
配置步骤
与工作流一的关键区别在于:upstream在这里只是集成来源,不是主干。因此:
- 只跟踪
main@origin:本地main只跟随 origin 更新,必要时可以推送。 - 不要跟踪
main@upstream。 - 将
main@origin设为trunk()别名:使其不可变。
# 默认只从 origin fetch(或按需同时拉取两个远程) $ jj config set --repo git.fetch '["origin"]' # 或者:jj config set --repo git.fetch '["upstream", "origin"]' # 默认只向 origin push $ jj config set --repo git.push origin # 只跟踪 origin 上的书签 $ jj bookmark track main --remote=origin $ jj bookmark untrack main --remote=upstream # origin 仓库定义主干 $ jj config set --repo 'revset-aliases."trunk()"' main@origin关键概念:跟踪与否的区别
正如 docs/bookmarks.md 所解释的:
- 跟踪(track)
main@origin意味着远程书签与本地main表示同一条分支:fetch 时远程的移动会传播到本地main(本地不存在则自动创建),push 时本地main的移动也会同步回远程。如果两者都发生了移动且一方领先,则领先者胜出;否则本地书签会进入冲突状态。 - 不跟踪
main@upstream,意味着上游main的移动只更新main@upstream这个远程记录本身,不会触碰你的本地main,从而保证你可以在main@origin上自由管理本地变更,再自行决定何时、以何种方式(merge/rebase/duplicate)把上游变更合入。
jj bookmark untrack命令同样支持--remote参数,其参数结构与track对称(详见 untrack.rs 源码)。另外注意:如果既要忘记本地书签、又要取消对应远程书签的跟踪,应改用jj bookmark forget。
补充:按远程精细化配置自动跟踪
除了手动jj bookmark track/untrack,你还可以通过remotes.<name>.auto-track-bookmarks配置每个远程自动跟踪哪些书签(docs/config.md 的 "Automatic tracking of bookmarks" 小节)。对于本工作流的 fork 场景尤其有用:
[remotes.origin] auto-track-bookmarks = "*" [remotes.upstream] auto-track-bookmarks = "main"即:fork(origin)上的所有书签自动跟踪,上游(upstream)只自动跟踪main。remotes.<name>.auto-track-created-bookmarks则只对本地新建的书签生效。类似的,remotes.<name>.fetch-bookmarks/fetch-tags可以控制每个远程默认拉取哪些书签和标签(docs/config.md)。
其他工作流与通用原则
其他工作流也可能被支持。以下是一些通用指导:
trunk()应设置为你通常在其上 rebase 的远程书签。如果你总是基于上游 rebase,就把它设为main@upstream;如果你的主干在本地仓库自己手里,就设为main@origin。- 跟踪
main@origin意味着它和main代表同一条分支:一方移动,另一方应随之移动。如果希望它们自动同步移动,就应该跟踪该远程书签;如果不希望,就不要跟踪。
关于trunk()与不可变性的底层原理
覆盖trunk()别名并非仅仅影响jj log的默认显示范围。从 cli/src/config/revsets.toml 可以看到,trunk()是builtin_immutable_heads()的组成部分:
'builtin_immutable_heads()' = ''' trunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags() ''' 'immutable_heads()' = 'builtin_immutable_heads()' 'immutable()' = '::(immutable_heads() | root())' 'mutable()' = '~immutable()'也就是说,trunk()指定的远程书签会被视为不可变主干(immutable heads),jj 的自动 rebase 等重写操作不会触碰它及其祖先。因此:
- 在工作流一中设置
trunk()为main@upstream,确保你基于上游主干开发的提交可以自由重写,而上游main本身保持稳定。 - 在工作流二中设置
trunk()为main@origin,则把你自己的main声明为稳定基线,而upstream的main只是一个普通远程书签,可以随时集成。
(trunk()别名的可覆盖性在 cli/src/config/revsets.toml 的注释中明确说明,用户配置会覆盖内置默认值。)
关于默认远程回退的源码细节
jj git fetch与jj git push在未配置对应设置时,都会回退到 "origin" 或唯一的远程。源码层面的回退逻辑如下:
- fetch.rs:未配置
git.fetch且存在多个远程时,默认使用名为origin的远程。 - push.rs:
git.push未配置时,若存在多个远程,默认使用origin。 - git/mod.rs 的
get_single_remote:当仓库只有一个远程时,无论名称如何,fetch/push 都会默认使用它(names.len() == 1时返回该唯一远程,多个远程时返回None走 "origin" 回退)。
这解释了为什么示例配置中显式设置git.fetch/git.push是必要的:一旦仓库有多个远程,默认行为是仅使用origin,而你的origin与upstream角色不同,必须显式声明 fetch 与 push 的目标。
更多参考
- 书签跟踪机制的完整说明(含冲突处理):docs/bookmarks.md
- 远程相关命令:
jj git remote list/add/remove/rename/set-url(实现见 cli/src/commands/git/remote/) - 与多远程相关的 git 配置项:
git.fetch、git.push、remotes.<name>.*,详见 docs/config.md
如果你的工作流在上述两种场景中未被良好覆盖,欢迎参与项目讨论(官方 Discord 与 GitHub issue #7072 中有关于增强书签跟踪机制的开放讨论)。
【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考