jj icon indicating copy to clipboard operation
jj copied to clipboard

WIP: docs/design: git branch mode

Open BatmanAoD opened this issue 1 year ago • 6 comments

This evolved from a discussion in Discord.

Rendered

Diagrams (aka very rough sketches)

TBD: follow the blueprint

BatmanAoD avatar Oct 18 '24 06:10 BatmanAoD

I specifically did not draw a diagram of the result of applying backout, because the behavior won't be simple or obvious with or without this configuration option until https://github.com/martinvonz/jj/issues/2802 is resolved.

BatmanAoD avatar Oct 18 '24 06:10 BatmanAoD

Thank you for the writeup!

ilyagr avatar Oct 20 '24 01:10 ilyagr

Writeup Comments / Confusion

Commenting on some parts of the writeup:

These commands always enter branch mode, by setting .git/HEAD appropriately: bookmark set @ bookmark create @-

I don't see how that could work without an extra option:

bookmark create foo @-
bookmark create bar @-
# What if I want HEAD@git->foo, but still create a non-advancing `bar`
# Git equivalent:
git checkout -B foo HEAD
git branch bar

It gets worse if @ would be supported in addition of @- (which would mean that HEAD@git points to @ via indirection, i.e. the working copy, something JJ currently never does), but see below.

When in branch mode, these operations preserve branch mode, and do not change which bookmark HEAD points to: new [without specifying revisions] [...]

Uhm, starting to list commands and the various modes they operate in (sometimes @ is updated, sometimes not) looks like it'd be very error prone. And definitely hard to model by a user (think cognitive load).

In a similar vein, your Changing branches, Merges, Diffing sections also sound like too much magic to me. Read "magic" as: Special rules of JJ operation behind the scenes, that are hard for the majority of users to keep in mind all the time

In particular having @ as auto-advance branch tip is something I don't fully understand. My guess is that this would not work for a colocated repo at all - after all HEAD@git is always a valid commit ID in a non-empty repo (the Git basis for the index and the JJ basis for the working copy @) and the tip of the history as far as git is concerned - think of git commit how it advances in detached-head mode. Related, jj edit 'root()' does not work and tracked bookmarks cannot point to root() either, because '@' and 'root()' are JJ concepts but not valid Git commit hashes.

Take that all with a grain of salt, I don't know the JJ code itself. I am merely pointing out that after several months of JJ use, I still have a hard time wrapping my head around how any of the advance-branch proposals that I have seen so far could work in practice. And therefore, I wonder how newcomers would be expected to model that behavior...

tim-janik avatar Oct 22 '24 02:10 tim-janik

Branch Interoperability

Note, one operation that already makes JJ update HEAD@git point to a Git ref, just a missing one, is this:

⮞ jj new 'root()' && jj log --no-graph -T builtin_log_oneline 
Working copy now at: upqzzzok 3acbda69 (empty) (no description set)
Parent commit      : zzzzzzzz 00000000 (empty) (no description set)
upqzzzok timj 2024-10-22 03:07:01 3acbda69 (empty) (no description set)
zzzzzzzz root() 00000000
⮞ cat .git/HEAD 
ref: refs/jj/root
⮞ cat .git/refs/jj/root
cat: .git/refs/jj/root: No such file or directory

That could just as well be a non-existing refs/heads/master, as is the case with a newly initialized Git repo. Interestingly, this is the one case where jj log does not show a HEAD@git ref.

Considering that JJ will have to work for people with colocated repos for many, many years to come, I think its interoperability with Git branches should be improved.

That means, make JJ properly update HEAD@git, i.e. without going into detached-head mode unless Git already is in detached-head mode.

The one, comparatively simple thing, that I think JJ should do to improve Git interoperability is this:

  • All places that currently update HEAD@git should check if HEAD@git contains a Git branch ref.
  • If so, write the commit hash into the Git branch ref instead of .git/HEAD and keep the ref pointer in .git/HEAD.
  • If JJ has a bookmark, that tracks the Git ref that was just written to, do jj git import <git-head-bookmark>

That is it. For a start. The way to enter the "advance-branch-mode" could be git checkout -B somebranch as long as there is no new JJ command for it. Note that JJ already deals with external commands writing a ref into .git/HEAD when it snapshots, this would just allow it to leave the branch name in .git/HEAD which is quite important when Git repos are shared with other tools, e.g. the Gollum Wiki, or tools operating on a gh-pages branch, etc. Just look for any other tool that operates on a Git repo and you will find that in most cases it will expect a repo that is not in detached-head mode (and has at least 1 commit).

