Working with remotes, e.g., GitHub
We’re going to talk about working with remote git servers, and using GitHub as
an example. The same principles apply to any given git server, though.
Pushing our code to GitHub
The first thing to do is to create the remote server. I have made a GitHub project at https://github.com/steveklabnik/jj-hello-world. I’m adding it as an upstream like this:
> jj git remote add origin git@github.com:steveklabnik/jj-hello-world.git
That’s the direction we need here, because our repository already exists. Going
the other way, jj git clone <url> takes a project that’s already on a server
and makes a jj repository from it. Like jj git init, it gives you a
colocated repository — a .jj directory and a working .git one — so gh and
anything else that reads .git keep working in a clone too.
You may have noticed we’ve been talking about change IDs as if they were
permanent, and they usually are, even across a clone. jj writes each change
ID into a small header on the underlying git commit, so a plain git clone
carries it along too, not just jj git clone — and it survives any number of
repeated clones, pure git included, as long as none of them rewrite the
commit. The one thing that loses it is rewriting the commit along the way, say
with git rebase or a squash-merge on GitHub, since that header isn’t part of
git’s own data model.
Before we push our commit up, we need to fix our repository:
$ jj log --limit 2
@ pzkrzopz steve@steveklabnik.com 2024-03-01 22:41:37 trunk fcf669c5
│ (empty) (no description set)
○ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 f68d1623
│ remove goodbye message
Let’s swap @ back to the previous change, and then abandon this one. We can
do that like this:
$ jj edit @-
Working copy (@) now at: povouosx f68d1623 remove goodbye message
Parent commit (@-) : vvmrvwuz d41c079b refactor printing
$ jj bookmark set trunk --allow-backwards
Moved 1 bookmarks to povouosx f68d1623 trunk | remove goodbye message
$ jj abandon pzkrzopz
Abandoned 1 commits:
pzkrzopz fcf669c5 (empty) (no description set)
$ jj log --limit 2
@ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 trunk f68d1623
│ remove goodbye message
○ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
We need the --allow-backwards flag to set the trunk branch to the previous
commit because it is a dangerous operation: if we had pushed trunk, things
would get weird when we try and push now. We’ve kept it all local, so there’s no
issues with doing this.
Anyway, let’s push trunk up to GitHub. A bare jj git push won’t do it:
jj refuses to create a bookmark on the remote that it doesn’t already know
about, so we have to name it explicitly with -b. We can use --dry-run to
inspect that push before performing it:
$ jj git push
Warning: Refusing to create new remote bookmark trunk@origin
Hint: Run `jj bookmark track trunk@origin` and try again.
Nothing changed.
$ jj git push --dry-run -b trunk
Changes to push to origin:
bookmark: trunk [add to f68d16233bdc]
Dry-run requested, not pushing.
$ jj git push -b trunk
Changes to push to origin:
bookmark: trunk [add to f68d16233bdc]
Warning: The working-copy commit became immutable; a new commit has been created on top of it.
Working copy (@) now at: znurnwmk f853107d (empty) (no description set)
Parent commit (@-) : povouosx f68d1623 trunk | remove goodbye message
Once the bookmark exists on the remote and is tracked, a bare jj git push is
enough for subsequent pushes.
That warning in the middle deserves a word. Remember those ◆ symbols from
back when we first looked at jj log? Pushing trunk just created some more
of them:
$ jj log
@ znurnwmk steve@steveklabnik.com 2024-03-01 18:15:00 f853107d
│ (empty) (no description set)
◆ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 trunk f68d1623
│ remove goodbye message
~
Two things happened. trunk turned into a ◆, and everything below it
vanished behind a ~. That’s not --limit doing it. With no -r, jj log
uses this revset:
present(@) | ancestors(immutable_heads().., 2) | trunk()
Which is to say: the working copy, whatever’s stacked above the immutable commits, and trunk. History you can’t rewrite doesn’t get drawn, on the theory that it’s rarely what you’re looking at. Ask for it explicitly and it’s all still there:
$ jj log -r '::@'
@ znurnwmk steve@steveklabnik.com 2024-03-01 18:15:00 f853107d
│ (empty) (no description set)
◆ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 trunk f68d1623
│ remove goodbye message
◆ vvmrvwuz steve@steveklabnik.com 2024-03-01 17:49:07 d41c079b
│ refactor printing
◆ yykpmnuq steve@steveklabnik.com 2024-03-01 17:07:36 2b93da0c
│ (empty) add better documentation
trunk and everything behind it are now immutable. The idea is simple: other
people can see this history now, so rewriting it would be antisocial. jj
enforces that for you rather than trusting you to remember:
$ jj describe trunk -m "let's rewrite some shared history"
Error: Commit f68d16233bdc is immutable
Hint: Could not modify commit: povouosx f68d1623 trunk | remove goodbye message
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
- https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
- `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.
This is also why we got that warning during the push. Our working copy was
sitting on povouosx, and povouosx had just become immutable, so jj
moved us up to a fresh change on top of it. It didn’t ask, because there’s no
sensible alternative: you can’t keep editing a commit you’re no longer allowed
to edit.
By default the immutable set is trunk(), any tags, and any remote bookmarks
you’re not tracking. It’s a configuration setting, not a law of nature; if you
really do need to rewrite shared history, you can adjust
revset-aliases."immutable_heads()" or pass --ignore-immutable. But the
default is a good default, and I’d leave it alone until you have a specific
reason not to.
And now our project is up on GitHub!
Updating the trunk branch from GitHub
If you collaborate on a project, as commits land on the main branch, you’ll want to update your local copy of that branch. I’m going to make a change in the GitHub UI:
All I did was update the Cargo.toml to remove some comments. If you’re
following along, you can make any change you’d like.
Let’s fetch those changes:
$ jj git fetch
bookmark: trunk@origin [updated] tracked
$ jj log --limit 3
◆ ksrmwuon steve@steveklabnik.com 2024-03-01 23:10:35 trunk e202b67c
│ Update Cargo.toml
│ @ znurnwmk steve@steveklabnik.com 2024-03-01 18:15:00 f853107d
├─╯ (empty) (no description set)
◆ povouosx steve@steveklabnik.com 2024-03-01 18:12:43 f68d1623
│ remove goodbye message
~
In this instance, trunk did move to the new commit: we asked jj to fetch
information from our origin, and so it’s adjusted things to match. However, @
is still at our current commit (ie. the empty commit created by jj git push).
Let’s fix that:
$ jj new trunk
Working copy (@) now at: vmunwxsk be917d2e (empty) (no description set)
Parent commit (@-) : ksrmwuon e202b67c trunk | Update Cargo.toml
Added 0 files, modified 1 files, removed 0 files
Now we’re working ahead of our trunk.
Creating a pull request
On GitHub, pull requests are tied to a branch. But if you’re doing this
jj-native workflow, you aren’t really thinking about branch names. Does this
advantage go away when you start working with pull requests? Not particularly.
Let’s make this empty change we’re on into a real change. Update src/main.rs
with a new comment:
/// A "Hello, world!" program.
///
/// This is the best implementation of this program to ever exist.
/// add documentation for main
fn main() {
print("Hello, world!");
}
// a function that prints a message
fn print(m: &str) {
println!("{m}")
}
Then let’s add a description, and push our change to GitHub so we can make a PR:
$ jj describe -m "add a comment to main"
Working copy (@) now at: vmunwxsk 9410db49 add a comment to main
Parent commit (@-) : ksrmwuon e202b67c trunk | Update Cargo.toml
$ jj git push -c @
Creating bookmark push-vmunwxsksqvk for revision vmunwxsksqvk
Changes to push to origin:
bookmark: push-vmunwxsksqvk [add to 9410db49f9ba]
$ jj log
@ vmunwxsk steve@steveklabnik.com 2024-03-02 08:27:30 push-vmunwxsksqvk 9410db49
│ add a comment to main
◆ ksrmwuon steve@steveklabnik.com 2024-03-01 23:10:35 trunk e202b67c
│ Update Cargo.toml
~
We’ve used jj git push to push code to a git remote before, but the -c
flag is new: we’re asking it to create us a new branch, from the revision @.
And so it did, and gave us the name push-vmunwxsksqvk. This is our change
ID; it’s longer because change IDs are actually longer than what’s been
displayed to us, there’s just never been a reason to show the whole ID, since
any unique prefix works: vmunwxsk is just as much a unique prefix of
vmunwxsksqvk as v is, it’s just not the shortest unique prefix.
We can now make a pull request out of this:
If you’d like to view this PR, you can find it here, though by the time you look at it, some changes will have been made! Even the smallest pull requests get feedback sometimes, and we’re gonna learn two ways of dealing with review comments in the next section.
Working with multiple remotes
By default, jj git push will push to origin, requiring --remote myfork
to push to a fork.
If you wish to push to your fork by default, you can configure it. git.fetch
takes several remotes, git.push takes one:
[git]
fetch = ["origin", "myfork"]
push = "myfork"
Use --repo to set this for one repository, or --user if you expect most of
your repositories to have the same remotes. jj config path --repo will tell
you where the repository’s own config file lives.
There’s more to say about two remotes, and we’ll say it in the chapter on working with a fork.