jujutsu
November 10, 2024
EDIT, 2025-12-25: I've gone back to straight git. I think jj will probably be a passing fad that will never have the full functionality as git, nor the documentation/support/community.
This is probably going to be something of an evergreen post, updated as necessary. Unless I forget it exists.
I heard about jujutsu sometime within the last year. I have to admit my initial thought was something along the lines of "why make something else when there's git?" but then I saw that Steve Klabnik (co-author of the Rust book) is writing a tutorial on it. So, as I said on Mastodon, I guess I'm going to have to check it out. (EDIT, 2025-07-23: "Jujustsu for busy devs" is a good intro.)
And so I've been using it on personal projects for the last couple weeks and I quite like it.
Anyway, I wanted to note some things I'm getting used to/figuring out.
Workflow:
- I'm mostly using the "edit" workflow Steve describes, but not quite as he describes it.
- The happy path: When I'm done with some set of changes, I'll update the description if necessary. I'm finding that writing the description before doing any work is helpful in thinking about it before doing it, so that's why I say "if necessary". But half the time it is, and if so it's done with
jj describe(to open an editor to write the subject and optionally body of the message) or withjj describe -m <message>to write it on the command line. Push to remote, possibly. Then runjj newto start a new revision. I don't often use the-m <message>option withjj new, as I'm just trying to finish up the one I've been working on and leave things in a fresh state, but you can do that. (There's also one command that will replacedescribeand thennew:jj commit, orjj cifor short.) - Aw fuck I forgot something:
- if I've already started a new revision with
jj new, just make whatever changes are necessary and then runjj squash. This will push the changes in the working copy into the previous revision, and the working copy will be empty. If you already added a description, an editor will pop up allow you to edit the commit, very much like in rebasing ingit. - if not, run
jj newand thenjj squash. If you just runjj squashwithout starting a new revision, you'll be pushing all your changes both now and what you previously did into the the revision before the one you're attempting to add to. - if you already pushed to a remote, you can do it again, just specify the revision:
jj git push -r <rev> --remote <remote-name>. There's no need (or option) for--force. Just push it. - it's also easy to do this with only some of the changes. I'll add that later.
- if I've already started a new revision with
Pushing to remote: If finished with a revision and want to push it somewhere, don't start a new one (because you can just work on the working copy without having to specify a revision). Update the bookmark with jj bookmark set main to move the main bookmark/branch to the working copy. Then do jj git push --remote <remote-name> to push it there. Then a new revision, to start further work, can be started with jj new.
Various things:
@is the working copy and@-is used for the revision before the working copy. You can pass in the revision on most commands --r <rev>. So-r @-is the one before the working. I'm not sure how far out it goes, but tacking on additional-will go one further.jj showwill show commit description (the full one, not just the subject likejj logdoes) and revision changes. Handy asjj show @-to see previous one from working copy.jj undois pretty great. I fucked up some things and it made them go away.jj abandonis both useful and good naming. Wrote some code that's actually not worth saving?jj abandon.- to use jujutsu out of the gate with a new Rust project (rather than "colocate" it with git), pass
--vcs=nonetocargo newand then runjj git initin the project's directory. - the "builtin_log_compact_full_description" template is the one that feels most like what I expect from
git log. So I've added an alias for it, to "v", which means it can be called withjj v. The new part of my ~/.config/jj/config.toml file looks like this:[aliases] v = ["log", "-T", "builtin_log_compact_full_description"] # v for verbose - Start a branch awhile ago and then just kind of forget about it? And then you're like 30 commits from where you diverged but you want to pick up the old branch again? I'd have to probably read several blog posts and forum threads for git, but for jujutsu it took me just
jj helpand a couple minutes to figure out that the answer is justjj rebase -r [revision] -d @and everything seems ... like I wanted it to be? (-bor-smay be a better choice than-r.jj help rebaseprovides clear explanation and graphs to make the decision easy.)
Diffing
April 15, 2024
There are (at least) two ways to view the diff of both an untracked file and tracked-but-changed/not-staged files in git.
-
Add the untracked file, run
git diff HEAD. If it turns out you don't want that file added, just follow the command shown on withgit status:git restore --staged <file>. -
Use the
-N(--intent-to-add) flag ingit add, which "record[s] only the fact that the path will be added later" (fromgit add --help). As the help continues, "This is useful for, among other things, showing the unstaged content of such files withgit diffand committing them withgit commit -a."
Not sure which way I'll settle on, but I'm glad I finally looked into it. I generally do the first version, but only because I didn't know any other way. And before I end up doing that, I usually think there is some simple flag I'm missing with git diff that'll do what I want, before realizing - once again - that there's not.
git show
April 1, 2024
Occasionally I'm looking at the current code for a project, and want to see some previous iteration of it, often because I want to bring back some part of it. I'll typically go to the code in the software forge, find the commit I think I'm looking for, click the link to view the source at the point in the commit history, and then navigate to the file.
There's an easier way:
git logto find the commit- copy first bunch of characters (6) of the hash for the commit you want to view code from
git show [hash]:relative/path/to/file.
That goes to stdout. Pipe it to an editor, e.g.: git show 3a53e76:src/main.rs | hx. I haven't yet figured out how to set the language of the file when opening with Helix in this way, but that can be solved by setting it once the file is open with :set-language rust.
Syncing Dotfiles
May 9, 2022
For a while I kept some configuration files in Dropbox so I could get to them outside my home computer. I wrote a simple bash script that would move them from their normal locations to a directory in my local Dropbox, and then set up a cronjob to run that script every day. That was ok, but they weren't easily accessible publicly. Or at least not in the way many people share their dotfiles, which is to just have a repo for them.
So I decided to move them from Dropbox to GitHub Codeberg, which presented a small challenge — how to do the commit and push once I collected all the files into a git repository? Here's the simplified bit of bash for that:
git_status=
if ; then
&& &&
fi
If the stdout of running git status doesn't contain "nothing to commit", then it adds all files in the repo, commits with the message "Update", and pushes it. That's not a very meaningful commit message — especially not as the only message in the history after the initial set up — but I'm not particularly concerned with that and more with having the files always up-to-date and accessible.
Another small challenge was with cron. I didn't want to run the script repeatedly all day, but if I just ran it once a day there was a chance my computer wouldn't be on at the time and so the cronjob wouldn't run. Anacron to the rescue! Anacron will run jobs on a regular basis like cron, except that it is aware of the last time jobs ran and will run them again if they haven't run within the specified interval. Anacron isn't installed on Linux distos by default (or at least not Debian and its derivatives), but it's a simple sudo apt install anacron to install it. By default, anacron's configuration file is location at /etc/anacrontab and it tracks jobs run at /var/spool/anacron. I wanted these to be in my user space, so I created those directories/files under ~/.anacron. Here is the part of the config file (~/.anacron/etc/anacrontab) related to this project:
There are two other pieces to this. The first is including this in my ~.profile file, so that anacron runs on startup:
And the second is a regular cronjob that will run anacron every hour (which causes anacron to check if any jobs need to be run, and run them if so):
0 * * * * anacron -t "$HOME/.anacron/etc/anacrontab" -S "$HOME/.anacron/var/spool/anacron"
That's pretty much it. Here's the link to the repo, which includes the full manage_dotfiles bash script.
A Couple Tips on GitHub Actions
July 5, 2021
I work primarily on a Linux machine, and for a while now I've wanted to set up the GitHub actions for my flashcards CLI (now at https://codeberg.org/kdwarn/flashcards) so that the tests can be run on Mac and Windows, because I honestly have no idea if the program will run correctly on those operating systems. The CLI is written in Python and so the main workings of it shouldn't be an issue; I was more concerned with storing and accessing the sqlite database that underpins it. (And in fact was 95% sure that how I entered its path would at least cause a failure on Windows.) Today I finally got around to doing that, although I didn't find the process exactly straightforward. Perhaps that's because I was skimming through the documentation too quickly and trying to find one particular thing, but in any case, here is how I was able to set it up.
I already had a "workflow" set up that ran tests on the Linux virtual environment (Ubuntu) that GH offers, and so I needed to update that. But before I changed that, I wanted to see if I could manually run the workflow, so I didn't have to push a new commit just to have it run. So I changed the code from:
# this is just a snippet of the larger file
on:
push:
branches:
pull_request:
branches:
to:
# this is just a snippet of the larger file
on:
push:
branches:
pull_request:
branches:
workflow_dispatch:
It turned out that was correct, although it wasn't immediate obvious that it was, because when I returned to the main Actions page, I didn't see the "This workflow has a workflow_dispatch event trigger" text with the "Run workflow" next to it, like I should have. I think this is because I did it on a non-main branch and did not yet submit a pull request, though I'm actually not that sure. In any case: keep an eye out, and possibly submit a pull request so that it shows up.
The other thing to point out is that just submitting a pull request, because of that pull_request: section above being in the workflow, triggers the workflow. I wasn't patient enough and left the page before GH could initiate the workflow and show me that it was running. Had I waited just a couple seconds after submitting the pull request, I would have seen this (as I learned subsequently on another). So I didn't even really need to set up the manual running of the workflow - just making a commit and pull request would have triggered it, without needing to commit on main or make a more substantial commit to the actual code of the project. I definitely should have realized this, as I've seen it when I've submitted pull requests to other projects, not to mention that it's specified right there in the workflow configuration. But hey, you sometimes you have to give yourself extra unnecessary busywork in order to realize something in a new context. And plus now I at least know how to set up a workflow so it can be run manually.
Finally, to the whole point of what I was doing. Setting up the additional OS environments was pretty simple. From:
# again, just a portion of the file
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
to:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
python-version: # also added 3.9 here
Rebase and Merge
July 5, 2021
daybook, vcs, python | permalink
Rebasing in git is similar to merging. You do it when you want to combine the changes from one branch into another. (Although you can also do it interactively within one branch to squash commits, which I've done a fair amount since figuring out how that works.) I don't understand the differences between the two strategies, but at least rebase is a little less mysterious. I learned this from Git Gud, a Python program that interactively teaches you git by having you try to do various tasks with it. The explanations of how things work are great (which you can view with git gud explain). (Side note: I don't know how this CLI is able to use git gud as its command - shouldn't git be immediately invoked and tell you it doesn't know wtf the gud subcommand is?) Anyway, also learned from that program what ~ and ^ do (at the end of a branch name or hash). I've used HEAD~ and HEAD~2 or similar before (again, with interactive rebasing), but now I understand what's going on. (~ is for the previous commit, ~N is for the nth previous commit; ^ is similar but for parent branches.)