Skip to content

# Using Jujutsu with GitHub and GitLab Projects

This guide assumes a basic understanding of either Git or Mercurial.

## Basic workflow

The simplest way to start with Jujutsu is to create a stack of commits first. You will only need to create a bookmark when you need to push the stack to a remote. There are two primary workflows: using a generated bookmark name or naming a bookmark.

### Using a generated bookmark name

In this example we're letting Jujutsu auto-create a bookmark.

    # Start a new commit off of the default bookmark.
    $ jj new main
    # Refactor some files, then add a description and start a new commit
    $ jj commit -m 'refactor(foo): restructure foo()'
    # Add a feature, then add a description and start a new commit
    $ jj commit -m 'feat(bar): add support for bar'
    # Let Jujutsu generate a bookmark name and push that to GitHub. Note that we
    # push the working-copy commit's *parent* because the working-copy commit
    # itself is empty.
    $ jj git push --change @- # or -c for short

### Using a named bookmark

In this example, we create a bookmark named `bar` and then push it to the remote.

    # Start a new commit off of the default bookmark.
    $ jj new main
    # Refactor some files, then add a description and start a new commit
    $ jj commit -m 'refactor(foo): restructure foo()'
    # Add a feature, then add a description and start a new commit
    $ jj commit -m 'feat(bar): add support for bar'
    # Create a bookmark so we can push it to GitHub. Note that we created the bookmark
    # on the working-copy commit's *parent* because the working copy itself is empty.
    $ jj bookmark create bar -r @- # `bar` now contains the previous two commits.
    # Set the bookmark to be tracked on the remote.
    $ jj bookmark track bar
    # Push the bookmark to GitHub (pushes only `bar`)
    $ jj git push

While it's possible to create a bookmark in advance and commit on top of it in a Git-like manner, you will then need to move the bookmark manually when you create a new commits. Unlike Git, Jujutsu will not do it automatically.

## Updating the repository

Jujutsu does not currently have a direct equivalent of `git pull`, though it may gain a similar command someday.

In the meantime, updating your branches is a two step process. The first is to `jj git fetch` to fetch everything that's happened on your remote. The second is to rebase all of your branches on top of your `main` branch with `jj rebase -o main`. This command's default is the same as passing `-b @`, so if you have more than one outstanding branch, you'll need to call `jj rebase` again with the `-b` flag for each of them, or pass multiple `-b` arguments, one for every branch.

## Working in a Git colocated workspaces

After doing `jj git init`, which colocates the `.jj` and `.git` directories, Git will be in a detached HEAD state, which is unusual, as Git mainly works with named branches; jj does not.

In a colocated workspace, every `jj` command will automatically synchronize Jujutsu's view of the repo with Git's view. For example, `jj commit` updates the HEAD of the Git repository, enabling an incremental migration.

    $ nvim docs/tutorial.md
    $ # Do some more work.
    $ jj commit -m "Update tutorial"
    # Create a bookmark on the working-copy commit's parent
    $ jj bookmark create doc-update -r @-
    $ jj bookmark track doc-update
    $ jj git push

## Working in a Jujutsu repository

In a Jujutsu repository, the workflow is simplified. If there's no need for explicitly named bookmarks, you can just generate one for a change. As Jujutsu is able to create a bookmark for a revision.

    $ # Do your work
    $ jj commit
    $ # Push change "mw", letting Jujutsu automatically create a bookmark called
    $ # "push-mwmpwkwknuz"
    $ jj git push -c mw

## Addressing review comments

There are two workflows for addressing review comments, depending on your project's preference. Many projects prefer that you address comments by adding commits to your bookmark1. Some projects (such as Jujutsu and LLVM) instead prefer that you keep your commits clean by rewriting them and then force-pushing2.

### Adding new commits

If your project prefers that you address review comments by adding commits on top, you can do that by doing something like this:

    $ # Create a new commit on top of the `your-feature` bookmark from above.
    $ jj new your-feature
    $ # Address the comments by updating the code. Then review the changes.
    $ jj diff
    $ # Give the fix a description and create a new working-copy on top.
    $ jj commit -m 'address pr comments'
    $ # Update the bookmark to point to the new commit.
    $ jj bookmark move your-feature --to @-
    $ # Push it to your remote
    $ jj git push

Notably, the above workflow creates a new commit for you. The same can be achieved without creating a new commit.

Warning

We strongly suggest to `jj new` after the example below, as all further edits still get amended to the previous commit.

    $ # Create a new commit on top of the `your-feature` bookmark from above.
    $ jj new your-feature
    $ # Address the comments by updating the code. Then review the changes.
    $ jj diff
    $ # Give the fix a description.
    $ jj describe -m 'address pr comments'
    $ # Update the bookmark to point to the current commit.
    $ jj bookmark move your-feature --to @
    $ # Push it to your remote
    $ jj git push

### Rewriting commits

