Blog. Different ramblings on mostly web development. Technically I'm also on Bluesky and Mastodon.


Jujutsu version control

jj

gg

hg

wait…

Hello! Jujutsu, or jj, is a newer version control system that can replace Git and these were my private notes as I’m learning it. Decided to publish them here.

Someone said that

AI is a version control problem

and I very much believe this. I don’t know if jj is “better” than git but it’s worth exploring.

Most who use git will at some point ask themselves: “Why so complicated? Is it me? Is it git? Well, both. Git suffer from decades of history. Git can not just change a command or improve how it works, because it is used everywhere. In software we usually avoid change when possible.

Jujutsu has a unique position in that it’s is, or was, not used anywhere. It can learn from years and years of millions of people working with git, mercurial and others daily. And it can give us a fresh take. This article explores Jujutsu from a beginner’s perspective and my aim is to be able to use it for myself to start with.

Since jj uses Git as its backend, you can try it on existing repositories with minimal risk.

To start if you already have a repo:

jj git clone <repo-url>

or

jj git init --git-repo <path>

Coming from git you will be used to working with commits. In Jujutsu the fundamental change is called a revision and importantly new revisions are AUTOMATICALLY created as files are changed. You do not need to git add anything. As you use jj log you can observe the change id staying consistent and the revision id updating with every file change.

I found these references:

and here’s my version:

The rough guide to Jujutsu (jj)

These are my (edited) notes as I’m trying to understand how jj works. This guide is aimed at people with experience with version control systems, particularly git.

  • Change → a unit of work with a stable ID (stays open for continued editing)
  • Revision → a draft of that change (each edit makes a new revision)
  • Working copy @ → always a revision; file edits apply directly
  • Bookmark → a named pointer to a change (independent of graph structure)
  • Revset → query language for selecting revisions (change IDs, revision IDs, bookmarks, @, @-, main..@)

Working copy

  • The working copy is itself a commit (revision)
  • Edits to files are auto-tracked — no staging step
  • Most jj commands create a new revision when you run them (not in background)
  • New files are tracked by default; respects .gitignore
  • To stop tracking a file: jj file untrack path/to/file

First steps

jj git clone <url>  # → start on fresh working copy `@`, already a new change
# edit files
jj describe -m "Add login form"  # → refine current change (same ID, new revision)
jj new  # → finish current change, start fresh one on top

Everyday flow

See where you are:

jj status # or jj st

# @- is the parent change, @ is the current change
jj log -r @-
jj log -r 'main..@'
jj show          # show changes in current revision
jj show -r @-    # show changes in parent revision

Pause current work to start something else:

jj new @-          # create new change based on parent, keeping current work
# work on urgent fix
jj new             # back to a fresh change when done

Discard unwanted work:

jj abandon         # discard current revision (work is lost)
jj abandon -r <id> # discard specific revision

Move your working copy to a different change:

jj edit -r main        # go to main (like git checkout main)
jj edit -r @-          # go to parent change
jj edit -r <change-id> # go to specific change

Restore specific files from other revisions:

jj restore --from main file.txt         # restore file from main
jj restore --from @- --to @ file.txt    # restore file from parent to current
jj restore --from @- .                  # restore all files from parent

Selective changes

jj split -i        # split working copy into two changes (interactive hunks)
jj squash -i       # move hunks into parent change
jj squash <file>   # move whole file into parent

This replaces Git’s git add -p and git commit --amend -p workflow. Note: jj split is subtractive - you remove what you don’t want in the first commit (opposite of Git’s additive staging).

Reshape history

jj squash           # combine changes
jj diffedit         # edit an earlier change

Conflicts

Unlike git, jj can save conflicts directly in commits. You don’t have to fix them immediately. The <<<<<<< markers in your files are just how jj shows you the conflict - the real conflict data is stored separately.

# Option 1: Fix conflicts in a new commit
jj new <conflicted-commit>
# edit files to remove conflict markers
jj squash                   # merge the fix back