It is really nothing more than, this:

  • Imagine a user that will issue the following command after every JJ operation that modified the repository: git checkout -B master HEAD (or any other branch name) The user could be having that in their shell prompt...
  • The above user would just always create 2 undo steps instead of one:
    @  4abd6660b3dff8ff timj 1 second ago import git refs args: jj --no-pager st
    ○  6b50b1756c3fb8fc timj 2 second ago new empty commit args: jj new
    
    Otheriwse it'd behave like any other colocated JJ repo.

So I am asking, are there any negative side effects to be expected in a JJ repository where the user runs git checkout -B master HEAD after every JJ command? Or should such a repository operate normally? This would just have the added side effect that all other Git-aware programs suddenly know how to deal with the repo and additionally there is a single JJ bookmark that auto-updates...

The Auto-Advance Conundrum

Wether a user needs a branch tip to be advance on DVCS-commit or DVCS-new/checkout/switch depends on the use case and more importantly the mental model the user applies at that moment. This mental model is vital information that JJ lacks and Git has available.

In this aspect, JJ is at a disadvantage compared to Git because:

  • Git pro: Git knows what branch the user mentally means to advance, it's found in .git/HEAD, so it can make that easy.
  • Git con: When .git/HEAD is in detached mode, commits are still possible, but a bunch of other things are hard or impossible (rebase).
  • JJ pro: Naturally operating in detached HEAD mode has caused JJ to make things easy for users in this mode, rebasing, ancestry commit editing etc all work properly and are easy.
  • JJ con: Forcing JJ into branch mode is cumbersome (needed e.g. because you are editing a gitit repo). Not only do you have to manually advance the current master/gh-pages/wiki branch after every JJ command, you also must make sure .git/HEAD points to this one branch again to make any other Git related tool behave. (Depends, most tools always look at .git/HEAD, but some might look only at a particular branch like gh-pages or are configurable like Gollum).

The thing is, JJ doesn't have to be at a disadvantage compared to Git here. The needed information about the user intent (and mental model) is readily available in .git/HEAD. JJ should just not throw it away, instead make as much good use of it as Git does by adding support to advance the ref that .git/HEAD points to. Also, make it easy for users to communicate their intent but do not try to second guess it - the circumstances if and which branch needs advancing change and need to be communicated by users. Instead, provide simple commands to switch between branches and detached-head mode, ideas:

$ jj head                                 # Inspect branch-tip vs detached-head mode
Detached HEAD mode.
Working copy at: rwvmpwzp aba47a45 (empty) (no description set)
Parent commit      : rtlqvrtv a3f650ff (empty) (no description set)
$ jj head foo                             # like: git checkout -B foo
Advancing branch mode, branch: foo
Working copy at: rwvmpwzp aba47a45 (empty) (no description set)
Parent commit      : rtlqvrtv a3f650ff foo | (empty) (no description set)
$ jj head -                               # like: git checkout --detach HEAD
Detached HEAD mode.
Working copy at: rwvmpwzp aba47a45 (empty) (no description set)
Parent commit      : rtlqvrtv a3f650ff foo | (empty) (no description set)

Native JJ Repositories

JJ repos that are not colocated could decide if they "simulate" an internal HEAD@git config setting or not. The needs are definitely not as pressing in this case, because there are no other external tools for which interoperability needs to be preserved. However there is one external factor that is important: The users mental model. For users that have come to expect .git/HEAD updates to properly support ref updates by using colocated repos, it could help to offer the same behavior in case something like a command like `jj head`` would be introduced.

tim-janik avatar Oct 22 '24 03:10 tim-janik

This design proposes keeping git branches in-sync with jj at any given time. If it's possible to do, it's great! But in the meantime it may be easier to implement command which establishes git branch state temporarily.

My usecase:

  • I am totally fine with jj detached-head mode most of the time
  • But I miss some tools which expect git to have current branch, notably "cargo release"

It would be just fine for me to run something like: jj git exec cargo release, where jj git exec:

  • sets current git branch to something
  • calls tool
  • absorbs new commits
  • returns to detached mode

kriomant avatar Oct 23 '24 02:10 kriomant

@kriomant I think you can just write a shell function to do what you want; for instance, in Bash:

with_branch() {
    git checkout --no-guess $1
    shift
    "$@"
    jj git import
    git checkout $(git rev-parse HEAD)
}

Usage would be something like with_branch main cargo release.

The final git checkout is probably not necessary if you don't actually need jj to be immediately back in headless mode.

BatmanAoD avatar Oct 23 '24 03:10 BatmanAoD