If your project prefers that you keep commits clean, you can do that by doing something like this:

    $ # Create a new commit on top of the second-to-last commit in `your-feature`,
    $ # as reviewers requested a fix there.
    $ jj new your-feature- # NOTE: the trailing hyphen is not a typo!
    $ # Address the comments by updating the code. Then review the changes.
    $ jj diff
    $ # Squash the changes into the parent commit
    $ jj squash
    $ # Push the updated bookmark to the remote. Jujutsu automatically makes it a
    $ # force push
    $ jj git push --bookmark your-feature

The hyphen after `your-feature` comes from the revset syntax.

## Working with other people's bookmarks

By default, `jj git clone` imports the default remote bookmark (which is usually `main` or `master`), but `jj git fetch` doesn't import new remote bookmarks to local bookmarks. This means that if you want to iterate or test another contributor's bookmark, you'll need to do `jj new <bookmark>@<remote>` onto it.

If you want to import all remote bookmarks including inactive ones, set `remotes.<name>.auto-track-bookmarks = "*"` in the config file. Then you can specify a contributor's bookmark as `jj new <bookmark>` instead of `jj new <bookmark>@<remote>`.

You can find more information on that setting here.

## Using GitHub CLI

GitHub CLI will have trouble finding the proper Git repository path in jj repos that aren't colocated (see issue #1008). You can configure the `$GIT_DIR` environment variable to point it to the right path:

    $ GIT_DIR=.jj/repo/store/git gh issue list

You can make that automatic by installing direnv and defining hooks in a `.envrc` file in the repository root to configure `$GIT_DIR`. Just add this line into `.envrc`:

    export GIT_DIR=$PWD/.jj/repo/store/git

and run `direnv allow` to approve it for direnv to run. Then GitHub CLI will work automatically even in workspaces that aren't colocated so you can execute commands like `gh issue list` normally.

## Useful Revsets

Log all revisions across all local bookmarks that aren't on the main bookmark nor on any remote:

    $ jj log -r 'bookmarks() & ~(main | remote_bookmarks())'

Log all revisions that you authored, across all bookmarks that aren't on any remote:

    $ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'

Log all remote bookmarks that you authored or committed to:

    $ jj log -r 'remote_bookmarks() & (mine() | committer(your@email.com))'

Log all ancestors of the current working copy that aren't on any remote:

    $ jj log -r 'remote_bookmarks()..@'

## Merge conflicts

For a detailed overview, how Jujutsu handles conflicts, revisit the tutorial.

## Using several remotes

It is common to use several remotes when contributing to a shared repository. For example, "upstream" can designate the remote where the changes will be merged through a pull-request while "origin" is your private fork of the project.

    $ jj git clone --remote upstream https://github.com/upstream-org/repo
    $ cd repo
    $ jj git remote add origin git@github.com:your-org/your-repo-fork

This will automatically setup your repository to track the main bookmark from the upstream repository, typically `main@upstream` or `master@upstream`.

You might want to `jj git fetch` from "upstream" and to `jj git push` to "origin". You can configure the default remotes to fetch from and push to in your configuration file (`jj config edit --user|repo|workspace`):

    [git]
    fetch = "upstream"
    push = "origin"

The default for both `git.fetch` and `git.push` is "origin".

If you usually work on a project from several computers, you may configure `jj` to fetch from both repositories by default, in order to keep your own bookmarks synchronized through your `origin` repository:

    [git]
    fetch = ["upstream", "origin"]
    push = "origin"

## Git push options

`jj git push` supports passing Git “push options” to the server via `-o/--option`. These are forwarded to the remote and interpreted by the hosting platform, if it supports them. You can repeat `-o` to send multiple options.

- Syntax: `jj git push -o <push_option>` or `jj git push --option <push_option>`
- Multiple options: `jj git push -o foo -o bar=val`
- Quoting: If the option’s value has spaces, wrap it in double quotes.

Support is server-dependent. GitLab supports push options for CI and merge requests; other platforms may not. Refer to your host’s documentation for the full list and behavior.

Examples with GitLab push options (see GitLab docs for details):

- Skip CI for a push:

      jj git push -o ci.skip

- Pass CI variables to the created pipeline:

      jj git push -o 'ci.variable=MAX_RETRIES=10' -o 'ci.variable=MAX_TIME=600'

- Create a merge request with metadata on push:

      jj git push --allow-new \
        -o merge_request.create \
        -o merge_request.target=main \
        -o 'merge_request.title=Add feature X' \
        -o 'merge_request.description=Implements X with tests' \
        -o merge_request.draft

- Auto-merge when pipeline succeeds and remove the source branch:

      jj git push \
        -o merge_request.merge_when_pipeline_succeeds \
        -o merge_request.remove_source_branch

- Add and remove labels:

      jj git push \
        -o 'merge_request.label=label1' \
        -o 'merge_request.label=label2' \
        -o 'merge_request.unlabel=label3'

- Assign and unassign users:

      jj git push \
        -o 'merge_request.assign=user1' \
        -o 'merge_request.assign=user2' \
        -o 'merge_request.unassign=user3'

1. This is a GitHub-style review, as GitHub currently only is able to compare bookmarks. ↩

2. If you're wondering why we prefer clean commits in this project, see e.g. this blog post ↩