# Option 2: Use a merge tool
jj resolve

Benefits: you can rebase or merge commits that still have conflicts. Other commits will update automatically even when conflicts exist.

Undo mistakes

jj op log             # see all repository operations
jj undo               # undo last operation
jj obslog -r <change> # see how a specific change evolved over time

Describe vs new

In a fresh repo, you usually jj describe first. Use jj new only when moving on to the next change. Changes have stable IDs that persist through all edits and descriptions.

  • jj describe → updates the the description of the current change. You can keep working on it after this.
  • jj new → close the current change, begin a new one (without a description) on top.
  • jj commit → shortcut for jj describe + jj new (Git-style workflow).

Bookmarks (branches)

In jj you won’t deal with branches as you know them from Git. Instead you create bookmarks when you want to share changes. You can work on anonymous changes until you’re ready to sync:

jj bookmark create feature-name    # create bookmark at current change
jj git push -c feature-name        # push and create remote bookmark

Or let jj auto-generate bookmark names when pushing:

jj git push -c @                   # auto-generates bookmark name

Sync with remotes

jj git fetch                        # updates local view of remote bookmarks
jj rebase -d main                   # restructures your change to sit on top of main
jj bookmark create feature-name     # create bookmark for sharing
jj git push -c feature-name         # push new bookmark (-c = create)

Merging into main (fast-forward)

# After rebasing onto main, move the main bookmark to your change
jj bookmark set main -r @           # moves bookmark pointer (doesn't change graph)
jj git push --bookmark main         # pushes bookmark movement to remote

Revset flexibility: Use change IDs (nrtvnxuk), revision IDs (fea9ae77), bookmarks (main, feature-name), or symbols (@, @-) interchangeably with -r. Jj shows unique prefixes in output - type just the highlighted characters.

In jj, you don’t “stage then commit.” You work inside a change, refine it with describe, splitting and squashing as needed. Use new only when you’re ready for the next change.

flowchart TD

subgraph Change_A["Change A (stable ID)"]
  R1["Revision A1 (first working copy)"]
  R2["Revision A2 (after jj describe)"]
  R3["Revision A3 (after jj describe again)"]
end

subgraph Change_B["Change B (new stable ID)"]
  R4["Revision B1 (jj new creates this)"]
end

R1 --> R2 --> R3 --> R4

Neovim lsp

I understand the joy and pain of programming without autocomplete, but navigating your code with an lsp is so powerful, that you should take the time to set it up, learn the bindings and use it.

With neovim it’s traditionally been super hard to get working. I believe it will work without plugins, but I found it a better experience using blink.cmp. This is how I got it working using Neovim v0.11.3

JUST TELL ME

  • blink.cmp (handles the auto-complete dropdown ui, just a better out of the box experience)
  • neovim/nvim-lspconfig (quality of life, so we don’t have to configure each LSP manually)
  • mason-org/mason.nvim (quality of life, for keeping your LSP servers up to date)

There are multiple ways of triggering the completions. I’m a tab-person, and with blink.cmp you enable that like so:

keymap = { preset = 'super-tab' },

Finally, since we installed nvim-lspconfig, and this is the reason we do it, we have default configs we can enable like this in our config.

Run :Mason to manage (install) your prefered LSP servers. For web dev, this is what I use:

vim.lsp.enable('lua_ls')
vim.lsp.enable('html_lsp')
vim.lsp.enable('css_lsp')
vim.lsp.enable('ts_ls')
vim.lsp.enable('svelte')
vim.lsp.enable('biome')

Use :checkhealth vim.lsp to debug whether it’s working for the active buffer.

As always, these tools all have one million settings available to customize into oblivion.

How to use

For completions you do smth like: (or up arrow) and (or down arrow) to select Tab to select completion C-k to show signature

For navigating, this is the beauty of it:

