如何使用 blueos 仓库
与独立仓库之间的关系
此前, 项目按组件拆分为 kernel、build、libc、book 等多个独立 Git 仓库。 各仓库的地址、版本和本地目录由 manifests 仓库描述, 开发者通过 repo init 和 repo sync 在本地组成完整的源码工作区。 工作区中的每个组件仍是独立 Git 仓库, 分别维护自己的分支、提交历史和 PR。
但这带来了不便, 如果需要进行多个仓库的修改, 就需要在每个涉及到的仓库中, 都单独推送分支与PR, 这导致了审查与CI构建检查上的不便.
blueos 仓库将这些组件整合在一起, 其组件目录与先前由 repo init 构建的本地工作区一致, 并通过 Josh 保留导入的提交历史。
基本使用原则
- 对于尚未开始的工作, 通常直接从中心仓库的
main创建分支即可. - 对于已经在子仓库中开展的工作, 推荐将分支导入中心仓库, 并整合完整的中心历史后继续开发, 随后重新提交中心仓库分支与PR。
选择开发与 PR 入口
尽可能将同一项工作的开发分支和 PR 统一维护在中心仓库, 减少同时在中心仓库和子仓库维护同一分支。 如果原分支由多人共用, 应先协商迁移方式和后续开发入口。
| 工作情况 | 建议方式 |
|---|---|
| 新工作, 仅依赖各仓库默认分支 | 从完整的中心 main 开始开发。 |
| 已有子仓库分支或未合并 PR | 按本章流程导入, 再作为中心分支和 PR 发布。 |
| 依赖其他尚未合并的分支或 PR | 将当前工作和所需依赖整合到同一个中心分支。 |
| 同时涉及多个子仓库 | 在中心仓库统一开发、验证和提交 PR。 |
仅修改 book 等不依赖其他仓库的内容时, 也可以直接在子仓库提交 PR。 存在未合并依赖时, 应将依赖一起导入和整合; 如果依赖已经是完整的中心分支, 可以直接使用 Git 合并或移植相关提交。 迁移已有 PR 后, 应及时关闭原 PR 并进行说明, 避免两边重复合并。
导入子仓库分支
安装工具
需要 Python 3、Git、josh 和 josh-filter。 以下安装命令会同时安装后两个程序:
cargo +stable install josh-cli --locked \
--git https://github.com/josh-project/josh.git \
--tag r26.07.19
安装时使用普通 Rust 工具链, 避免使用 blueos 定制的 Cargo。
配置远端并获取分支
Josh 远端将子仓库内容放到中心仓库对应目录中。 例如, :prefix=build 将子仓库根目录映射到中心仓库的 build/。
子仓库名称, 即脚本的 --repo | Josh 远端过滤表达式 |
|---|---|
apps_example | :prefix=apps/example |
apps_shell | :prefix=apps/shell |
book | :prefix=book |
build | :prefix=build |
external | :prefix=external |
kernel | :prefix=kernel |
libc | :prefix=libc |
librs | :prefix=librs |
配置所需远端, 已配置过的远端无需重复添加:
完整模板见附录。
josh remote add fork-build \
https://github.com/{{YOUR_USERNAME}}/build.git ':prefix=build'
josh remote add fork-kernel \
https://github.com/{{YOUR_USERNAME}}/kernel.git ':prefix=kernel'
josh fetch --remote fork-build
josh fetch --remote fork-kernel
git --no-pager branch --list --all
这里的远端名 fork-build 和 fork-kernel 是本地自定义名称。 fetch 后可用 fork-build/branch-name 等远端跟踪引用选定导入的分支。 配置上游仓库时使用同样的方式, 只需更换远端名和 URL。
josh fetch 更新远端跟踪引用, 不移动当前本地开发分支, 也不会自动将子仓库分支整合到完整的中心历史。
将导入分支整合到完整 blueos 仓库
导入分支只包含对应子仓库的内容。 需要将它整合到包含所有组件的中心历史上, 再作为完整的中心分支使用并发布中心仓库 PR。
中心仓库的早期历史逐步导入了各个子仓库。 在 vivoblueos/blueos 中, 完成这些初始导入的提交是 a0d6549f6078a624dc513d4d2a34f5cf47e02f13。 通常选择中心 main, 或上一轮已经整合完成的中心分支作为起点。 整合后, 可以检查该导入完成提交是否是结果的祖先:
git merge-base --is-ancestor \
a0d6549f6078a624dc513d4d2a34f5cf47e02f13 \
branch-name
命令退出码为 0 表示祖先关系成立。 这个检查用于确认历史关系, 整合后的文件和构建仍需按修改内容进行验证。
可以先在子仓库实验对分支的 merge 或 rebase 更新。 中心仓库可能包含尚未同步回子仓库的修改, 因此中心整合时仍需处理冲突并验证结果。
使用 rebase 更新导入分支
使用 Git rebase 将导入分支的修改移植到中心仓库的 main 上。 示例假定被 rebase 排除的子仓库 main 基线修改, 已经包含在中心仓库的整合起点中。
# 将 build 的 miri/allocator 分支移植到完整中心历史。
git switch -c import/build-miri-rebased fork-build/miri/allocator
git rebase --onto main fork-build/main
# 该结果现在可以作为完整的中心分支使用。
# 在上一轮结果上叠加 kernel 的业务修改。
git switch -c blueos/miri/allocator-rebased fork-kernel/miri/allocator
git rebase --onto import/build-miri-rebased fork-kernel/main
git push origin blueos/miri/allocator-rebased
使用 merge 更新导入分支
直接把josh导入的分支 merge 到中心 main, 会将导入分支的完整历史一起带入。 已经同步到中心仓库的修改, 在不同转换下可能具有不同 SHA, 因而仍可能作为额外提交出现在分支和 PR 中。
可以使用 merge-subtree-branches.py 脚本来处理这个问题. 每次调用时会使用merge方式更新一个导入分支, 可以依次导入多个分支:
# 以中心仓库 main 为起点导入 build 的 miri/allocator 分支。
python3 tools/josh/merge-subtree-branches.py \
--repo build \
--source-remote fork-build \
--source-branch miri/allocator \
--target-base main \
--target-branch blueos/miri/allocator
python3 tools/josh/merge-subtree-branches.py \
--repo kernel \
--source-remote fork-kernel \
--source-branch miri/allocator \
--target-base blueos/miri/allocator \
--target-branch blueos/miri/allocator
| 参数 | 含义 |
|---|---|
--repo | 子仓库名称, 决定中心目录, 必须与远端的 prefix 对应。 |
--source-remote | 已配置的 Josh 远端名。 |
--source-branch | 子仓库中需要导入的分支名。 |
--target-base | 完整中心历史中的本轮起点, 默认 main。 |
--target-branch | 中心仓库中要创建或追加 merge 的本地分支名。 |
--repo-root | 中心仓库路径, 默认脚本所在仓库。 |
--target-base 支持分支名、标签、完整或唯一缩写 SHA, 以及 HEAD~1 等 Git 提交表达式;两个 branch 参数只接受分支名。 脚本在 merge 前固定起点, 因此 --target-base 可以与目标分支同名。
目标分支不存在时, 脚本从起点创建分支; 已经存在时追加 merge, 起点必须是目标分支的祖先或自身。 脚本保留目标分支中已有的提交, 不会重置分支。
运行前提交已跟踪文件的修改, 或用 git stash 保存。 脚本拒绝已跟踪文件的未提交修改, 以及正在进行的 merge、rebase 等操作。 它在当前工作区切换分支并合并, 成功后停留在目标分支。
将修改推回子仓库分支
如果后续只在中心仓库维护分支, 可以跳过这一节。 需要继续维护子仓库业务分支时, 使用 push-subtree-branch.py 导出对应目录。
# 选择 --source-base(排除) 和 --source-rev(包含) 之间的提交。
#
# 添加 --push 才会实际推送, 非快进更新需要 --force。
python3 tools/josh/push-subtree-branch.py \
--source-base main \
--source-rev blueos/miri/allocator \
--repo build \
--target-remote fork-build \
--target-base main \
--target-branch miri/allocator \
--force
选择导出范围和子仓库基线
用两个中心提交参数指定范围:
--source-base:业务修改之前的中心基线, 排除该提交及其祖先。--source-rev:最终中心分支头, 包含该提交。
范围是 Git 的 SOURCE_BASE..SOURCE_REV。 对于 merge 分支, 它也包含分支头可达、但基线不可达的其他合入提交, 因此线性分支和 merge 分支使用同一种表达方式。 每次只过滤一个目录, 在最终分支上继续开发的相关修改也会包含在导出结果中。
两个中心参数支持分支名、标签、完整或唯一缩写 SHA, 以及 HEAD、HEAD~1 等提交表达式, 运行开始时固定为具体 SHA。 --source-base 必须是 --source-rev 的祖先或自身; 两者相同表示空范围。
--target-base 指定子仓库中承接这些修改的基线分支, 通常是其 PR 目标分支。 它与 --source-base 必须一起使用。 中心基线在所选目录中的文件, 必须与子仓库基线的文件一致。 两个基线不需要同名, 脚本也不会自动选择或保存中心基线。
脚本将过滤后的范围重放到子仓库基线上, 再用所得结果更新 --target-branch。脚本只推送该目标分支; 当它与 --target-base 不同时, 基线分支不会被修改。 范围模式会展平过滤后的 merge。 发生重放冲突或最终文件与源目录不一致时, 脚本返回错误, 不会实际推送。
预演和推送
先预演 build 的回推结果:
python3 tools/josh/push-subtree-branch.py \
--source-base main \
--source-rev blueos/miri/allocator \
--repo build \
--target-remote fork-build \
--target-base main \
--target-branch miri/allocator \
--force
从同一个最终中心分支回推 kernel, 只需更换目录和远端:
python3 tools/josh/push-subtree-branch.py \
--source-base main \
--source-rev blueos/miri/allocator \
--repo kernel \
--target-remote fork-kernel \
--target-base main \
--target-branch miri/allocator \
--force
| 参数 | 含义 |
|---|---|
--source-base | 排除的中心基线;与 --target-base 配对。 |
--source-rev | 最终中心提交, 包含该提交;必填。 |
--repo | 子仓库名称, 目录映射与 merge 脚本相同。 |
--target-remote | 已配置的目标 Josh 远端名。 |
--target-base | 子仓库承接导出范围的基线分支名。 |
--target-branch | 子仓库中要更新或创建的分支名。 |
--repo-root | 中心仓库路径, 默认脚本所在仓库。 |
--force 或 -f | 使用 Josh 普通强推, 允许非快进更新。 |
--push | 预演成功后实际推送;省略时只预演。 |
回推脚本的 --target-base 和 --target-branch 只接受子仓库分支名。 默认只预演, 也不强制推送;添加 --force 仍然只是预演。 历史重写后需要非快进更新时使用 --force.
输出中:
BEFORE (current remote)是本次 fetch 得到的更新前子分支。AFTER (proposed, not pushed)是尚未推送的候选结果。
检查预演结果后, 在同一条命令后添加 --push。 例如, 实际强推 build 分支:
python3 tools/josh/push-subtree-branch.py \
--source-base main \
--source-rev blueos/miri/allocator \
--repo build \
--target-remote fork-build \
--target-base main \
--target-branch miri/allocator \
--force --push
只有实际推送成功后才会显示 Push completed。 脚本仅处理已提交内容, 不移动中心开发分支, 也不修改当前工作区。 用于重放的 detached 临时工作区会自动清理, 不创建临时分支。 过滤结果保存在 refs/export/<remote>/<target-branch>, 范围模式还使用 refs/export-bases/<remote>/<target-branch>; 这些辅助引用每次重新生成, 与导入脚本没有回推进度联动。
示例使用 merge 流程生成的 blueos/miri/allocator。 如果采用 rebase 流程, 将 --source-rev 改为 blueos/miri/allocator-rebased。
导出完整历史
需要过滤最终提交的完整可达历史时, 省略两个基线参数, 只指定 --source-rev:
python3 tools/josh/push-subtree-branch.py \
--source-rev blueos/miri/allocator \
--repo build \
--target-remote fork-build \
--target-branch miri/allocator
此模式可能保留中心导入 merge 和旧同步提交。 希望排除业务修改之前的历史时, 应使用明确基线的范围模式。 SHA 只是选定一个提交, 不能单独表达业务修改的起止边界。
从子仓库获取更新
josh fetch 更新子仓库的远端跟踪引用, 不移动本地开发分支。 如果分支已经迁移并只在中心仓库维护, 按普通中心仓库分支继续开发即可。 仍需获取子仓库业务分支更新时, 要区分下面两种情况。
远端正常快进
对于仍沿用josh导入历史的分支, 可以按普通 Git 方式合并新的远端跟踪引用。
对于已经整合完整中心历史的开发分支, 应先重新映射或移植更新, 避免直接 merge 原始导入引用而带回旧历史。 采用 merge 流程时, 可以以当前中心分支为起点继续调用导入脚本:
python3 tools/josh/merge-subtree-branches.py \
--repo build \
--source-remote fork-build \
--source-branch miri/allocator \
--target-base blueos/miri/allocator \
--target-branch blueos/miri/allocator
脚本会自行 fetch, 再映射和合并来源。 采用 rebase 流程时, 需明确上次已导入的子仓库提交, 将它之后的新修改移植到当前中心分支;当前子仓库 main 不一定就是这个边界。
远端发生 rebase 或强推
追加 merge 会保留中心分支中原有的旧版本历史。 如果需要移除远端已经删除或改写的旧提交, 应从完整中心基线重新整合最新子仓库分支, 再重放需要保留的本地新增修改和其他子仓库的业务修改。
重建时应保留原分支供对照, 并核对提交范围、最终文件和构建结果。 这种更新仍涉及子仓库历史到完整中心历史的转换, 不能只根据 fetch 是否成功判断。
两个脚本的完整参数说明和辅助引用行为见 辅助分支操作工具。
附录
导入子仓库分支
cd blueos
josh remote add upstream-apps_example \
https://github.com/vivoblueos/apps_example.git ':prefix=apps/example'
josh remote add upstream-apps_shell \
https://github.com/vivoblueos/apps_shell.git ':prefix=apps/shell'
josh remote add upstream-book \
https://github.com/vivoblueos/book.git ':prefix=book'
josh remote add upstream-build \
https://github.com/vivoblueos/build.git ':prefix=build'
josh remote add upstream-external \
https://github.com/vivoblueos/external.git ':prefix=external'
josh remote add upstream-kernel \
https://github.com/vivoblueos/kernel.git ':prefix=kernel'
josh remote add upstream-libc \
https://github.com/vivoblueos/libc.git ':prefix=libc'
josh remote add upstream-librs \
https://github.com/vivoblueos/librs.git ':prefix=librs'
josh fetch --remote upstream-apps_example
josh fetch --remote upstream-apps_shell
josh fetch --remote upstream-book
josh fetch --remote upstream-build
josh fetch --remote upstream-external
josh fetch --remote upstream-kernel
josh fetch --remote upstream-libc
josh fetch --remote upstream-librs
josh remote add fork-apps_example \
https://github.com/{{YOUR_USERNAME}}/apps_example.git ':prefix=apps/example'
josh remote add fork-apps_shell \
https://github.com/{{YOUR_USERNAME}}/apps_shell.git ':prefix=apps/shell'
josh remote add fork-book \
https://github.com/{{YOUR_USERNAME}}/book.git ':prefix=book'
josh remote add fork-build \
https://github.com/{{YOUR_USERNAME}}/build.git ':prefix=build'
josh remote add fork-external \
https://github.com/{{YOUR_USERNAME}}/external.git ':prefix=external'
josh remote add fork-kernel \
https://github.com/{{YOUR_USERNAME}}/kernel.git ':prefix=kernel'
josh remote add fork-libc \
https://github.com/{{YOUR_USERNAME}}/libc.git ':prefix=libc'
josh remote add fork-librs \
https://github.com/{{YOUR_USERNAME}}/librs.git ':prefix=librs'
josh fetch --remote fork-apps_example
josh fetch --remote fork-apps_shell
josh fetch --remote fork-book
josh fetch --remote fork-build
josh fetch --remote fork-external
josh fetch --remote fork-kernel
josh fetch --remote fork-libc
josh fetch --remote fork-librs
git --no-pager branch --list --all