Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: docs/design: git branch mode #4670

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/design/branch-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Branch mode

This is a proposed configuration option to replace
`experimental-advance-branches`. It is not intended to replace or obviate
topics.
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would help me if this document started with a problem statement. Can you list some scenarios that you think we should improve? Here's one I can think of where it should be easy to make jj behave better:

$ jj
@  n [email protected] 2024-10-19 17:22:54 70e302a0
│  (no description set)
○  w [email protected] 2024-10-19 17:05:55 main HEAD@git 873ebde8
│  (empty) (no description set)
○  u [email protected] 2024-10-19 17:05:47 52b0bdfd
│  (empty) (no description set)
◆  z root() 00000000

$ git branch
* main

$ jj desc w -m blah
Rebased 1 descendant commits
Working copy now at: n e270b828 (no description set)
Parent commit      : w a316f16d main | (empty) blah

$ git branch
* (HEAD detached at a316f16)
  main

I.e. we left Git in a "detached HEAD" state where we could have left main checked out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since describe doesn't move @ (it changes the commit ID but not the change ID), it also wouldn't change .git/HEAD in "active branch" mode, since branch mode means that .git/HEAD contains a branch reference, not a commit sha.

(As before, I will add this to the doc, I just wanted to briefly answer your question now.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since describe doesn't move @ (it changes the commit ID but not the change ID), it also wouldn't change .git/HEAD in "active branch" mode, since branch mode means that .git/HEAD contains a branch reference, not a commit sha.

(As before, I will add this to the doc, I just wanted to briefly answer your question now.)

But the Git branch that HEAD@git points to always points to a commit hash, not a change_id, so it does need updating. Or, in terms of bookmarks, the JJ bookmark might still point to the same change_id, but as a newly created commit is associated with the change_id from the bookmark, and the old commit becomes hidden, JJ effectively needs to run jj git export to update the commit hash that git sees for the branch that the bookmark is tracking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposal is currently only for colocated repos. In colocated repos, the export already happens automatically:

Jujutsu will import and export from and to the Git repo on every jj command automatically.

I'll need to learn a little more about how non-colocated repos currently behave before adding them to the design.


The design below assumes that the configuration option is enabled. There should
be no change from current behavior if the option is not enabled.

**"Non-headless"** or **"branch"** mode: a git-colocated repository with the
new configuration option enabled is considered to be in this mode whenever
`.git/HEAD` (hereafter `HEAD`) is populated with a valid `jj` bookmark, and the
bookmark is currently at `@` or `@-`. In this design, the bookmark will be
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, we currently don't allow HEAD to point to @. At the start of the command, we import HEAD and create a new working-copy commit on top of it if it has changed since last time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting; I assumed jj edit <bookmark> would set HEAD to @.

For a merge commit <m>, how does jj edit <m> decide which parent to assign to .git/HEAD? Is it arbitrary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably the first parent, as far as I can remember. So, pretty close to being arbitrary.

referred to as `b(HEAD)`.

**Note:** The only part of this design that is specific to `git` colocation is
the use of `.git/HEAD`. The design could be implemented for any backend that
has a concept of a "current" branch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if we make this work, it should work on non-colocated repos as well. To me, this looks like a proposal of "what if we added the notion of a 'current branch' to jj?". TBH, I'm not sure I'm excited about this -- I'd probably want this to be optional and to keep it off by default -- but it is probably worth thinking through, and my first impression might be wrong.

This makes more sense to me than the experimental advance-branches feature in some ways, but it would also be a much larger change to implement. E.g., you'd need to adjust the log template to show which branch is current, store this information in the repo, etc.

Copy link
Contributor

@ilyagr ilyagr Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also something to be said for fixing the advance-branches feature, if it's possible without storing extra info (which I'm not at all sure about).

I think your original idea was to store the extra info in the Git repo only (by storing a branch in the HEAD ref), but I'm suspicious about this part of it. We probably want it to work with undo, and so it must be stored on the jj side as well.


## Invariants

The configuration option will never affect any of these `jj` behaviors:

* how `@` is updated by each `jj` command
* the way commit topology or contents (ignoring bookmarks) evolve with each
command

## Diagrams

These Excalidraw diagrams show the effect of various `jj` commands when in
branch mode:
[final rendering TBD; see PR-description for updated link]

In these diagrams, a colored arrow is a `jj` bookmark, and a bold and colored
commit-graph node is `b(HEAD)`.

## Entering branch mode

These commands always enter branch mode, by setting `.git/HEAD` appropriately:

```
git init
git clone
bookmark set <name> @
bookmark create <name> @
bookmark set <name> @-
bookmark create <name> @-
```

See also "Changing branches."

## Staying on the same Git branch

When in branch mode, these operations preserve branch mode, and do not change
which bookmark `HEAD` points to:

```
rebase
abandon
commit
new [without specifying revisions]
duplicate
backout
```
Comment on lines +54 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of these commands currently do anything special w.r.t. branches - it's all generic behavior that's implemented at a lower level. I'd really like to keep it that way. That ensures that the behavior is consistent between commands, including for our custom commands at Google, and in operations implemented by various UIs that use the library.

So, can you instead describe the behavior in a way that doesn't refer to a specific command (other than as an example)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to rephrase, but the short version is that these commits aren't doing anything "special." If you've got an active branch, then that branch stays active, and the corresponding jj bookmark moves appropriately, i.e. it moves together with @.


... as well as any commands that don't change the change topology, such as
`status`, `describe`, `diff`, etc.

In branch mode, each of the above commands acts on `b(HEAD)` the same way it
acts on `@`:
* when `@` is `b(HEAD)`, after the command, `b(HEAD)` is set to `@`.
* when `@-` is `b(HEAD)`, after the command, `b(HEAD)` is set to `@-`, unless
that would be ambiguous (for instance, with `jj abandon @-` if the original
`@-` has multiple parents); in case of ambiguity, `b(HEAD)` is set to `@`.

Note: I haven't thought of any cases where this would occur, but it may be that
there is an "obvious" resolution to some otherwise-ambiguous `@-` cases, in
which case we would not need to set `b(HEAD)` to `@`; for instance, if only one
commit in `@-` is a viable candidate for `jj branch set b(HEAD) <rev>` without
`--allow-backwards`, then we would pick that revision.

## Changing branches

These operations may either enter branch mode or change which branch is in
`HEAD`:

```
edit [bookmark]
new [bookmark]
```

With a revision argument that is *not* a bookmark (including `<branch>@git` or
`<branch>@origin`), these commands will always exit branch mode (i.e. leave git
in the "headless" state).

## Merges:

```
new [bookmark1] [bookmark2] ...
```

If none of the bookmarks is `b(HEAD)`, this will simply enter headless mode.

As long as any of the bookmarks is `b(HEAD)`, this will remain in branch mode,
but will *not* update the bookmark to the new commit (even if `@` is initially
`b(HEAD)`). That is, after this operation, `b(HEAD)` will be a member of `@-`.

This will ensure that the next `commit` or `new` (without a revision argument)
will behave as described above: only `b(HEAD)` will be advanced. This means
that, in Git terminology, to merge changes "from" branch (bookmark) A "into"
branch (bookmark) B, the user would first checkout `B` and then use `jj new A B
; jj new`. B would be advanced to the merge commit, while A would not.

## Diffing:

In non-headless mode, `jj diff` (with no revset) would default to `jj diff @
--from b(HEAD)`.