Jjvcs

2025-12-22jjvcs
How I use jjvcs in my daily stydy/work.

Jj is the version control system (vcs) I'm currently using. At this point they have brought me much convenience but unavoidably, some missing features for me. One day I will build my own vcs, but before that, let's talk about how I'm using jj.

I like it when jj forces me to use jk to exit insert mode instead of jj. This inspires me to name my future vcs as jk.
# This will be explained in the future page of frman
ln -sf ~/opt/jj/bin/jj-v0.43.0 ~/opt/bin/jk

Multiple tasks in parallel

It's common to work on different features/bugs in parallel. Take my website as example, here's the listing of bookmarks:

jj b l
f/build-from-source: owskvmym 45e1da76 entry(f): frman the package manager where you build everything from source
f/optparse-zig: tlqpyovq 8056572d entry(f): optparse-zig
pages: rlvopurm eb4b4ccc entry(r): zig learning notes
  @codeberg (behind by 1 commits): uxynmovk 6192fcf9 general website content update on august 2026
r/decision-support: wpykmzxs c6f1aad4 entry(r): studying sources for cs master at uu
r/zig-learning-notes: rlvopurm eb4b4ccc entry(r): zig learning notes

For every page(/feature/doc/etc.), I edit its content in an invariant/unique bookmark. The bookmark will move along when I create future revisions to update the page. pages (/stable/master/etc.) marks the most recent stable revision instead of any specific page.

After updating a page, we want to:

Rebase all descendants of pages (except current revision and its descendants) to current revision,

Resolve conflict if there's any,

Move pages to current revision,

Push pages to codeberg (the remote).

jj rebase -s 'roots((pages:: ~ pages) ~ @::)' -o @
# resolve conflict is there's any
jj b m pages -t @
jj git push -b pages --remote codeberg

When updating a page, or suppose we have a bookmark foo for a specific feature, and we want to fix a bug for that feature. We create a new merge revision by passing all desired parent revisions to make the dependency graph more accurate.

jj new stable 'feat(foo)' -m 'fix(foo): bar'
You should never move a bookmark backwards. For example in my use of jj, this action would ruin the history.

Jj, ssh and local collaboration between multiple machines

This section provides instructions for using jj to collaborate between two laptops (host and guest) over ssh, without relying on any (remote) hosting service. Essentially this approach should apply to any number of local machines.

This setup targets at macos. It relies on mDNS to announce a machine's hostname & ip via multicast dns on every network it joins, so other machines can automatically resolve .local hostnames without manual dns configuration.

We use host/guest to indicate the commands in this section will be run on t host/guest, respectively.

The ssh part

Ensure the ssh server is running (host)

# check if ssh is enabled
sudo systemsetup -getremotelogin
# if not, enable it
# sudo systemsetup -setremotelogin on

Sudo set hostname (host)

The formation of this section shall allow us to use hostname.local addresses that automatically resolve on any local network without manual configuration, so we needn't re-configure ip addresses on new networks.

# verify your local hostname
scutil --get LocalHostName

This returns something like MacBook (without the .local suffix).

We can set a memorable hostname:

sudo scutil --set LocalHostName host

This machine will now be reachable at host.local on any local network.

Verify mDNS is working (guest)

ping host.local

Generate an ssh key (guest)

ssh-keygen -t ed25519 -C "your_email@example.com"

Copy guest's public key to host (guest)

# substitute username with the output of `whoami`
ssh-copy-id username@host.local

Test the connection (guest)

ssh username@host.local
You don't need to ssh into the host in the following steps.

The jj part

Initialize a jj repo (host)

Let's assume that we put all of our projects under ~/dev. We create a new project jjssh here:

cd ~/dev && mkdir jjssh && jj git init --colocate

Make some dummy changes,

echo "initial content" >readme

and commit them.

jj ci -m "first commit" && jj b s main -r @-

So far so good. But unfortunately, we can't directly use this repo for ssh-based collaboration. Instead, we need a bare git repo.

Create a bare repo (host)

We need to create a bare repo that will serve as the remote. We will put all our bare repos under ~/remotes.

mkdir -p ~/remotes && cd ~/remotes && git init --bare jjssh.git

Push jj repo to bare repo (host)

We can now add the bare repo as origin push the bookmark to it:

cd ~/dev/jjssh && jj git remote add origin ~/remotes/jjssh.git
jj b t main --remote origin && jj git push -b main --remote origin

The workflow should henceforth be analogous to that in normal jj.

Clone the bare repo (guest)

cd ~/dev && jj git clone --colocate ssh://username@host.local/~/remotes/jjssh.git jjssh

Make changes and push the bookmark (guest)

cd ~/dev/jjssh && jj new main@origin
echo "changes from guest" >>readme && jj ci -m "changes from guest"
jj b s guest -r @- && jj b t guest --remote origin && \
   jj git push -b guest --remote origin

Fetch commits (host)

cd ~/dev/jjssh && jj git fetch --remote origin

Fish function to automate the project setup

function jjinit
   set project_name $argv[1]
   set project_dir ~/dev/$project_name
   set remote_dir ~/remotes/$project_name.git

   mkdir -p $project_dir
   cd $project_dir

   jj git init --colocate

   mkdir -p ~/remotes
   git init --bare $remote_dir

   jj git remote add origin $remote_dir
end