Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

abrenneke/jj-vine

A tool for submitting stacked Pull/Merge Requests from Jujutsu bookmarks. GITHUB MIRROR. CANONICAL REPOSITORY AT https://codeberg.org/abrenneke/jj-vine

abrenneke/jj-vine.json
{
"createdAt": "2026-01-11T22:57:25Z",
"defaultBranch": "main",
"description": "A tool for submitting stacked Pull/Merge Requests from Jujutsu bookmarks. GITHUB MIRROR. CANONICAL REPOSITORY AT https://codeberg.org/abrenneke/jj-vine",
"fullName": "abrenneke/jj-vine",
"homepage": null,
"language": "Rust",
"name": "jj-vine",
"pushedAt": "2026-03-18T01:45:35Z",
"stargazersCount": 5,
"topics": [],
"updatedAt": "2026-03-18T01:45:39Z",
"url": "https://github.com/abrenneke/jj-vine"
}

A tool for submitting stacked Pull/Merge Requests from Jujutsu bookmarks.

Supports the following code forges:

The canonical location for jj-vine is codeberg.org/abrenneke/jj-vine. GitHub is used as a mirror and CI only.

  • [Overview]!(#overview)

  • [Main Features]!(#main-features)

  • [Planned Features]!(#planned-features)

  • [Installation]!(#installation)

    • [Cargo Binstall]!(#cargo-binstall)
    • [Mise]!(#mise)
    • [Pre-built Binaries]!(#pre-built-binaries)
    • [Attestations]!(#attestations)
    • [Alias Setup]!(#alias-setup)
  • [Quick Start]!(#quick-start)

  • [Commands]!(#commands)

    • [submit]!(#submit)
    • [init]!(#init)
    • [status]!(#status)
  • [Configuration]!(#configuration)

    • [Forge-Specific Settings]!(#forge-specific-settings)

      • [Forge]!(#forge)
      • [GitLab]!(#gitlab)
      • [GitHub]!(#github)
      • [Forgejo/Codeberg/Gitea]!(#forgejo-codeberg-gitea)
      • [Azure DevOps]!(#azure-devops)
    • [Common Settings]!(#common-settings)

  • [Description Generation / Stack Visualization]!(#description-generation—stack-visualization)

    • [Configuration]!(#description-configuration)
    • [Linear Format]!(#linear-format)
    • [Tree Format]!(#tree-format)
    • [Description Generation]!(#description-generation)
  • [Title Generation]!(#title-generation)

    • [Configuration]!(#title-configuration)
    • [Custom Title Templates]!(#custom-title-templates)
  • [Credits]!(#credits)

  • [FAQs]!(#faqs)

    • [Is this vibe-coded slop?]!(#is-this-vibe-coded-slop)
    • [Ok, but really?]!(#ok-but-really)
    • [Why a new project?]!(#why-a-new-project)
  • [Contributing]!(#contributing)

  • [License]!(#license)

As jj is so flexible, it can sometimes be tedious to manage pull/merge requests for a jj repository. Additionally, many people like the “stacked pull/merge request” workflow, where a tool can manage your stack of pull/merge requests for you, including modifying the base branch, description, and other settings. jj-vine aims to smooth out the process of managing pull/merge requests for a jj repository.

There are several tools these days that aim to solve this problem, most notably jj-spr and jj-stack. jj-vine has its own design choices, and may not be a direct replacement for these tools. Take a look at the differences & features below to see if jj-vine is a good fit for you.

Major differences:

  • jj-vine is bookmark-based, rather than change-based. It expects you to create your bookmarks before submitting them, and (usually) expects you to push them (e.g. jj git push -c @) as well. This means that you can have multiple commits in each pull/merge request.
  • jj-spr aims to more be a full workflow rather than a lightweight tool. jj-vine is less opinionated.
  • jj-vine is primarily based around the submit --tracked command. This submits all (your) tracked bookmarks at once. Other tools often require you to submit each bookmark individually. The idea is to simply “sync your current state to the code forge”.
  • Bookmarks are not automatically forwarded. You’ll need to use jj bookmark set to update the target of a bookmark.
  • Stacked pull/merge request creation

    Automatically creates pull/merge requests with correct base branches based on bookmark dependencies. Works great for single branches as well.

  • [Stack visualization]!(#description-generation—stack-visualization)

    Adds a navigable stack diagram to pull/merge request descriptions with links to related pull/merge requests. Can be customized and disabled.

  • Unopinionated

    jj-vine is not opinionated about how you should structure your commits, bookmarks, and pull/merge requests. It can work with what you have. It also works great with auto-generated bookmarks (jj git push -c <rev>).

  • [Complex branching]!(#complex-graph-of-bookmarks)

    Not only does jj-vine support trees of bookmarks, but it fully supports complex (DAG) graphs of changes just like jj itself does. While actual forge support for PRs/MRs with multiple parents may be limited, jj-vine can still visualize and manage them.

  • [Status]!(#status)

    Can easily report the status of your bookmarks and their pull/merge requests.

  • Automatic syncing

    Updates pull/merge request base branches & all related descriptions when stack structure changes.

  • Automatic rebasing

    Once a pull/merge request is merged, rebases the stack on top of the trunk

  • Landing

    Merge a pull/merge request and automatically rebase dependent pull/merge requests on top of the trunk

The preferred way to install jj-vine is to use cargo-binstall:

Terminal window
# Will put the binary in $HOME/.cargo/bin
cargo binstall jj-vine

If you use mise, you can install with:

Terminal window
mise use -g cargo:jj-vine

Pre-built binaries are available for Linux, macOS, and Windows (ARM64 and x86_64 for all). You can download directly from the releases page.

Binaries are built with GitHub attestations. You may verify the provenance of a binary by running:

Terminal window
gh attestation verify <binary> -R abrenneke/jj-vine

You can set up a jj alias to make it easier to use jj-vine. Aliases that work great are jj pr, jj mr, or jj vine. You can run the following command to install an alias:

Terminal window
# Take your pick:
jj config set --user aliases.pr '["util", "exec", "--", "jj-vine"]'
jj config set --user aliases.mr '["util", "exec", "--", "jj-vine"]'
jj config set --user aliases.vine '["util", "exec", "--", "jj-vine"]'
  1. Run jj-vine init to set up your code forge configuration for your repository. This is stored in .jj/repo/config.toml. You may also move any configuration settings to the global config file ~/.config/jj/config.toml.

    Terminal window
    jj-vine init
  2. Push up some bookmarks (auto-generated bookmarks work great!)

    Terminal window
    jj new main
    jj commit -m "Add feature A"
    # Make some changes
    jj git push -c @-
    jj commit -m "Add feature B"
    # Make some changes
    jj git push -c @-
  3. Submit all tracked bookmarks at once:

    Terminal window
    jj-vine submit --tracked

    This creates two pull/merge requests:

    • feature-a targeting main
    • feature-b targeting feature-a

Submit a bookmark and its dependencies as pull/merge requests.

Terminal window
# Submit a single bookmark or revset (and its dependencies!)
jj-vine submit <revset/bookmark>
jj-vine submit -r <revset/bookmark>
# Submit all tracked bookmarks. Roughly equivalent to `(mine() & tracked_remote_bookmarks()) ~ trunk()`, but has additional stipulations. See `jj-vine submit --help` for more details.
# This is the recommended command to use!
jj-vine submit --tracked
# Preview without making changes
jj-vine submit <options> --dry-run

See all options and additional help with jj-vine submit --help.

Interactive setup wizard to configure jj-vine for your repository.

Terminal window
jj-vine init

Show the status of tracked bookmarks and their pull/merge requests.

Terminal window
# Show the status of all my bookmarks
jj-vine status
# Show the status of all tracked bookmarks
jj-vine status --tracked
# Show the status of a specific revset that includes bookmarks
jj-vine status -r <revset>

The output will look roughly like this (but with colors):

Terminal window
!100 "This is the title of the first merge request"
push-xxxxxxxa Checks OK Needs approval (0/1) • 2 open discussions • 24d 21h old • https://forge-url.example/100
!101 "This is the title of the second merge request"
push-xxxxxxxb [READY] • ✓ Checks OK • Approved (1/1) • 16d 18h old • https://forge-url.example/101
!102 "Third title"
push-xxxxxxxc Checks OK Needs approval (0/1) • 5d 6h old • https://forge-url.example/102
!103 "Fourth merge request title"
push-xxxxxxxd Checks OK Needs approval (0/1) • 19h old • https://forge-url.example/103
!104 "Fifth title"
push-xxxxxxxe Checks failing Needs approval (0/1) • 1 open discussion • 19h old • https://forge-url.example/104
wip No merge request
wip-2 No merge request

See all options and additional help with jj-vine status --help.

Configuration is stored in jj’s configuration system under the jj-vine section. You can use jj config edit --repo to edit the configuration for a specific repository or jj config edit --user to edit the global configuration. You can also use jj config set --repo <key> <value> to set a configuration value for a specific repository:

Terminal window
jj config set --repo jj-vine.deleteSourceBranch true
jj config set --repo jj-vine.defaultReviewers '["alice", "bob"]'

All listed settings are under the jj-vine section so should be prefixed with jj-vine..

Ad a minimum, the jj-vine.forge configuration setting must be set to the type of forge you are using.

SettingDescriptionTypeRequiredDefault
forgeThe type of forge you are using”gitlab” | “github” | “forgejo” | “azure”Yes-

Required when jj-vine.forge is set to gitlab.

SettingDescriptionTypeRequiredDefault
gitlab.hostGitLab instance URL (e.g., https://gitlab.example.com)StringYes-
gitlab.projectProject ID where branches are pushed (group/project or numeric ID like 12345)StringYes-
gitlab.tokenPersonal Access Token with api scopeStringYes-
gitlab.targetProjectTarget project ID for merge requests (e.g., upstream/project). Use if you are using a fork.StringNo(same as gitlab.project)
gitlab.createMergeRequestDependenciesWhether to create dependencies between merge requests, requiring that all parent merge requests are merged before the child merge request can be merged.BooleanNotrue

Required when jj-vine.forge is set to github.

SettingDescriptionTypeRequiredDefault
github.hostGitHub API URL (defaults to https://api.github.com for GitHub.com, or https://github.example.com/api/v3 for Enterprise)StringYes-
github.projectRepository where branches are pushed in owner/repo formatStringYes-
github.tokenPersonal Access Token with repo scopeStringYes-
github.targetProjectTarget repository for pull requests (e.g., upstream-owner/repo). Use if you are using a fork.StringNo(same as github.project)

Required when jj-vine.forge is set to forgejo.

SettingDescriptionTypeRequiredDefault
forgejo.hostForgejo/Codeberg/Gitea instance URL (e.g., https://codeberg.org)StringYes-
forgejo.projectRepository where branches are pushed in owner/repo formatStringYes-
forgejo.tokenAPI access token with repo scopeStringYes-
forgejo.targetProjectTarget repository for pull requests (e.g., upstream-owner/repo). Use if you are using a fork.StringNo(same as forgejo.project)
forgejo.wipPrefixPrefix for WIP/draft pull requests. What counts as a draft pull request is configurable per-repository on Forgejo.StringNo”WIP: “

Required when jj-vine.forge is set to azure.

SettingDescriptionTypeRequiredDefault
azure.hostAzure DevOps instance URL (e.g., https://dev.azure.com)StringYes-
azure.vsspsHostAzure DevOps Security (VSSP) host (e.g., https://vssps.dev.azure.com). Used to look up other users for automatic review requests.StringNo-
azure.projectOrganization and project where branches are pushed, formatted as organization/projectStringYes-
azure.sourceRepositoryNameName of the repository in the project where branches are pushedStringRequired if azure.sourceRepositoryId is not set-
azure.sourceRepositoryIdID of the repository in the project where branches are pushedStringRequired if azure.sourceRepositoryName is not set-
azure.tokenPersonal Access TokenStringYes-
azure.targetProjectTarget organization & project for pull requests (e.g., upstream-organization/project). Use if you are using a fork.StringNo(same as azure.project)
azure.targetRepositoryNameName of the repository in the target project for pull requestsStringRequired if azure.targetRepositoryId is not set and azure.targetProject is different from azure.project-
azure.targetRepositoryIdID of the repository in the target project for pull requestsStringRequired if azure.targetRepositoryName is not set and azure.targetProject is different from azure.project-

These settings apply to all forges:

SettingDescriptionTypeRequiredDefault
remoteNameThe remote name to use for pushing and pulling branchesStringNo”origin”
deleteSourceBranchConfigures the pull/merge request to delete the source branch when merged. Currently has no effect for GitHub and Forgejo (is a repository-level setting and on-merge flag only)BooleanNotrue
squashCommitsConfigures the pull/merge request to squash commits when merging. Currently has no effect for GitHub and Forgejo (is a repository-level setting and on-merge flag only)BooleanNofalse
assignToSelfAutomatically assign created pull/merge requests to yourself. Has no effect for AzureDevOpsBooleanNofalse
defaultReviewersList of usernames to automatically add as reviewers when creating pull/merge requests. For Azure DevOps, this should be a list of “user descriptors” and azure.vsspsHost must be setArrayNo[]
caBundlePath to CA certificate bundle for custom TLSString | nullNonull
tlsAcceptNonCompliantCertsAccept non-compliant TLS certificates (for certificates that don’t meet strict X.509 standards). This is almost always unnecessary unless you have a unique situation.BooleanNofalse
defaultBaseBranchDefault target branch for pull/merge requests into trunk()StringNo(detected automatically using the trunk() revset)
openAsDraftOpen newly created pull/merge requests as draftsBooleanNofalse
descriptionConfiguration for pull/merge request description generationObject (see below)No(see below)

Description Generation / Stack Visualization

Section titled “Description Generation / Stack Visualization”

If enabled, jj-vine can generate both a description/body for a pull/merge request, and a stack diagram to include in the description. The stack diagram will always be kept in sync with your PR/MR stack upon submitting your bookmark(s). The description generation will only sync if description.sync is enabled, otherwise the description generation will only happen when a pull/merge request is first created.

If you would like description generation, but not a stack diagram, you can set each value of description.diagram to none. Alternatively, if you would like to only generate a stack diagram, you can set description.singleRevision and description.multipleRevisions to none.

SettingDescriptionTypeRequiredDefault
description.enabledWhether to enable or disable description generation entirely. If false, pull/merge request descriptions will not be touchedBooleanNotrue
description.syncWhether to sync the description of a pull/merge request every time the bookmark is submitted. If this is enabled, any changes you make to the description will be overwritten by the generated description on the next submission. Defaults to falseBooleanNofalse
description.diagramHow to render the stack diagram for different types of merge request stacksObjectNo(see next rows)
description.singleRevisionHow to generate the non-stack part of the description for a pull/merge request when there is only one revision in the pull/merge requestnone | notFirstLine | fullMessage | commitListFirstLine | commitListFull | file(path_to_file) (see below)NonotFirstLine
description.multipleRevisionsHow to generate the non-stack part of the description for a pull/merge request when there are multiple revisions in the pull/merge requestnone | notFirstLine | fullMessage | commitListFirstLine | commitListFull | file(path_to_file) (see below)NocommitListFull
description.diagram.singleHow to render a single pull/merge request, without any parents or children besides the trunknone | linear | treeNonone
description.diagram.linearHow to render a linear stack of bookmarks. This means that no tracked bookmark has multiple parents or multiple childrennone | linear | treeNolinear
description.diagram.treeHow to render a tree of bookmarks, where two bookmarks merge into a common parent, but no bookmark has multiple parentsnone | linear | treeNotree
description.diagram.complexHow to render a complex (DAG) graph of bookmarks, where any bookmark has multiple parents. Because forges only support a pull/merge request merging into a single parent, in this situation you may see commits of one pull/merge request included in other pull/merge requestsnone | linear | treeNocomplex

The following sections show examples of the different stack formats.

(description.diagram.single = "linear" and description.diagram.linear = "linear")

This PR is part of a stack containing 5 PRs:

  1. main
  2. [#1]!(#) “Feature A”
  3. “Feature B” (this PR)
  4. [#3]!(#) “Feature C”
  5. [#4]!(#) “Feature D”
  6. [#5]!(#) “Feature E”

(description.diagram.tree = "linear")

This PR is part of a tree containing 8 PRs:

  1. main
  2. [#1]!(#) “Feature A” → main
  3. [#4]!(#) “Feature D” → [#1]!(#)
  4. [#2]!(#) “Feature B” → [#1]!(#)
  5. “Feature E” (this PR) → [#2]!(#)
  6. [#3]!(#) “Feature C” → [#2]!(#)
  7. [#7]!(#) “Feature G” → [#3]!(#)
  8. [#8]!(#) “Feature H” → [#7]!(#)
  9. [#6]!(#) “Feature F” → [#3]!(#)

(description.diagram.complex = "linear")

This PR is part of a complex set of PRs containing 10 PRs:

  1. main
  2. [#9]!(#) “Feature I” → main
  3. [#10]!(#) “Feature J” → [#9]!(#)
  4. [#1]!(#) “Feature A” → main
  5. [#2]!(#) “Feature B” → [#1]!(#)
  6. “Feature E” (this PR) → [#2]!(#), [#10]!(#)
  7. [#4]!(#) “Feature D” → [#1]!(#), [#2]!(#)
  8. [#3]!(#) “Feature C” → [#2]!(#)
  9. [#7]!(#) “Feature G” → [#3]!(#), [#5]!(#), [#10]!(#)
  10. [#8]!(#) “Feature H” → [#7]!(#)
  11. [#6]!(#) “Feature F” → [#3]!(#), [#9]!(#)

(description.diagram.single = "tree" and description.diagram.linear = "tree")

This PR is part of a stack containing 5 PRs:

  • main

    • [#1]!(#) “Feature A”

      • “Feature B” (this PR)

        • [#3]!(#) “Feature C”

          • [#4]!(#) “Feature D”

            • [#5]!(#) “Feature E”

(description.diagram.tree = "tree")

This PR is part of a tree containing 8 PRs:

  • main

    • [#1]!(#) “Feature A”

      1. [#2]!(#) “Feature B”

        1. [#3]!(#) “Feature C”

          1. [#7]!(#) “Feature G”

            • [#8]!(#) “Feature H”
          2. [#6]!(#) “Feature F”

        2. “Feature E” (this PR)

      2. [#4]!(#) “Feature D”

(description.diagram.complex = "tree")

This PR is part of a complex set of PRs containing 10 PRs:

  • main

    1. [#9]!(#) “Feature I”

      1. [#10]!(#) “Feature J”

        1. [#7]!(#) “Feature G” (→ [#3]!(#), [#5]!(#) also)

          • [#8]!(#) “Feature H”
        2. “Feature E” (this PR) (→ [#2]!(#) also)

          • [#7]!(#) “Feature G” (→ [#3]!(#), [#10]!(#) also)

            • [#8]!(#) “Feature H”
      2. [#6]!(#) “Feature F” (→ [#3]!(#) also)

    2. [#1]!(#) “Feature A”

      1. [#2]!(#) “Feature B”

        1. “Feature E” (this PR) (→ [#10]!(#) also)

          • [#7]!(#) “Feature G” (→ [#3]!(#), [#10]!(#) also)

            • [#8]!(#) “Feature H”
        2. [#3]!(#) “Feature C”

          1. [#7]!(#) “Feature G” (→ [#5]!(#), [#10]!(#) also)

            • [#8]!(#) “Feature H”
          2. [#6]!(#) “Feature F” (→ [#9]!(#) also)

        3. [#4]!(#) “Feature D” (→ [#1]!(#) also)

      2. [#4]!(#) “Feature D” (→ [#2]!(#) also)

jj-vine can generate a description for a pull/merge request when it is created. Note that this description will not be updated automatically if changes are made later (in case you want to remove it entirely, or change it, etc).

You can configure how the description is generated by setting the description.singleRevision and description.multipleRevisions settings. singleRevision is used when there is only one revision in the pull/merge request, and multipleRevisions is used when there are multiple revisions in the pull/merge request.

If jj-vine.description.enabled is false, the description will not be generated.

The following options are available:

Do not generate a description in this situation.

Take the commit message of the head commit in the branch, trim the first line, and use the rest as the description. This is the default behavior for when there is only one revision in the pull/merge request (because the title of the PR/MR uses the first line of the commit message).

Use the full commit message of the head commit in the branch as the description.

Generate a list of all commits in the branch, with their hashes and the first line of each commit message. For example:

  • xxxxxxxa Head Commit Message
  • xxxxxxxb Parent 1 Message
  • xxxxxxxc Parent 2 Message

Generate a list of all commits in the branch, with their hashes and the full commit messages. This is the default behavior for when there are multiple revisions in the pull/merge request (because the default title of the PR/MR uses the bookmark name). For example:

  • xxxxxxxa Head Commit Message
    Message line 2
    Message line 3
  • xxxxxxxb Parent 1 Message
    Message line 2

    Message line 3
    Message line 4
  • xxxxxxxc Parent 2 Message
    Message line 2
    Message line 3

Include the contents of a file at the given path as the description. This is useful if you use pull request templates. For example, file(.github/pull_request_template.md) will include the contents of .github/pull_request_template.md as the description. The file path is relative to the root of the repository.

jj-vine will automatically generate a title for a pull/merge request when it is created. It can also optionally sync the title of a pull/merge request every time the bookmark is submitted (default on).

By default, the title is generated as:

  • When there is only one revision in an MR/PR, the first line of the revision description.
  • When there are multiple revisions in an MR/PR, the name of the bookmark.

The title generation is highly configurable, using the below settings:

SettingDescriptionTypeRequiredDefault
title.syncWhether to sync/update the title of a pull/merge request every time the bookmark is submitted. If enabled, this will overwrite any changes you may have manually made to the title.BooleanNotrue
title.singleRevisionHow to generate the title when an MR has only one revision on top of its parent(s)firstRevisionFirstLine | firstRevisionFullMessage | headRevisionFirstLine | headRevisionFullMessage | bookmarkName | (custom template, see below)NofirstRevisionFirstLine
title.multipleRevisionsHow to generate the title when an MR has multiple revisions on top of its parent(s)firstRevisionFirstLine | firstRevisionFullMessage | headRevisionFirstLine | headRevisionFullMessage | bookmarkName | (custom template, see below)NobookmarkName

You can use custom templates to generate the title of a pull or merge request. While the full power of jj’s template language is planned, at the moment you can use any string with the following placeholders replaced with the corresponding values:

PlaceholderDescription
{first.id}The jj commit/revision ID of the first (bottommost) revision in the PR/MR.
{first.change_id}The jj change ID of the first (bottommost) revision in the PR/MR.
{first.description}The full description of the first (bottommost) revision in the PR/MR.
{first.description_first_line}The first line of the description of the first (bottommost) revision in the PR/MR.
{first.description_not_first_line}The description of the first (bottommost) revision in the PR/MR, excluding the first line (trimmed).
{head.id}The jj commit/revision ID of the head revision in the PR/MR.
{head.change_id}The jj change ID of the head revision in the PR/MR.
{head.description}The full description of the head revision in the PR/MR.
{head.description_first_line}The first line of the description of the head revision in the PR/MR.
{head.description_not_first_line}The description of the head revision in the PR/MR, excluding the first line (trimmed).
{stack_index}The 1-based index of the revision in the stack.
{stack_count}The total number of revisions in the stack.
{bookmark_name}The name of the bookmark.
{parent_bookmark_name}The name of the parent bookmark (i.e. the target branch).

Some examples:

  • {bookmark_name}: {head.description_first_line}

    • feature-a: Add feature A
    • feature-b: Add feature B
  • [{stack_index}/{stack_count}] {first.description_first_line}

    • [1/2] Add feature A
    • [2/2] Add feature B
  • {stack_index}/N: {head.description_first_line} ({bookmark_name} -> {parent_bookmark_name})

    • 1/N: Add feature A (feature-a -> main)
    • 2/N: Add feature B (feature-b -> feature-a)
  • {bookmark_name} → {parent_bookmark_name}: {first.description_first_line}

    • feature-a → main: Add feature A
    • feature-b → feature-a: Add feature B
  • [{head.change_id}] {head.description_first_line}

    • [kxqpmsyz] Add feature A
    • [rlvkpnkp] Add feature B
  • jj-spr heavily for inspiration & code approaches
  • jj-stack heavily for inspiration & code approaches

Don’t worry, I berated Claude with profanity until things looked good.

Nah. It may have started out as a test to see how well Claude Code was (conclusion: meh not great), but large swathes of the code has been rewritten by hand at this point. AI-generated code is so verbose and inelegant at times, often 2x the size of the hand-written code that uses Rust best practices. I’m not against AI coding, nor think it will replace developers. Be measured, people.

There are a decent amount of tests, but there could always be more.

Well primarily, existing tools did not support GitLab (though jj-vine now supports GitLab, GitHub, Forgejo, and Azure DevOps). jj-spr was too heavy-handed - it imposes a strict “one pull request per commit” workflow. jj-stack was in TypeScript (nothing against it, but seems sane for a jj tool to also be built in Rust). My current jj workflow was also just different enough that those existing tools did not fit my needs.

All contributions extremely welcome! Please feel free to open an issue or pull request. See [CONTRIBUTING.djot]!(./CONTRIBUTING.djot) for more details.

[MIT License]!(./LICENSE)