常见问题
已经有了 Git / jj-vcs,为什么还要自己造一个工具?
Git 有着悠久的历史,并且绝大多数开源代码都托管在基于 Git 的平台上。几乎不可能撼动 Git 的地位。
但是你会发现 Git 并不好用。
大多数人的工作流是单一的,而 Git 做了太多事情。很多资深的程序员一旦遇到一些相对复杂的 Git 使用场景,都要去反复查阅各种文档。Git 的命令繁杂、手册冗长。它的很多设计理念新手难以理解。
如果你感兴趣的话,这里有一篇关于 Git 的文章:关于 Git 的礼节。
jj-vcs 是一个革新性的工具,具备了很多新的理念:
- 工作副本(工作区)也是一个提交。这样不需要 stash,而且切换分支和变基永远不会失败。
- 不需要索引区(暂存区),所有变更默认被提交,也包括 Git 中的未追踪文件。
- 不再需要交互式变基,子提交会被自动变基。
- 使用函数式语言指定文件和变更,而不是使用上古时期的方式解析文本。
- 有着默认的根提交。
- ……
这些理念上的革新使得 jj 的本地版本控制非常优秀。
但是 jj 在与 Git 远程的互作性上有待进步。例如书签(分支 HEAD)并不会随着提交而推进;pull / push 的模型依然需要非常熟练才能掌握。jj 与 Git Forge(如 GitHub)的互作性也不够好。
而且 jj 是另一套工具和使用理念,不论你是否学过 Git 都有着不小的学习成本。
所以有了 hj,致力于基于 jj 的优秀理念,提供一个 90% 场景下极简的版本控制体验,降低你的学习成本(你甚至可以在不了解 Git 和 jj 的情况下上手),并且在一些特殊场景中你可以随时切换回 jj 进行操作。这符合 渐进性 的设计理念。
让我们来举几个例子说明 hj 相较于 jj 的极简性:
- 你可以一行命令开始版本控制、新建一个默认的主分支、创建一个 GitHub 仓库、并把远程仓库关联到本地:
hj init --gh- 你可以一行命令提交指定文件,并给出提交信息:
hj commit "feat: add new feature"- 你可以一行命令拉取当前分支的变更,并推送到远程:
hj push main --pullhj 将不断进步!💪🏻
为什么不向 jj 贡献代码,而是直接封装 jj ?
jj 是一个有着优秀设计理念的工具,为了保持其功能的 正交性,不可避免地要提供尽量 通用 的工具,以满足所有用户的版本控制需求。
hj 的理念有所不同,「简单」是我们的第一设计理念。我们封装了许多有用的子命令使得你可以以 最小的心智负担 来完成 Git 和 jj 中复杂的操作。目前我们的工具是对多个命令行工具的组合,以可维护性极强的 Rust 脚本形式为您提供。
我们额外提供的功能中,我确信其中有很多 jj 在短期内一定不会提供,甚至永远不会提供,因为这些功能与 jj 的设计理念相悖。如果你追求简单,不妨来尝试一下我们的工具。 💪🏻
目前存在哪些已知缺陷?
hj undo是基于jj undo的。而 hj 的很多操作不是原子化的,例如hj switch内部执行了两行 jj 命令,所以hj undo不能够完全撤销一个操作。未来我们会根据hj op restore封装一个命令。因为 hj 基于 jj,所以短期内几乎不可能实现各种 IDE 或插件的原生适配(因为 jj 的知名度远远不够)。如果你非常需要这些适配的话,可以考虑 与 Git 仓库共存。
hj 的很多改变工作副本指向的操作不会自动推进书签。这是 jj 的特意设计,hj 正在尝试解决。
jj 和 hj 不支持 Git 的子模块功能。目前你可以做的是在 Git 与 hj 共存的仓库中,手动用 Git 管理子模块间的同步等逻辑,在每个子模块中使用
hj init进行版本控制。hj 几乎每个命令都会和 jj 有着两百毫秒的速度差距,这在命令行中是非常明显的:

原因是 hj 每次调用 jj 时都有着 spawn 子进程的开销。未来我们会考虑使用 jj-lib,或使用 jj 提供的对外 API 来优化。
- hj 目前仅在 GitHub 中发布各架构的 Release。除了从 GitHub 手动下载二进制文件外,你仅能通过
cargo install --path .来从源码编译。未来我们将会至少支持以下安装方式:
路线图
✅ 表示已经实现,🚧 表示正在实现,🤔 表示未来计划。你也可以在 Issues 里提出你的想法!
- ✅
clone支持省略owner。 - ✅
hj clone --fork - ✅
hj commit和hj describe时支持打开编辑器进行多行描述的编辑。 - ✅ 提供 Hooks 支持。
- 🚧 全面移除对 gh cli 的依赖,使用 GitHub API。
- 🚧 重构对 jj 调用的封装。
- 🤔 发布到 scoop、homebrew;编写 PowerShell script 和 bash script。
- 🤔
hj init时支持下载.gitignore。(来源) - 🤔 全面支持 GitLab。
- 🤔
hj rollback:交互式选择要回退的命令。 - 🤔 优化与 Git 仓库共存时的体验。
- 🤔 提供更多与
hj log相关的简化命令。 - 🤔 完善的 Stacked PR 支持。
- 🚮
hj download下载单个文件(夹) 时直接下载到指定的目录,而不包含远程仓库中的文件夹结构。
hj 是如何实现的?
hj 用 Rust 语言编写的一个命令行工具。
目前多数子命令我们使用 Shell Command 的形式构建基于 jj、Git、gh 等工具的命令。
移除 jj、git、gh 依赖的计划
目前 hj 强依赖 jj 可执行文件;涉及 GitHub 的操作强依赖 gh(有些操作如下载仓库是调用的 GitHub API);涉及与 Git 仓库共存的操作强依赖 Git。
未来我们将逐步移除这些依赖。
而一些子命令(如 hj squash/split/rebase 等),直接调用 jj 来实现,不做任何命令行参数的转换和命令的封装,这是因为它们已经做得足够完善了,不需要多余的封装。
我应该从 Git 切换到 Jujutsu 或 hj 吗?
切换成本很低,因为它可以与您现有的工作流程无缝集成。坦白说,最多一天就能习惯使用,而且使用好东西确实有道理。当然,你可以在任何食品安全的壶里泡出好茶,但如果你有一个漂亮的茶壶,每次泡茶都会享受。你一定经常使用版本控制系统,为什么不让它也变得愉快呢?
这个比喻来自 kubamartin 的博文。
如何实现当切换(cd)到 hj 项目目录时,自动拉取最新的代码?
cd 的行为需要注册到 Shell 的 Hook 中,具体根据 Shell 不同存在多种实现。
例如 fish 中,可以监听 PWD 的变化:
function on_dir_change --on-variable PWD
if test -d "$PWD/.jj"
hj pull
end
end例如 nushell 中,也可以监听 PWD 的变化:
$env.config.hooks = {
env_change: {
PWD: [
{|before, after|
if $after | path join ".jj" | path exists {
hj pull
}
}
]
}
}以上都是全局配置,如果你要项目级配置,则需要更复杂的代码。而 mise 提供了这个功能!只需在 mise.toml 中编写:
[hooks]
enter = "hj pull"我有工作邮箱和自己的邮箱,一个用于工作、一个用于开源项目,怎么灵活地切换呢?
一个朴素的方法是在每个代码仓库的 .jj/repo/config.toml 中指定:
[user]
name = "YOUR NAME"
email = "YOUR_EMAIL@example.com"这样这个配置就会覆盖全局配置。
当然,这么做需要你对每个项目都单独配置。以下是我使用 mise 的好方法:
指定两个目录,一个为 Work,另一个为 Project,将工作项目和开源项目分开放置。在 Work 的目录下新建 mise.toml:
[env]
JJ_USER = "{{env.WORK_USER}}"
JJ_EMAIL = "{{env.WORK_EMAIL}}"(当然,你也可以直接写值,不从环境变量中读取)
这样,你每次 cd 到 Work 及其子目录时,就会自动给你挂上这两个环境变量。你可以使用 hj config list 来验证。
在 Work 外的其它目录,因为没有这两个环境变量,就会正常使用默认的全局配置。
必须使用这种变通方法的原因是 jj 只从本仓库内和全局读取配置,而不从本目录递归向上读取配置。作为一个版本控制工具,jj 有其特殊的考虑。未来 hj 可能考虑在 hj.toml 中加上配置项,为每个操作自动加上 --config 标志。