"grn" is mapped in Normal mode to vim.lsp.buf.rename()
"gra" is mapped in Normal and Visual mode to vim.lsp.buf.code_action()
"grr" is mapped in Normal mode to vim.lsp.buf.references()
"gri" is mapped in Normal mode to vim.lsp.buf.implementation()
"grt" is mapped in Normal mode to vim.lsp.buf.type_definition()
"gO" is mapped in Normal mode to vim.lsp.buf.document_symbol()
CTRL-S is mapped in Insert mode to vim.lsp.buf.signature_help()

You can skip through diagnostics using [d and ]d.

Malicious npm packages (and AI...)

It’s not too complicated.

  1. Find popular npm package
  2. Insert malware in package
  3. Steal the npm token to publish packages
  4. Publish to npm directly without touching the git repo

The newest example is unfortunately the nx package as detailed here: https://socket.dev/blog/nx-packages-compromised. Wild, but obvious. Can recommend you also read https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/.

This one was interesting as it relies on the affected users having AI CLI tools installed and uses it to do the work of discovering and extracting sensitive files, disguised as a “pentest agent prompt”.

How do we deal with this?

Deno programs run without file, network or environment access unless you give it permissions. It looks similar to this deno run --allow-net --allow-read --allow-env server.ts.

Socket provides both CI actions that notify you about nasty packages in your code, and they offer a a “safe npm” wrapper when you npm i.

Claude (and the other AI CLI tools) all offer granular permissions through “tool usage” and more.

We’ll see! For sure there will be many examples as we continue to let AI control our systems.

uhtml v5

.innerHTML() can take you a loooong way, but there’s a breaking point where it just becomes too tedious to use, too annoying to deal with event handlers or escaping security. So when you want to generate HTML with JS, most use a framework. But you might not need one. Sometimes it is very fine to organize to your own desires and let uhtml handle rendering + producing the HTML. And the new v5 version has (finally?) made the API less confusing, and in my experience just works.

Here’s a quick example from https://github.com/WebReflection/uhtml:

<!doctype html>
<script type="module">
  import { html } from 'https://esm.run/uhtml';

  document.body.prepend(
    html`<h1>Hello DOM !</h1>`
  );
</script>

masonry web layout

What feels like two decades ago I was busy making masonry-style layouts for different agency & portfolio websites. Funny to see the masonry layout slowly but surely reaching the web platform as a standard.

CSS masonry layout example

Here’s an excerpt of the proposed (!) syntax:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  item-flow: row collapse;  
  gap: 1rem;
}

<rough-sheet>

For another idea I needed a drawer UI like you know from mobile UI.

If you’re using React, Vaul seems good. But I’m not using React.

I prototyped a new custom element: <rough-drawer>, but quickly learned that this pattern is commonly called a “sheet”. And since all my web components are prefixed with my last name… welcome - your new drawer sheet ui element.

It’s not done, but try dragging on https://sheet.0sk.ar/ and view source.

Links 2025-07

Observable is a friendly tool, and it’s more aligned with the web with their new version.

When it comes to standard web UI, Radix is a good place to start.

SVG is wild, and this is a great intro:

uhtml is (was?) my library of choice for rendering HTML when I don’t have a framework. It was always a very confusing API to me, but the new version seems to unify things under one module, which is relieving.

And a shout out to Ink & Switch, who continues to inspire more thinking

Also, pondering whether to post these as single posts with a bit more context, or keep it a collection whenever I have a few to share.

CLI AI assistants

We’ve had the copy/pasting era of the LLMs, we have AI assistants inside our editors and now we also have them as CLI programs. Having them side-by-side with your local files and letting it loose is a sight to behold. The UI is interesting, too. Slash commands, auto-complete and a text input with many shortcuts, vim-bindings and so on. Pretty fun way to interact with your files.

claude just works really well.

gemini (Google’s AI) released their CLI tool and made it open source. It was interesting to read their system prompt.

cursor-agent

jules