Signing Git commits and tags with SSH
You can sign Git commits and tags with SSH keys instead of GPG.
You can sign Git commits and tags with SSH keys instead of GPG.
Jujutsu ( ) is a distributed version control system (like , , , , etc.). However, it is rather unique in that it is -compatible—it uses as a storage layer, meaning you can use it right now on your existing repos without disrupting anyone else. It also provides other features, such as It also clearly has a (currently small but) passionate group of users, which is a good sign of a useful tool. Some of these users strongly dislike , finding its user interface unintuitive and clunky. I, however, am very much not part of that group. I love . Over the years, I’ve curated a configuration that I find a joy to use. I’ve built up years’ worth of muscle memory. As a result, even though I love investing in interfaces , such as new software tools, I haven’t ever felt motivated enough to try . That changed this past week when I learned that Steve Klabnik found so interesting that he’s leaving Oxide to pursue it further . As an experiment, I’ve committed to using only for at least a couple of weeks. As a user, the tutorial that clicked best for me is Steve’s . So, if you’re a fan, I suggest starting there. What I miss from I quickly ran into two features that I wish were supported. First, does not support submodules . Sure, submodules are not great in many cases. However, one very common case I use them for is Zola themes, such as the theme for this blog. My customization lives in my repository, and the theme for the site lives in a submodule. However, it looks like they are working on that actively and with great care. Second, I missed . This one is more minor, because I can always just fall back to to do so. Even in just a few days, I’ve wanted to serialize a patch to a file (i.e., ) or apply some patch I have sitting around with . One example is a patch file I keep around that customizes an environment for debugging purposes. As a -native approach, I’ve been keeping a change separate that I can rebase into my patch series and move out later if needed, but that feels clunkier. Then, there are a handful of more rarely used things that I have not needed yet, and consequently have not figured out equivalents for. For example, I’m not sure about an equivalent for view release notes via tags. With , I used an alias like But, then it seems right now doesn’t support anything but simply listing tags right now. No creation or deletion. Ouch. I’m not sure an equivalent for . Most likely would need to build some template. I don’t have some equivalent of . But again, it’s -compatible, so I can just keep using . I’m sure there will be a long tail of things like this. Steve and others have described as “both simpler and easier than git, but at the same time, it is more powerful.” I’m not sure I fully agree. In some ways, it has better defaults (e.g., , ), but in other ways, it is complex (e.g., revset language). That said, I do see how it is more powerful. You cannot exactly your mental model to directly onto ’s. That said, one of the main things I enjoy at $WORK is that for a repository, ’s default “view” (for lack of a better word) is a branch, whereas ’s default view is much higher level. I find ’s approach more intuitive: it’s always pretty trivial to see what branches/PRs I have ongoing at a glance (i.e., with ). It also means you can easily do fun stuff like rebase all of your branches at the same time, and push them all simultaneously to your remote! This is made even better by the fact that conflicts are first-class objects, meaning you can rebase all your branches, and deal with conflicts later. In a similar vein: emphasizes commits or changes in a way I find pleasing. Even though in I have ways to do similar things, does indeed make them easier. For example, splitting one commit into multiple is easier. In , you’d probably do something like In , just does the more intuitive thing. Instead, the same process looks more like As another example, I’m a huge fan of . It makes folding follow-up changes into an appropriate earlier commit easy and effective. However, it isn’t built into . Meanwhile, ships with , which does the same thing. I found that in , I was already following a workflow close to the squash workflow recommended by ’s creator, Martin. The main thing I still feel clumsy with is named bookmarks (similar to branches). Specifically, I find it hard to remember to the bookmark when I make updates, and how to rebase them without looking up commands. That said, it’s becoming more familiar over time. Much like , I’ve also found that ’s defaults aren’t entirely sufficient for me. I needed to make some tweaks. You can see my up-to-date dotfiles here , but as a snapshot, these are changes I found to be particularly valuable right from the start. First, I strongly prefer over the default viewer. also makes it easy to view with another tool with . For example, I also like using . I added a config from this discussion so doesn’t clear the screen on quit. Second, I like having my conventional commit template automatically populated on . I also have the diff included , which mimics the config I had with using Third, I changed the default bookmark prefix to . Finally, I added a bunch of aliases I use frequently. I use to copy-paste commit descriptions into PR descriptions. I use to view all the logs most relevant to me. This was discovered courtesy of Will Richardson . I use to view all logs. I use and to push all my bookmarks, and rebase all my changes on main, respectively. Finally, I use to move my bookmark to the latest change (I found this one thanks to Shaddy ). I actually find quite fun! Again, I do not think it “solved” any pain points I had with . I was already a very happy user. Ultimately, ’s interface and mental model do feel refreshing enough that I’m likely to continue using it. conflicts as a first-class object no explicit index revset language operation log and powerful undo automatic rebase and conflict resolution
If you have a git repository on a server with ssh access, you can just clone it: You can then work on it locally and push your changes back to the origin server. By default, git won’t let you push to the branch that is currently checked out, but this is easy to change: This is a great way to sync code between multiple computers or to work on server-side files without laggy typing or manual copying. If you want to publish your code, just point your web server at the git repo: … although you will have to run this command server-side to make it cloneable: That’s a lot of work, so let’s set up a hook to do that automatically: Git hooks are just shell scripts, so they can do things like running a static site generator: This is how I’ve been doing this blog for a while now: It’s very nice to be able to type up posts locally (no network lag), and then push them to the server and have the rest handled automatically. It’s also backed up by default: If the server breaks, I’ve still got the copy on my laptop, and if my laptop breaks, I can download everything from the server. Git’s version tracking also prevents accidental deletions, and if something breaks, it’s easy to figure out what caused it.
Just like git, jj offers tiers of configuration that layer on top of one another. Every setting can be set for a single repo, for the current user, or globally for the entire system. Just like git, jj offers the ability to create aliases, either as shortcuts or by building up existing commands and options into new completely new commands. Completely unlike git, jj also allows configuring revset aliases and default templates, extending or replacing built-in functionality. Let’s look at the ways it’s possible to customize jj via configurations. We’ll cover basic config, custom revsets, custom templates, and custom command aliases. Let’s start with some configuration basics. You can globally configure jj to change your name and email based on a path prefix, so you don’t have to remember to set your work email separately in each work repo anymore. Or perhaps you want jj to wait for your editor if you are writing a commit message, but you don’t want jj to wait for your editor to exist if you are editing your jj configuration file. You can ensure that using a scope. I also highly recommend trying out multiple options for formatting your diffs, so you can find the one that is most helpful to you. A very popular diff formatter is , which provides syntax aware diffs for many languages. I personally use , and the configuration to format diffs with delta looks like this: Another very impactful configuration is which tool jj uses to handle interactive diff editing, such as in the or commands. While the default terminal UI is pretty good, make sure to also try out Meld, an open source GUI. In addition to changing the diff editor, you can also change the merge editor, which is the program that is used to resolve conflicts. Meld can again be a good option, as well as any of several other merging tools. Tools like mergiraf provide a way to attempt syntax-aware automated conflict resolution before handing off any remaining conflicts to a human to resolve. That approach can dramatically reduce the amount of time you spend manually handling conflicts. You might even want to try FileMerge, the macOS developer tools built-in merge tool. It supports both interactive diff editing and conflict resolution. Just two more configurations before we move on to templates. First, the default subcommand, which controls what gets run if you just type and hit return. The default is to run , but my own personal obsessive twitch is to run constantly, and so I have changed my default subcommand to , like so: The last significant configuration is the default revset used by . Depending on your work patterns, the multi-page history of commits in your current repo might not be helpful to you. In that case, you can change the default revset shown by the log command to one that’s more helpful. My own default revset shows only one change from my origin. If I want to see more than the newest change from my origin I use to get the longer log, using the original default revset. I’ll show that off later. While the jj template docs are a great reference, they don’t do very much to show off what’s possible by using templates, so we’ll show some examples. The first and most obvious template is the template, which controls how each change is rendered in the log output. The default template is named , and jj comes with a few pre-built template options for the log view, like and . You can see them all by running . Use to try them out and see how they look. If you want to experiment with your own custom log formats, you can provide a template string instead of the name of an existing template. Here’s an example inline template that prints out just the short change ID, a newline, and then the change description: Try out the various documented template properties yourself! Once you’re happy with a template that you’ve tested, you can add it to your config with a name, and then use it by name. Here’s a more complicated example, adapted from @marchyman in the jj Discord, with several of the elements that we’ve discussed so far. This example changes the default command, adding extra options. It also uses a named revset alias, and a named template alias. The named template recreates the regular template from , and uses a revset filter to include the list of changed files in changes that are both mutable (that is, not yet pushed) and also within 3 commits of the end of a branch. By showing up to 7 changes, but the file list for up to 3 mutable changes, the log output becomes more useful, reminding you what files have been changed in the most recent commits that you might want to push. One more template you might want to adjust is the default description, shown when running or for a change that does not yet have a description. If you don’t use a VCS GUI, it can be helpful to see the diff of what is being committed at the same time as you write the commit message. In git, that meant running , but in jj that means adjusting the default description. The jj config docs provide an example template that will replicate that effect, and show you the diff while you write the message. Building on the earlier section where we talked about , creating your own revset aliases is a powerful way to construct views tailored to your personal needs. A built-in revset alias we can use to illustrate this is . In the same way that git requires to push over an existing remote commit, jj requires to edit a commit matched by . (Incidentally, I believe this arrangement is also an example of the way jj’s design is an improvement on git. Instead of deciding to overwrite published commits during a push, you are forced to decide much earlier, during the edit itself, if you are okay with changing a published commit. Anyway, back to revset aliases.) The default revset is , which composes four other revsets together. Let’s look at each one. The revset is simply the primary branch, whether it is named or , wrapped in to remove it if none of those branches exist. The revset is every change that has been given a tag. The revset is exactly what it sounds like: any branch provided by the remote that you have not manually opted in to tracking locally (which is what you would do if you are working on the branch). All three revsets are combined into one overall list with the operator. Those heads are then used to construct the full list of immutable commits, which is every ancestor of those heads. Now that you know how that works, we can change it. For example, perhaps you want the same immutability rules that provides, where commits are immutable once they have been pushed to any remote at all. In that case, you could add this to your config file: With that configuration, jj will extrapolate that it cannot change any commits on the primary branch, all the commits leading up to a tag, and all commits leading up to a named branch in the remote. If you use this revset, jj will stop you from changing commits once you have pushed them to a branch, since you told it to make those immutable. With this power at your disposal, you can change the default revset shown when you run , or you can create your own named revsets for your own purposes. You can see my revset aliases in my dotfiles, and read more about the default aliases in the jj docs. Okay. Now we can talk about command aliases. First up, the venerable . In the simplest possible form, it takes the closest bookmark, and moves that bookmark to , the parent of the current commit. What if you want it to be smarter, though? It could find the closest bookmark, and then move it to the closest pushable commit, whether that commit was , or , or . For that, you can create a revset for , and then tug from the closest bookmark to the closest pushable, like this: Now your bookmark jumps up to the change that you can actually push, by excluding immutable, empty, or descriptionless commits. What if you wanted to allow tug to take arguments, for those times when two bookmarks are on the same change, or when you actually want to tug a different bookmark than the closest one? That’s also pretty easy, by adding a second variant of the tug command that takes an argument: This version of tug works just like the previous one if no argument is given. But if you do pass an argument, it will move the bookmark with the name that you passed instead of the closest one. How about if you’ve just pushed to GitHub, and you want to create a pull request from that pushed bookmark? The command isn’t smart enough to figure that out automatically, but you can tell it which bookmark to use: Just grab the list of bookmarks attached to the closest bookmark, take the first one, pass it to , and you’re all set. What if you just want single commands that let you work against a git remote, with defaults tuned for automatic tugging, pushing, and tracking? I’ve also got you covered. Use to colocate jj into this git repo, and then track any branches from upstream, like you would get from a git clone. Then, you can to find the closest bookmark to , do a git fetch, rebase your current local commits on top of whatever just got pulled, and then show your new stack. When you’re done, just . This push handles looking for a tuggable bookmark, tugging it, doing a git push, and making sure that you’re tracking the origin copy of whatever you just pushed, in case you created a new branch. For another perspective on jj configuration, partly overlapping with this post, check out my JJ Con talk, stupid jj tricks . You can also try reading some jj config files directly, like my jj config , or thoughtpolice’s jj config , or pksunkara’s jj config . Hopefully this tour through jj configuration options has revealed some ways that jj can be used to do more than was possible with only git. Next time, we’ll focus on the ways that jj goes beyond git, offering things that were impractical or even impossible before.
I want to talk about three things that has fundamentally changed my dev-life. There are a lot of things, like ImGUI, that are very amazing and useful but they don’t provide a general solution across many problems. In no particular order… I’ve been using Git since 2010 and it really has changed my dev-life. I’d used version control before that, mainly Perform, SVN and PVSC, but Git felt nice and unobtrusive and I like that everything was local until I pushed to the server. It’s both nice and annoying that you can’t lock files (like art). You can (kind of) with LFS but that feels tacked on and not ready for primetime. Don’t think so? Try explaining installing and using it to a non-technical artist sometime. Git can be frustrating if you’re trying to do anything but the basics. Accidentally check in a secret file months ago and need to scrub it? Good luck with that. There are ways but it requires a lot of Git-Fu. I mainly use a GUI for git ( Fork ) and that takes most of the pain away. I do use the command line, but mostly in automation scripts. Before Markdown became the de-facto standard, I used my own custom format. It worked but wasn’t great and only I understood it. Markdown has it’s issues when you start using the more esoteric features. I’m also annoyed at bold and italics notation. Why is italics and bold is ? Why not and . That would make a lot more sense to me. I also have issue with it’s creator, John Gruber. He is a highly annoying smug Apple Fanboy. His writing was fine in the early days when Apple was #3, but got intolerable as Apple became the 800lb gorilla. It’s changed recently as Apple has snubbed him but I still can’t read anything he writes. But, I like his Markdown. I use JSON for just about every data file format in my games. JSON was created by Douglas Crockford as a notation for Javascript objects. I worked with Doug Crockford at Lucasfilm for several years. I always had a lot of respect for Doug and was somewhat intimidated (in a good way) by him. Doug was also the producer for the Nintendo Maniac Mansion. As much as I love JSON, there are some things about it that annoy me. I dislike that trailing commas are not allowed There is no need for this and it makes writing out valid JSON more complex. I also don’t like you that have to wrap keys names in quotes if they are simple ascii. I wrote a custom JSON parser I use in all my games that relaxes these, but then general JSON readers fail on my data.
This post was originally given as a talk for JJ Con . The slides are also available. Welcome to “stupid jj tricks”. Today, I’ll be taking you on a tour through many different jj configurations that I have collected while scouring the internet. Some of what I’ll show is original research or construction created by me personally, but a lot of these things are sourced from blog post, gists, GitHub issues, Reddit posts, Discord messages, and more. To kick things off, let me introduce myself. My name is André Arko, and I’m probably best known for spending the last 15 years maintaining the Ruby language dependency manager, Bundler. In the world, though, my claim to fame is completely different: Steve Klabnik once lived in my apartment for about a year, so I’m definitely an authority on everything about . Thanks in advance for putting into the official tutorial that whatever I say here is now authoritative and how things should be done by everyone using , Steve. The first jj tricks that I’d like to quickly cover are some of the most basic, just to make sure that we’re all on the same page before we move on to more complicated stuff. To start with, did you know that you can globally configure jj to change your name and email based on a path prefix? You don’t have to remember to set your work email separately in each work repo anymore. I also highly recommend trying out multiple options for formatting your diffs, so you can find the one that is most helpful to you. A very popular diff formatter is , which provides syntax aware diffs for many languages. I personally use , and the configuration to format diffs with delta looks like this: Another very impactful configuration is which tool jj uses to handle interactive diff editing, such as in the or commands. While the default terminal UI is pretty good, make sure to also try out Meld, an open source GUI. In addition to changing the diff editor, you can also change the merge editor, which is the program that is used to resolve conflicts. Meld can again be a good option, as well as any of several other merging tools. Tools like mergiraf provide a way to attempt syntax-aware automated conflict resolution before handing off any remaining conflicts to a human to resolve. That approach can dramatically reduce the amount of time you spend manually handling conflicts. You might even want to try FileMerge, the macOS developer tools built-in merge tool. It supports both interactive diff editing and conflict resolution. Just two more configurations before we move on to templates. First, the default subcommand, which controls what gets run if you just type and hit return. The default is to run , but my own personal obsessive twitch is to run constantly, and so I have changed my default subcommand to , like so: The last significant configuration is the default revset used by . Depending on your work patterns, the multi-page history of commits in your current repo might not be helpful to you. In that case, you can change the default revset shown by the log command to one that’s more helpful. My own default revset shows only one change from my origin. If I want to see more than the newest change from my origin I use to get the longer log, using the original default revset. I’ll show that off later. Okay, enough of plain configuration. Now let’s talk about templates! Templates make it possible to do many, many things with jj that were not originally planned or built in, and I think that’s beautiful. First, if you haven’t tried this yet, please do yourself a favor and go try every builtin jj template style for the command. You can list them all with , and you can try them each out with . If you find a builtin log style that you especially like, maybe you should set it as your default template style and skip the rest of this section. For the rest of you sickos, let’s see some more options. The first thing that I want to show you all is the draft commit description. When you run , this is the template that gets generated and sent to your editor for you to complete. Since I am the kind of person who always sets git commit to verbose mode, I wanted to keep being able to see the diff of what I was committing in my editor when using jj. Here’s what that looks like: If you’re not already familiar with the jj template functions, this uses to combine strings, to choose the first value that isn’t empty, to add before+after if the middle isn’t empty, and to make sure the diff status is fully aligned. With this template, you get a preview of the diff you are committing directly inside your editor, underneath the commit message you are writing. Now let’s look at the overridable subtemplates. The default templates are made of many repeated pieces, including IDs, timestamps, ascii art symbols to show the commit graph visually, and more. Each of those pieces can be overrides, giving you custom formats without having to change the default template that you use. For example, if you are a UTC sicko, you can change all timestamps to render in UTC like , with this configuration: Or alternatively, you can force all timestamps to print out in full, like (which is similar to the default, but includes the time zone) by returning just the timestamp itself: And finally you can set all timestamps to show a “relative” distance, like , rather than a direct timestamp: Another interesting example of a template fragment is supplied by on GitHub, who changes the node icon specifically to show which commits might be pushed on the next command. This override of the template returns a hollow diamond if the change meets some pushable criteria, and otherwise returns the , which is the regular icon. It’s not a fragment, but I once spent a good two hours trying to figure out how to get a template to render just a commit message body, without the “title” line at the top. Searching through all of the built-in jj templates finally revealed the secret to me, which is a template function named . With that knowledge, it becomes possible to write a template that returns only the body of a commit message: We first extract the title line, remove that from the front, and then trim any whitespace from the start of the string, leaving just the description body. Finally, I’d like to briefly look at the possibility of machine-readable templates. Attempting to produce JSON from a jj template string can be somewhat fraught, since it’s hard to tell if there are quotes or newlines inside any particular value that would need to be escaped for a JSON object to be valid when it is printed. Fortunately, about 6 months ago, jj merged an function, which makes it possible to generate valid JSON with a little bit of template trickery. For example, we could create a output of a JSON stream document including one JSON object per commit, with a template like this one: This template produces valid JSON that can then be read and processed by other tools, looks like this. Templates have vast possibilities that have not yet been touched on, and I encourage you to investigate and experiment yourself. Now let’s look at some revsets. The biggest source of revset aliases that I have seen online is from @thoughtpolice’s jjconfig gist, but I will consolidate across several different config files here to demonstrate some options. The first group of revsets roughly corresponds to “who made it”, and composes well with other revsets in the future. For example, it’s common to see a type alias, and a type alias to let the current user easily identify any commits that they were either author or committer on, even if they used multiple different email addresses. Another group uses description prefixes to identify commits that have some property, like WIP or “private”. It’s then possible to use these in other revsets to exclude these commits, or even to configure jj to refuse to push them. Thoughtpolice seems to have invented the idea of a , which is a group of commits on top of some parent: Building on top of the stack, it’s possible to construct a set of commits that are “open”, meaning any stack reachable from the current commit or other commits authored by the user. By setting the stack value to 1, nothing from trunk or other remote commits is included, so every open commit is mutable, and could be changed or pushed. Finally, building on top of the open revset, it’s possible to define a “ready” revset that is every open change that isn’t a child of wip or private change: It’s also possible to create a revset of “interesting” commits by using the opposite kind of logic, as in this chain of revsets composed by . You take remote commits and tags, then subtract those from our own commits, and then show anything that is either local-only, tracking the remote, or close to the current commit. Now let’s talk about jj commands. You probably think I mean creating jj commands by writing our own aliases, but I don’t! That’s the next section. This section is about the jj commands that it took me weeks or months to realize existed, and understand how powerful they are. First up: . When I first read about absorb, I thought it was the exact inverse of squash, allowing you to choose a diff that you would bring into the current commit rather than eject out of the current commit. That is wildly wrong, and so I want to make sure that no one else falls victim to this misconception. The absorb command iterates over every diff in the current commit, finds the previous commit that changed those lines, and squashes just that section of the diff back to that commit. So if you make changes in four places, impacting four previous commits, you can to squash all four sections back into all four commits with no further input whatsoever. Then, . If you’re taking advantage of jj’s amazing ability to not need branches, and just making commits and squashing bits around as needed until you have each diff combined into one change per thing you need to submit… you can break out the entire chain of separate changes into one commit on top of trunk for each one by just running and letting jj do all the work for you. Last command, and most recent one: . You can use fix to run a linter or formatter on every commit in your history before you push, making sure both that you won’t have any failures and that you won’t have any conflicts if you try to reorder any of the commits later. To configure the fix command, add a tool and a glob in your config file, like this: Now you can just and know that all of your commits are possible to reorder without causing linter fix conflicts. It’s great. Okay. Now we can talk about command aliases. First up, the venerable . In the simplest possible form, it takes the closest bookmark, and moves that bookmark to , the parent of the current commit. What if you want it to be smarter, though? It could find the closest bookmark, and then move it to the closest pushable commit, whether that commit was , or , or . For that, you can create a revset for , and then tug from the closest bookmark to the closest pushable, like this: Now your bookmark jumps up to the change that you can actually push, by excluding immutable, empty, or descriptionless commits. What if you wanted to allow tug to take arguments, for those times when two bookmarks are on the same change, or when you actually want to tug a different bookmark than the closest one? That’s also pretty easy, by adding a second variant of the tug command that takes an argument: This version of tug works just like the previous one if no argument is given. But if you do pass an argument, it will move the bookmark with the name that you passed instead of the closest one. How about if you’ve just pushed to GitHub, and you want to create a pull request from that pushed bookmark? The command isn’t smart enough to figure that out automatically, but you can tell it which bookmark to use: Just grab the list of bookmarks attached to the closest bookmark, take the first one, pass it to , and you’re all set. What if you just want single commands that let you work against a git remote, with defaults tuned for automatic tugging, pushing, and tracking? I’ve also got you covered. Use to colocate jj into this git repo, and then track any branches from upstream, like you would get from a git clone. Then, you can to find the closest bookmark to , do a git fetch, rebase your current local commits on top of whatever just got pulled, and then show your new stack. When you’re done, just . This push handles looking for a huggable bookmark, tugging it, doing a git push, and making sure that you’re tracking the origin copy of whatever you just pushed, in case you created a new branch. Last, but definitely most stupid, I want to show off a few combo tricks that manage to deliver some things I think are genuinely useful, but in a sort of cursed way. First, we have counting commits. In git, you can pass an option to log that simply returns a number rather than a log output. Since jj doesn’t have anything like that, I was forced to build my own when I wanted my shell prompt to show how many commits beyond trunk I had committed locally. In the end, I landed on a template consisting of a single character per commit, which I then counted with . That’s the best anyone on GitHub could come up with, too . See? I warned you it was stupid. Next, via on Discord, I present: except for the closest three commits it also shows at the same time. Simply create a new template that copies the regular log template, while inserting a single conditional line that adds if the current commit is inside your new revset that covers the newest 3 commits. Easy. And now you know how to create the alias I promised to explain earlier. Last, but definitely most stupid, I have ported my previous melding of and over to , as the subcommand , which I alias to because it’s inspired by , the shell cd fuzzy matcher with the command . This means you can to see a list of local bookmarks, or to see a list of all bookmarks including remote branches. Then, you can to do a fuzzy match on , and execute . Jump to work on top of any named commit trivially by typing a few characters from its name. I would love to also talk about all the stupid shell prompt tricks that I was forced to develop while setting up a zsh prompt that includes lots of useful jj information without slowing down prompt rendering, but I’m already out of time. Instead, I will refer you to my blog post about a jj prompt for powerlevel10k , and you can spend another 30 minutes going down that rabbit hole whenever you want. Finally, I want to thank some people. Most of all, I want to thank everyone who has worked on creating jj, because it is so good. I also want to thank everyone who has posted their configurations online, inspiring this talk. All the people whose names I was able to find in my notes include @martinvonz, @thoughtpolice, @pksunkara, @scott2000, @avamsi, @simonmichael, and @sunshowers. If I missed you, I am very sorry, and I am still very grateful that you posted your configuration. Last, I need to thank @steveklabnik and @endsofthreads for being jj-pilled enough that I finally tried it out and ended up here as a result. Thank you so much, to all of you.
I’ve been working on a blog post about migrating to jj for two months now. Rather than finish my ultimate opus and smother all of you in eight thousand words, I finally realized I could ship incrementally and post as I finish each section. Here’s part 1: what is jj and how do I start using it? Sure, you can do that. Convert an existing git repo with or clone a repo with . Work in the repo like usual, but with no needed, changes are staged automatically. Commit with , mark what you want to push with , and then push it with . If you make any additional changes to that branch, update the branch tip by running again before each push. Get changes from the remote with with . Set up a local copy of a remote branch with . Check out a branch with , and then loop back up to the start of the previous paragraph for commit and push. That’s probably all you need to get started, so good luck and have fun! Still here? Cool, let’s talk about how jj is different from git. There’s a list of differences from git in the jj docs, but more than specific differences, I found it helpful to think of jj as like git, but every change in the repo creates a commit. Edit a file? There’s a commit before the edit and after the edit. Run a jj command? There’s a commit before the command and after the command. Some really interesting effects fall out of storing every action as a commit, like no more staging, trivial undo, committed conflicts , and change IDs. When edits are always immediately committed, you don’t need a staging area, or to manually move files into the staging area. It’s just a commit, and you can edit it by editing the files on disk directly. Any jj command you run can be fully rewound, because any command creates a new operation commit in the op log. No matter how many commits you just revised in that rebase, you can perfectly restore their previous state by running . Any merge conflict is stored in the commit itself. A rebase conflict doesn’t stop the rebase—your rebase is already done, and now has some commits with conflicts inside them. Conflicts are simply commits with conflict markers, and you can fix them whenever you want. You can even rebase a branch full of conflicts without resolving them! They’re just commits. (Albeit with conflict markers inside them.) Ironically, every action being a commit also leads away from commits: how do you talk about a commit both before and after you amended it? You add change IDs. Changes give you a single identifier for your intention, even as you need many commits to track how you amended, rebased, and then merged those changes. Once you’ve internalized a model where every state is a commit, and change IDs stick around through amending commits, you can do some wild shenanigans that used to be quite hard with git. Five separate PRs open but you want to work with all of them at once? Easy. Have one commit that needs to be split into five different new commits across five branches? Also easy. One other genius concept jj offers is revsets . In essence, revsets are a query language for selecting changes, based on name, message, metadata, parents, children, or several other options. Being able to select lists of changes easily is a huge improvement, especially for commands like log or rebase. For more about jj’s design, concepts, and why they are interesting, check out the blog posts jj strategy , What I’ve Learned From JJ , jj init , and jj is great for the wrong reason . For a quick reference you can refer to later, there’s a single page summary in the jj cheat sheet PDF . Keep an eye out for the next part of this series in the next few days. We’ll talk about commands in jj, and exactly how they are both different and better than git commands.
Many programmers would admit this: our knowledge of Git tends to be pretty… superficial. “Oops, what happened? Screw that, I’ll cherry pick my commits and start again on a fresh branch”. I’ve been there. I knew the basic use cases. I even thought I was pretty experienced after a hundred or so resolved merge conflicts. But the confidence or fluency somehow wasn’t coming. It was a hunch: learned scenarios, commands from Stack Overflow or ChatGPT, trivia-like knowledge without a solid base. In software engineering, you don’t need to have all the knowledge : you just need to quickly identify and fetch the missing bits of knowledge . My goal is to give you that low-level grounding to sharpen your intuition. Git isn’t really complicated in its principles! Disclaimer: I am not a Git expert either. Let’s learn together. Do you know how commit hashes are generated? I have to admit, I thought for a while that those hashes were somehow randomized. After all, I can run , change nothing, and still get the same commit, but with a new hash, right? Likewise, ing the same commit onto another branch gives me yet another hash. Boy, I couldn’t be more wrong. The commit hash is literally just a SHA-1 checksum of the information that constitutes the commit. So two identical commits have identical hashes. Let’s look what a commit consists of. Run the following command: (In case you don’t know: HEAD resolves to the commit you currently checked out) . Let’s call the output of this command the payload. For example, the payload might be: That’s it. That’s the full commit. Then prepend the following null-terminated string to the : “ ”, where is the size of the payload in bytes. Compute a SHA-1 over the result and boom: you’ve got a Git commit hash! Try it yourself: Now compare the output to the actual commit hash: It works. So simple. Now, let’s ponder what the contains: We are not hashing the diff a commit introduces. Rather, the commit header , together with the referenced tree and parent , determines the hash. And now it’s easy to see what happens when you run and change “nothing”. Something still changes: the date in the field! (Note that doesn’t display the committer; the date you see comes from the author field). But if you are fast enough to amend within the same second as the original commit, the commit hash remains unchanged! And on a , the parent field changes, and usually, though not always, the tree field as well. If you’re a careful reader, you might wonder what the parent field is for the first commit in a repo, and for a merge commit. What do you think? Grab a repo and verify. We saw that a commit references a tree. Let’s check what it really is: Oops, the isn’t human-readable text; it’s binary data. But just like with commits, if you prepend “ ” to the payload bytes, you can compute the tree’s hash from the result! Fortunately, Git lets you pretty-print a tree’s contents: A is just like a directory: it references other files (blobs) and directories (trees) nested inside it. It looks a bit like ls output. The first column records, of course, the Unix file permissions. Nothing more, nothing less than the raw file content – no metadata. And yes, prepend null-terminated “ ” to the bytes, run sha1sum, and you’ll get the blob’s hash! No extra metadata such as file modification time: that can be inferred from commit history. A simple and immutable structure : you can’t change a commit without changing its hash. And if you think about it, you will notice that it is a… There are three types of nodes in this graph: commits, trees, and blobs. And four types of edges: Interestingly, the graph fragment reachable from a tree node doesn’t have to form a strict tree. For example, a single blob can be referenced by multiple parents. As you probably know, a branch is just a ref pointing to a commit hash. If you run this in your repo root, you’ll see all local branches as file names, each file just a few bytes, with the referenced commit’s hash inside. Likewise, directory contains pointers to the remote-tracking branches. So you can think of branches as labels for commit histories. If you commit on main: And the file contains the name of the current branch – or commit hash, if you’re in a detached state. This special pointer tells Git what is currently checked out. I hope this clarifies your mental model and clears some of the mystery around Git. The building blocks are simple. Now you shouldn’t have a problem answering questions such as: In the next articles, I plan to cover more advanced concepts, such as Git object storage, garbage collection, and how the default merge strategy works. If you have a little more time and want to keep going, I recommend a few resources: – the hash of a tree object. More on trees later; for now, think of it as a snapshot of all files in the repo. – a hash of parent commit(s). , – self-explanatory, but notice that they include date (seconds since the Unix epoch) and time zone; in several scenarios it’s possible that the author is not the committer . the commit message. commit -> commit – parent relationship; a commit has zero or more parents (usually one). commit -> tree – each commit points to exactly one tree (a snapshot of files and folders). tree -> tree – subdirectory relationship. tree -> blob – files contained in a directory. the new commit will have the hash pointed to by as its parent field; then the branch label will be updated to point to the new commit’s hash. How are Git commit hashes generated? Why does rebasing produce different commit hashes? Can a remote-tracking branch update without your local branch updating? Which data structure represents the repository? What are the node and edge types in this DAG, and how do they relate? Pro Git Book : very practical, but it doesn’t lack depth; look at the Git Internals section. Git for Computer Scientists by Tommi Virtanen; short and sweet: this is where I got the DAG analogy.
I’ve been experimenting with git for a few months now , and I’ve been finding it both useful and enjoyable. It’s nice to learn something new, with a purpose in mind. For keeping dot files available across machines, in circumstances where Nextcloud is not a great option, I’ve been using git on a locked-down, private server. I commit locally, on whatever machine I make the change, and then push to the remote git server. This is just plain git, with no web front end or anything. Since it is just for me, and since I don’t want to expose it to the Internet, this has been fine, and it works well. I also wanted a git server with a web interface, which I can use for public-facing stuff. For that, I went with forgejo. I am always hesitant to say that installing and running something is easy, but that is subjective, but, for me, so far, it has been a very pleasant experience. At the moment, I am only using it for hosting some documentation - some sample terms for fedi instances - rather than code. I like it, and it is easy for me to use, pushing to it from a terminal. I am running it on a tiny container on a proxmox server; I might need to beef it up a little, but so far, it is doing pretty well, even in the face of sustained distributed traffic when I post links to it to the fediverse. I was pleased to see so many options for importing from other forges, including GitHub. As it turns out, I have not bothered to move anything from GitHub, as I’ve nothing there that I actually value or which I think would be of use to others. I’ll probably just delete those repos. But I very much like it that the option is there, and simple. Being my own instance, I am the only user of it, and, sadly, forgejo’s federated functionality - which would enable a user on another foregjo instance to make pull requests, raise issues etc. - is not there yet. So, for now, it is a bit isolated: it is there, on the web, but no-one else can raise issues and the like, and that feels like a shame for a collaboration platform. Perhaps I just didn’t think this through well enough when I picked a self-hosted instance of forgejo; I might have been better going with GitLab, or the like. I am always nervous about putting private information on a server accessible from the Internet. I have a Nextcloud instance, but it is not accessible from the Internet, even though I could see advantages to that. I lock down my mail server, so that one cannot log into IMAP over the Internet. I don’t expose a webmail interface. So, for now, I have not experimented with private repositories, and I am sticking with the separate git server (mentioned above) for things which I want to keep to myself. Perhaps I worry too much, and perhaps I should have some more faith in both my own sysadmin skills, and also the security of the services themselves. But, for now, separation seems like a better plan.
If you are the kind of person who has bothered to click the link to read this, this may already be obvious. But it stumped me for a bit, so I’m putting it here in case it helps others. I run an instance of LiberaForms . I like it very much. I was keen to upgrade to v4.2.0 in particular, because of its support for conditional form fields . When I followed the upgrade process in the blog post, I ran: and got the error The problem, it turns out, is that LiberaForms used to use Gitlab for source distribution / development, but has recently moved to Codeberg. I had installed LiberaForms from Gitland, so my was: v4.2.0 is not on Gitlab, but on their new choice of forge, Codeberg. I changed to: And re-ran . That worked, as did the rest of the upgrade instructions. There might be a better / proper way of changing an origin than hand-editing the config file, but this worked for me. Update : thanks to the kind person who suggested .
I'm posting this from a very, very rough cut at a bespoke blogging client I've been having my friend Claude build out over the past couple days. I've long suspected that "just edit text files on disk to make blog posts" is, to a certain kind of person, a great sounding idea...but not actually the way to get me to blog. The problem is that my blog is...a bunch of text files in a git repository that's compiled into a website by a tool called "Eleventy" that runs whenever I put a file in a certain directory of this git repository and push that up to GitHub. There's no API because there's no server. And I've never learned Swift/Cocoa/etc, so building macOS and iOS tooling to create a graphical blogging client has felt...not all that plausible. Over the past year or two, things have been changing pretty fast. We have AI agents that have been trained on...well, pretty much everything humans have ever written. And they're pretty good at stringing together software. So, on a whim, I asked Claude to whip me up a blogging client that talks to GitHub in just the right way. This is the very first post using that new tool, which I'm calling "Post Through It." Ok, technically, this is the fourth post. But it's the first one I've actually been able to add any content to.
To clone a specific branch of a git repository without cloning all other branches, use the following command formula: For example, if you want to clone the branch of the Kubernetes GitHub repository , run: If you only want to clone the latest commit of a specific branch (which results in a faster and smaller cloning operation) use . The command formula looks like this: And here is another example using the branch of the Kubernetes GitHub repository : https://www.freecodecamp.org/news/git-clone-branch-how-to-clone-a-specific-branch/ https://git-scm.com/docs/git-clone
Back in 2017, I got tired of manually checking and creating git tags.
I’ve written previously about using to interactively stage changes . But did you know that you can use (aka ) to similar effect with other Git commands? Let’s take a look… is great for temporarily stashing changes that you want to apply later, and handily it also supports selectively stashing changes with the flag: Bonus tip: you can also selectively stash entire files using to disambiguate the command from the paths you want stashing: You can use the command to discard local changes and restore files to their last committed state. It can also be called with the flag to interactively select specific hunks to discard: Note the different phrasing of the prompt on the last line: Here we are choosing the changes we want to discard . Be careful, this is a destructive change, and because these are unstaged and uncommitted changes git won’t be able to help you recover the changes once they’ve been discarded!
There’s an old saying: There are only two hard things in Computer Science: cache invalidation and naming things. ― Phil Karlton I also appreciate the joke version that adds “and off by one errors.” Lately, I’ve been thinking about this saying, combined with another old joke: “The patient says, “Doctor, it hurts when I do this.” The doctor says, “Then don’t do that!” ― Henny Youngman Specifically, if naming things is so hard… why do we insist on doing it all the time? Now, I am not actually claiming we should stop giving things names. But I have had at least two situations recently where I previously found names to be kinda critical, and then I changed to systems which didn’t use names, and I think it improved the situation. One of the most famous examples of not giving something a name, lambdas/closures, took some time to catch on. But many folks already recognize that naming every single function isn’t always neccesary. I wonder if there are more circumstances where I’ve been naming things where I didn’t actually have to. Anyway, here’s my two recent examples: I haven’t written much about it on my blog yet, but I’m fully converted away from git to jj . I’ll say more about this in the future, but one major difference between the two is that jj has anonymous branches. If, like me, you are a huge fan of git, this sounds like a contradiction. After all, the whole thing about branches are that they’re a name for some point in the DAG. How do you have a nameless name? Here’s some output from : Here’s some sample output from jj log: Here, we are working on change . ( means the working copy.) There are colors in the real CLI to make the differences more obvious, and to show you unique prefixes, so for example, you probably only need or instead of to uniquely identify the change. I’ll use the full IDs since there’s no syntax highlighting here. We have two anonymous branches here. They have the change IDs of and . The log output shows the summary line of their messages, so we can see “create hello and goodbye functions” on one branch, and “add better documentation” on the other. You don’t need an additional branch name: the change ID is already there. If you want to add even more better documentation, (or again, likely in practice) and you’re off to the races. (jj new makes a new change off of the parent you specify.) (And if you’re in a larger repo with more outstanding branches, you can ask to show specific subsets of commits. It has a powerful DSL that lets you do so. For example, say you only want to see your commits, can do that for you.) That’s all there is to it. We already have the commit messages and IDs, giving an additional identifier doesn’t help that much. In practice, I haven’t missed named branches at all. And in fact, I kind of really appreciate not bothering to come up with a name, and then eventually remembering to delete that name once the PR lands, stuff like that. Life is easier. Another technology I have learned recently is tailwind . But Tailwind is just one way of doing a technique that has a few names, I’m going to go with “utility CSS”. The idea is in opposition to “semantic CSS.” To crib an example from a blog post by the author of Tailwind (which does a better job of thoroughly explaining why doing utility CSS is a good thing, you should go read it), semantic CSS is when you do this: Whereas, with Tailwind, you end up instead having something like this: We don’t have a new semantic name , but instead describe what we want to be done to our element via a utility class. So the thing is, as a previous semantic CSS enjoyer, this feels like using inline styling. But there’s a few significant differences. The first one is, you’re not writing plain CSS, you are re-using building blocks that are defined for you. The abstraction is in building those utility classes. This means you’re not writing new CSS when you need to add functionality, which to me is a great sign that the abstraction is working. It’s also that there is some sleight of hand going on here, as we do, on another level. An objection that gets raised to doing things this way is “what happens when you need to update a bunch of similar styles?” And the answer for that is components. That is, it’s not so much that utility CSS says that semantic names are bad, it’s that semantic names at the tag level are the wrong level of abstraction to use names. To sort of mix metaphors, consider the lambda/closure example. Here’s a random function in Rust: The is unfortunate, but this function takes a list of numbers, selects for the even ones, and then sums them. Here, we have a closure, the argument to , but it’s inside a named function, . This is what using Tailwind feels like to me, we use names for higher level concepts (components), and then keep things semantically anonymous for some of the tasks inside of them (markup). Heck, even the most pro-semantic-styles folks don’t advocate that you must give every single element a class. Everyone recognizes the value of anonymous things sometimes, it’s just a matter of what level of abstraction deserves to get named. Here’s my post about this post on BlueSky: Against Names: steveklabnik.com/writing/agai... Against Names
I needed to figure out how to debug the binary to figure out exactly what it was doing, and I thought I'd share the setup because it took me a little while to get going. make a directory: open the git directory with visual studio code: create a file in the directory called that tells vs code how to start for debugging. Its contents should be: Open the file in the editor Set a breakpoint on the first line inside the function; in my current version of git, that's located at line 1504 Switch to the tab of VS code click on , the green arrow located at the top left of the window, or press F5 to do the same thing. If all goes well, you should now be controlling the execution of git inside VS code! Go ahead and dig in to try and understand what's going on. I might write more about what it is that's going on in there, but I'm not sure - let me know if you would like to understand some part of it better.
Today I want to talk about jujutsu , aka , which describes itself as being “a Git-compatible VCS that is both simple and powerful”. This is selling itself short. Picking up has been the best change I’ve made to my developer workflow in over a decade. Before , I was your ordinary git user. I did things on Github and knew a handful of git commands. Sometimes I did cherry picks. Very occasionally I’d do a non-trivial rebase, but I had learned to stay away from that unless necessary, because rebasing things was a perfect way of fucking up the git repo. And then, God forbid, I’d have to re-learn about the reflog and try to unhose myself. You know. Just everyday git stuff. What I hadn’t realized until picking up was just how awful the whole git experience is. Like, everything about it sucks. With git, you need to pick a branch name for your feature before you’ve made the feature. What if while doing the work you come up with a better understanding of the problem? With git, you can stack PRs, but if you do, you’d better hope the reviewers don’t want any non-trivial changes in the first PR, or else you’ll be playing commit tag, trying to make sure all of your branches agree on the state of the world. With git, you can do an interactive rebase and move things relative to a merge commit, but you’d better make sure you know how works, or else you’re going to spend the next several hours resolving the same conflicts across every single commit from the merge. We all know our commit history should tell the story of how our code has evolved. But with git, we all feel a little bit ashamed that our commit histories don’t , because doing so requires a huge amount of extra work after the fact, and means you’ll probably run into all of the problems listed above. Somehow, that’s just the state of the world that we all take for granted. Version control Stockholm syndrome. Git sucks. And jujutsu is the answer. The first half of this post is an amuse bouche to pique your interest, and hopefully convince you to give a go. You won’t regret it. The second half is on effective strategies I’ve found for using in my day to day job. In git, the default unit of work is a “commit.” In , it’s a “change.” In practice, the two are interchangeable. The difference is all in the perspective. A commit is a unit of work that you’ve committed to the git log. And having done that, you’re committed to it. If that unit of work turns out to not have been the entire story (and it rarely is), you must make another commit on top that fixes the problem. The only choice you have is whether or not you want to squash rebase it on top of the original change. A change, on the other hand, is just a unit of work. If you want, you can pretend it’s a commit. But the difference is that you can always go back and edit it. At any time. When you’re done, automatically rebases all subsequent changes on top of it. It’s amazing, and makes you feel like a time traveler. Let’s take a real example from my day job. At work, I’m currently finessing a giant refactor, which involves reverse engineering what the code currently does, making a generic interface for that operation, pulling apart the inline code into instances of that interface, and then rewriting the original callsite against the interface. After an honest day’s work, my looked something like this: This is the version of the . On the left, we see a (linear) ascii tree of changes, with the most recent being at the top. The current change, marked with has id and description . I’m now ready to add a new change, which I can do via : I then went on my merry way, rewriting the second callsite. And then, suddenly, out of nowhere, DISASTER. While working on the second callsite, I realized my original abstraction didn’t actually help at callsite 2. I had gotten the interface wrong. In git land, situations like these are hard. Do you just add a new commit, changing the interface, and hope your coworkers don’t notice lest they look down on you? Or do you do a rebase? Or do you just abandon the branch entirely, and hope that you can cherry pick the intermediary commits. In , you just go fix the change via : and then you update your interface before jumping back ( ) to get the job done. Honestly, time traveler stuff. Of course, sometimes doing this results in a conflict, but is happy to just keep the conflict markers around for you. It’s much, much less traumatic than in git. Branches play a much diminished role in . Changes don’t need to be associated to any branch, which means you’re usually working in what git calls a detached head state. This probably makes you nervous if you’ve still got the git Stockholm syndrome, but it’s not a big deal in . In , the only reason you need branches is to ship code off to your git-loving colleagues. Because changes don’t need to be associated to a branch, this allows for workflows that git might consider “unnatural,” or at least unwieldy. For example, I’ll often just do a bunch of work (rewriting history as I go), and figure out how to split it into PRs after the fact. Once I’m ~ten changes away from an obvious stopping point, I’ll go back, mark one of the change as the head of a branch , and then continue on my way. This marks change as the head of a branch , but this action doesn’t otherwise have any significance to ; my change tree hasn’t changed in the slightest. It now looks like this: where the only difference is the line . Now when sends this off to git, the branch will have one commit for each change in . If my colleagues ask for changes during code review, I just add the change somewhere in my change tree, and it automatically propagates downstream to the changes that will be in my next PR. No more cherry picking. No more inter-branch merge commits. I use the same workflow I would in that I would if there weren’t a PR in progress. It just works. It’s amazing. The use and abuse of the dev branch pattern , makes a great argument for a particular git workflow in which you have all of your branches based on a local branch. Inside of this branch, you make any changes relevant to your local developer experience, where you change default configuration options, or add extra logging, or whatever. The idea is that you want to keep all of your private changes somewhere organized, but not have to worry about those changes accidentally ending up in your PRs. I’ve never actually used this in a git workflow, but it makes even more sense in a repository. At time of writing, my change tree at work looks something like the following: Here you can see I’ve got quite a few things on the go! , and are all branched directly off of , which correspond to PRs I currently have waiting for review. and are stacked changes, waiting on to land. The ascii tree here is worth its weight in gold in keeping track of where all my changes are. You’ll notice that my branch is labeled as , which is to say it’s a change with no diff. But even so, I’ve found it immensely helpful to keep around. Because when my coworkers’ changes land in , I need only rebase on top of the new changes to , and will do the rest. Let’s say now has conflicts. I can just go and edit to fix the conflicts, and that fix will be propagated to and !!!! YOU JUST FIX THE CONFLICT ONCE, FOR ALL OF YOUR PULL REQUESTS. IT’S ACTUALLY AMAZING. In , sets of changes are first class objects, known (somewhat surprisingly) as revsets. Revsets are created algebraically by way of a little, purely functional language that manipulates sets. The id of any change is a singleton revset. We can take the union of two revsets with , and the intersection with . We can take the complement of a revset via . We can get descendants of a revset via , and its ancestors in the obvious way. Revsets took me a little work to wrap my head around, but it’s been well worth the investment. Yesterday I somehow borked my change (????), so I just made , and then reparented the immediate children of over to in one go. You can get the children of a revset via , so this was done via . Stuff like that is kinda neat, but the best use of revsets in my opinion is to customize the experience in exactly the right way for you. For example, I do a lot of stacked PRs, and I want my to reflect that. So my default revset for only shows me the changes that are in my “current PR”. It’s a bit hard to explain, but it works like an accordion. I mark my PRs with branches, and my revset will only show me the changes from the most immediate ancestral branch to the most immediate descendant branch. That is, my log acts as an accordion, and collapses any changes that are not part of the PR I’m currently looking at. But, it’s helpful to keep track of where I am in the bigger change tree, so my default revset will also show me how my PR is related to all of my other PRs. The tree we looked at earlier is in fact the closed version of this accordion. When you change to be inside of one of the PRs, it immediately expands to give you all of the local context, without sacrificing how it fits into the larger whole: The coolest part about the revset UI is that you can make your own named revsets, by adding them as aliases to . Here’s the definition of my accordioning revset: You can see from that we always show (the current edit), all of the named bases (currently just , but you might want to add ), and all of the named branches. It then shows everything from to , which is to say, the changes in the branch leading up to , as well as everything from to the beginning of the next (stacked) branch. Finally, we show all the leafs of the change tree downstream of , which is nice when you haven’t yet done enough work to consider sending off a PR. Jujutsu is absolutely amazing, and is well worth the four hours of your life it will take you to pick up. If you’re looking for some more introductory material, look at jj init and Steve’s jujutsu tutorial
Update : I’ve since moved on to stagit for my public-facing repositories. The repositories are still found at git.garrido.io , and can be cloned using the dumb http protocol . It’s been a couple of months since I started hosting my own software forge . Since then I’ve used it as a remote for all of my git repositories, as a registry for container images, to store gists , and for continuous integration (CI). I chose Forgejo because, for my use case, it has feature parity with Github, while also being open-source, lightweight, and simple to operate. It also happens to be governed by its contributors, and is stewarded by Codeberg , non-profit organization. Unlike Github, I’m not using Forgejo to collaborate on other people’s projects. My server is a single-user instance. This isn’t much of a problem for me as my main motivation to host a software forge is not social. However, something particularly exciting about Forgejo is that federation is being implemented via ForgeFed , an ActivityPub extension for software forges: ForgeFed is a federation protocol for software forges and code collaboration tools for the software development lifecycle and ecosystem. This includes repository hosting websites, issue trackers, code review applications, and more. ForgeFed provides a common substrate for people to create interoperable code collaboration websites and applications. What’s great about version control systems like git is that it is easy to move code from one remote to another as one deems approriate. Yet, tangential artifacts that are equally as important are not as portable or accessible. Initiatives like ForgeFed pave the way to a world in which we’re not dependent on a centralized entity whose interests and incentives may no longer align with one’s own. Hosting Forgejo has been uneventful and virtually maintenance-free. The setup was quick, though configuring Forgejo Runners did require some trial and error as some parts of the documentation were not entirely clear to me. With regards to performance, Forgejo does run lightly. A quick glance at Grafana tells me that in the past seven days it has been using approximately 150MB of memory and 1-2% of CPU on a low-powered VPS. Such footprint would be even smaller if I were not using this for CI. Like all other services that I self host, Forgejo and Forgejo Runners run in Docker containers. This adds some overhead but that’s the tradeoff I choose to keep things simpler on the host 1 . I use Tailscale to access the server privately over SSH while also exposing the public repositories through a proxy on a separate public server. As for backups, Forgejo has a helpful command to export its files and SQLite database. I use this to run automated backups using Restic . When I decide to upgrade versions I read their well-written release notes , bumps the container’s image, and run an Ansible playbook that runs a backup, restarts the containers, and migrates the database if necessary. It’s worth pointing out that Forgejo just released their first Long Term Support release so the maintenance burden could be eased further by sticking with the LTS version and applying minor updates. Forgejo is written in Go so running it on “bare metal” is as simple as it gets, but I still use a container to keep logging, deployments, and configuration management consistent across many services that I host on a single VPS. ↩︎ Forgejo is written in Go so running it on “bare metal” is as simple as it gets, but I still use a container to keep logging, deployments, and configuration management consistent across many services that I host on a single VPS. ↩︎
My friend harper built a neat tool to let the llm draft your commit messages . I ended up tweaking his code a bit for my own use. I wanted commit messages that were a bit more...boring. And I wasn't really happy with how the original code put together the 'why' of the commit message. I made changes to harper's prompt to try to rein things in just a bit and also hacked up his git hook to tweak the model's "temperature" and to provide full-file context to the llm to give it a bit more to go on when writing a commit message. I find that with this setup, I do need to edit my commit messages about 90% of the time, but that those edits are relatively minor and that I'm ending up with better, more detailed commit messages. (Also, I am generally much more productive when I'm hacking up something bad than when I'm staring at a blank screen.) Although, having said that, when I added the first version of this post to the git repo for my blog, it generated this commit message, which I accepted unchanged: You should refer to harper's post for context about what the heck this tool does and also how to set it up. My script: My prompt lives inside llm's "prompt templates" feature at
Inspired by Julia Evans' posts on git , I'm jotting down an obscure trick to combine repos. Sometimes you have multiple repos, and you want to create a monorepo out of them. Perhaps you have a distribution of many components, and it's convenient to across all of them together. The following annotated snippet creates two repos and joins them together. The key insight is that a commit in git is made up of (roughly) a message, pointers to parent commits, and a "tree," the latter of which can be made synthetically by using plumbing commands. This approach is probably overkill for a one time merging of two repos. In that case, create a commit in your second repo that moves everything to a subdirectory, add the second repo as a remote, and merge in that second repo using the . Rendered another way (with from and questionable abuse of ): If you're doing this for real, note that the above will fail spectacularly if you have spaces (and their ilk) in your names. Use or something. Create two repos, a-repo and b-repo, and initialize them with a file and some commits. Initialize the monorepo Add both subrepos as remotes and fetch them. Synthesize a tree listing and create it. Synthesize a commit with all the parents Update our working copy You can see how the tree preserves the subrepo histories Now let's let the A repo change Now we have to redo the merge. We use the same trick, sorta. Since we have that nice "README.md" in the mono repo tree, we want to preserve that. But, when we pull out , we have those and trees, and we want to filter those out. So, here we're abusing to filter them out. The and expression is producing . And, sure enough, voila!