#| file: modules/home-manager/git/default.nix
{
lib,
pkgs,
config,
...
}:
let
cfg = config.mjm.git;
<<mkExeclineAlias>>
in
{
options.mjm.git = {
<<options>>
};
config = lib.mkIf cfg.enable {
home.packages = [
<<packages>>
];
<<config>>
};
_class = "homeManager";
}enable = lib.mkOption {
type = lib.types.bool;
default = config.mjm.desktop.enable;
};The Git module is enabled by default on workstation machines.
Even though I don't really use the Git client directly for the most part, there is still some configuration for it that is relevant.
programs.git.enable = true;
programs.git.settings.user = {
name = "MJ Moriarity";
email = "matt@mattmoriarity.com";
};The basics of using Git are to install it and set your name and email address that will be attached your commits.
programs.git.package = pkgs.git.override { withLibsecret = true; };
programs.git.settings.credential = {
helper = [ "libsecret" ];
"https://git.midna.dev" = {
oauthClientId = "e90ee53c-94e2-48ac-9358-a874fb9e0662";
oauthAuthURL = "/login/oauth/authorize";
oauthTokenURL = "/login/oauth/access_token";
};
};
programs.git-credential-oauth.enable = true;I'm using git-credential-oauth to handle credentials for HTTPS repositories, which I usually prefer to use. It's meant to be used with another credential helper to actually store the results, so I'm using the libsecret helper for that. That helper is not packaged in Git from Nixpkgs by default, so I need to enable that (using pkgs.gitFull is also an option, but enables several other things I don't care about).
I also need to configure the OAuth client for my Forgejo instance.
programs.git.signing.format = null;
This updates the value for this option to the newer default that changed after my state version.
programs.jujutsu.enable = true; programs.jujutsu.settings.user = config.programs.git.settings.user;
I primarily work with Git repositories through Jujutsu, so I need to install that and configure my name and email just like I would with Git.
programs.jujutsu.settings.fsmonitor.backend = "watchman";
#| id: packages pkgs.watchman
For some larger repositories, using watchman to track file changes drastically improves performance. This must be enabled in Jujutsu's config and requires watchman to be installed.
programs.jujutsu.settings.signing = {
behavior = "own";
backend = "ssh";
key = "~/.ssh/id_ed25519.pub";
};I enable signing my commits with my SSH key. I mostly do this because it's easy, but I'm skeptical of the actual value of it to be honest.
programs.jujutsu.settings.ui = {
default-command = "log";
diff-editor = ":builtin";
};These simply configure the same values as the defaults, in order to suppress a hint message that will otherwise be shown every time they are used.
programs.jujutsu.settings.ui.diff-formatter = ":git";
I don't really like the default color-words formatter Jujutsu uses for diffs. I find it a bit hard to read. The git style formatter suits me better.
programs.jujutsu.settings.ui.merge-editor = "meld";
#| id: packages pkgs.meld
Meld is my default choice for resolving merge conflicts.
programs.jujutsu.settings.templates.draft_commit_description = ''
concat(
builtin_draft_commit_description,
"\nJJ: ignore-rest\n",
diff.git(),
)
'';When drafting a commit description in the editor, this includes the diff of the changes in the commit at the bottom. This is similar to "git commit -v" which I used a lot.
programs.jujutsu.settings.revsets = {
log = "present(@) | ancestors(reachable(@, mutable()), 2)";
short-prefixes = "builtin_log()";
};
programs.jujutsu.settings.aliases.ll = [
"log"
"-r"
"builtin_log()"
];The default log revisions for Jujutsu show more than I usually want to see, so I replace them with a new revset that is more focused. It only shows the working copy, plus any other mutable commits reachable from the working copy, plus one additional parent of those commits in order to see where they come from.
The "jj ll" alias shows me what would normally be "jj log". The short-prefixes revset is updated so that the builtin log is still used for that, otherwise too few commits would get short prefixes.
programs.jujutsu.settings.aliases = {
history = [
"log"
"-r"
"::@"
];
hist = [ "history" ];
h = [ "history" ];
};Often I just want to see the complete history from my current revision, similar to what a "git log" would produce. This "jj history" alias does that.
programs.jujutsu.settings.aliases = {
dc = [
"diff"
"-r"
"trunk()..@-"
];
di = [
"diff"
"-r"
"trunk()..@"
];
};These two similar aliases are shortcuts for common diff operations. Both show diffs relative to the trunk bookmark, though they allow for trunk to have advanced past the point the changes are based on. The difference is that one includes changes from the working copy and one does not. The "dc" is carried over from a git alias, where the 'c' referred to "cached", because it would not show changes that were not staged in the index.
programs.jujutsu.settings.aliases = {
gf = [
"git"
"fetch"
];
gfa = [
"git"
"fetch"
"--all-remotes"
];
};These are simple aliases to common git fetch operations.
programs.jujutsu.settings.aliases.up = mkExeclineAlias "jj-up" ''
if { jj git fetch }
jj rebase -d trunk()
'';Jujutsu lacks a convenient single command to fetch changes from git and combine them with any local changes you might have, similar to what a "git pull" might do. I call this "jj up", which just does a fetch followed by a rebase on top of the trunk bookmark.
#| id: mkExeclineAlias mkExeclineAlias = name: text: [ "util" "exec" "--" (pkgs.execline.writeScript name "" text) ];
The mkExeclineAlias is a convenience for defining an alias that runs an execline script. I'll use this anywhere I need to run multiple commands in an alias.
programs.jujutsu.settings.revset-aliases."tip(x)" = "heads(::x & mutable() & ~description(exact:'') & ~(empty() | merges()))"; programs.jujutsu.settings.revsets.bookmark-advance-to = "tip(@)";
By default, the "jj bookmark advance" command advances the nearest bookmark to point to the working copy. I prefer a smarter default for where it should point. This will grab the closest revision to the working copy that makes any sense to push to a bookmark. Usually this will end up being the parent of the working copy, unless the working copy is non-empty and has a description already set.
programs.jujutsu.settings.aliases = {
tug = [
"bookmark"
"advance"
];
pb = mkExeclineAlias "jj-pb" ''
jj transact
if { jj bookmark advance }
jj git push
'';
};"jj tug" is just a shorter way to do "jj bookmark advance" now, though it used to be more involved before the "jj bookmark advance" command existed.
"jj pb" combines this with a git push. This is the command I run when I have new commits I want to add to a bookmark. You could probably argue that I should get used to doing these two operations separately, but I think the habit would be hard to break. An interesting thing here is that "jj pb" might fail at the actual push if the bookmark I'm pushing to has changes I haven't fetched. Normally, that would leave me in an awkward state, where I've advanced the bookmark to a point that will now be in conflict with the remote state. The "jj transact" at the start of the script avoids this problem by reverting to the starting state if something fails.
programs.jujutsu.settings.aliases.transact = mkExeclineAlias "jj-transact" ''
elgetpositionals
backtick -E curr_op { jj op log --no-graph -T id --limit 1 }
if -nt { exec $@ }
jj op restore --what repo $curr_op
'';"jj transact" takes a command to run (probably a sequence of execline commands) inside a "transaction". It records the ID of the current operation before doing anything. Then it runs the command it was given, and if the command fails, it restores the repository to the operation it recorded, undoing any changes that happened during the command.
Because "jj pb" uses this, if the push fails, the advancing of the bookmark will be undone. Usually this means I will then do a "jj up" to fetch the current state and rebase on top of it, and then I can try my "jj pb" command again. Without the transaction, running "jj up" after the failed push would introduce a conflict in the bookmark.
programs.jujutsu.settings.aliases = {
p = [
"git"
"push"
];
pc = [
"git"
"push"
"--change"
"tip(@)"
];
};These are simpler aliases for other common push commands. "jj p" is just shorthand for "jj git push", and is often combined with flags to control what is pushed. "jj pc" is how I push changes with automatic bookmark names. I use the same "tip" revset alias used for advancing bookmarks to determine the commit the new bookmark should point to.
programs.jujutsu.settings.aliases.pr = mkExeclineAlias "jj-pr" ''
elgetpositionals -P 1
define branch pr-''${1}
if { git fetch -f upstream refs/pull/''${1}/head:''${branch} }
if { jj new $branch }
jj bookmark delete $branch
'';The "jj pr" alias is used to checkout a pull request from GitHub and have it imported properly by Jujutsu. It takes the PR number as an argument, and assumes that the "upstream" remote is the repository that the PR was opened against. It fetches the head commit of the PR by dropping down to git, create a branch named "pr-$num" to point to it. Then it creates a new jj working copy commit on top of it, and then finally deletes the branch.
The branch create/delete dance here is needed because just fetching the commit is not enough for Jujutsu to see it. Creating a branch for it causes Jujutsu to import that branch as a local bookmark, at which point it has knowledge of the commit's existence. From there, the branch is not needed anymore: the change ID or commit ID can be used to refer to it from there.
#| id: packages (pkgs.execline.writeScriptBin "jjj" "" <| builtins.readFile ./jjj)
I have a command "jjj" that will run a jj command using a revision that is selected interactively. It's inspired by a blog post describing it, but I've implemented it in my own way.
=> jjj
#| file: modules/home-manager/git/jjj importas -D show cmd 1 shift elgetpositionals
The first argument is used as the subcommand to pass to jj. If no arguments are passed, then "show" is used. The rest of the arguments are then imported as $@.
#| file: modules/home-manager/git/jjj
backtick -E selected {
pipeline { jj log -r all() --no-graph --color=always -T skim_changes }
sk --nth 2.. --output-format {1} --prompt "jj ${cmd}>" --ansi --preview "jj show --color always {1}"
}programs.jujutsu.settings.template-aliases.skim_changes =
''if(description, separate(' ', format_short_change_id_with_change_offset(self), description.first_line(), if(conflict, label('conflict', 'conflict'))) ++ "\n")'';The next step is to present the interactive chooser, populated with the list of all revisions in the repository. I use skim as the chooser. I have a special template for this purpose which shows the short change ID, the first line of the description, and whether the revision is conflicted. The chooser will fuzzy match on the description, skipping the change ID. It will also show the command to be run in the prompt of the chooser, and it will show a preview pane with the contents of the currently selected revision to the side. The output will just be the chosen change ID, which will be captured as "selected".
#| file: modules/home-manager/git/jjj
if { eltest -n $selected }
jj $cmd -r $selected $@Finally, as long as something was actually selected, the jj command is run, passing the selected change ID with -r and passing along any additional arguments.
programs.jujutsu.settings.template-aliases.skim_bookmarks =
''if(!remote && present && !conflict, label('bookmark', name) ++ format_ref_targets(self) ++ "\n")'';
programs.jujutsu.settings.aliases = {
pf = mkExeclineAlias "jj-pf" ''
backtick -E bookmark { jj fb }
jj git push --bookmark $bookmark
'';
fb = mkExeclineAlias "jj-fb" ''
elgetpositionals
pipeline { jj bookmark list --color always -t --quiet -T skim_bookmarks $@ }
sk --ansi -d "[\\t\\n: ]+" --nth 1,4.. --output-format {1}
'';
};I have something similar for bookmarks. The "jj fb" alias presents an interactive chooser for bookmarks, which can fuzzy match on the bookmark name or the description of the revision it points to. The bookmark name that was chosen is printed. The "jj pf" uses this to select a bookmark to push.
Jujutsu makes it easy to work with "megamerges": merge commits with possibly many parents. One way that I use them is for forks of repositories I depend on, particularly Nixpkgs. I'll have changes in those repos that I want to use that are not yet merged into the upstream branch. To use those changes, I'll maintain a separate branch that points at a megamerge of the upstream branch as well as the commits for all of the additional changes I want to pull in, and this is what I will add as a source for npins.
To make this even easier to manage, I have some extra tooling for Jujutsu for this purpose. Some is for working with megamerges in general and some is specifically for working with these forks.
programs.jujutsu.settings.aliases.mega = mkExeclineAlias "jj-mega" <| builtins.readFile ./jj-mega;
I add a "jj mega" alias, which is by far my most complex alias. It has a number of subcommands for working with megamerges.
#| file: modules/home-manager/git/jj-mega
<<mega-options>>
case -- $cmd {
list {
<<mega-list>>
}
add {
<<mega-add>>
}
add-pr {
<<mega-add-pr>>
}
remove {
<<mega-remove>>
}
replace {
<<mega-replace>>
}
update {
<<mega-update>>
}
}
echo "\
Usage: jj mega [OPTIONS] <COMMAND>
Commands:
list List the changes that make up the megamerge
add Add a revision to the megamerge
add-pr Add a pull request to the megamerge
remove Remove a revision from the megamerge
replace Replace revisions in the megamerge
update Update the megamerge to include the latest upstream changes
"The overall structure of the "jj mega" script is simple. It parses options/arguments, then handles the subcommands it knows about. If the subcommand given doesn't match a known one, it prints the usage information for the command.
#| id: mega-options elgetopt m: importas -D mega() mega ELGETOPT_m importas -D "" cmd 1 shift elgetpositionals
There's only one proper option supported, -m. It can be used to indicate the revision that is the megamerge to work with, which will then be available in the rest of the script as $mega. If the option isn't given, it defaults to mega(), which is a revset alias I'v defined in Jujutsu:
programs.jujutsu.settings.revset-aliases = {
"closest_bookmark(to)" = "heads(::to & bookmarks())";
"mega()" = ''
coalesce(
closest_bookmark(@) & bookmarks(glob:"deploy/*"),
bookmarks(exact:"mega"),
bookmarks(exact:"deploy/master"),
)
'';
};mega() is usually meant to refer to a particular bookmark. The bookmarks I use in the forks I maintain all follow a particular pattern to support easy automation: they are named "deploy/" followed by the name of the upstream branch they are tracking. For instance, "deploy/nixos-unstable" is the bookmark in my fork of Nixpkgs that contains the nixos-unstable branch merged with any other changes I want to use along with it.
Given that convention, mega() first tries to resolve to the closest bookmark to the working copy, as long as it has the "deploy/" prefix. This is convenient for a repository like Nixpkgs where I have multiple megamerge bookmarks to manage: it means if my working copy is a descendant of one of these bookmarks, that's the one "jj mega" will use. If the closest bookmark is something else, then it will try to find one named "mega" or "deploy/master". If these don't work for a particular repo, the mega() revset alias can be set locally for that repo with "jj config".
#| id: mega-list
jj log --no-graph -r ${mega}-"jj mega list" will list the revisions that make up the megamerge.
#| id: mega-add
jj rebase
--source $mega
--onto ${mega}-
--onto $1"jj mega add" uses "jj rebase" to add a new parent to the megamerge in addition to the ones that are already there.
#| id: mega-add-pr
define branch pr-${1}
if { git fetch -f upstream refs/pull/${1}/head:${branch} }
foreground {
jj rebase
--source $mega
--onto ${mega}-
--onto $branch
}
jj bookmark delete $branch"jj mega add-pr" is like a combination of "jj pr" and "jj mega add". Instead of checking out the PR, it adds it to the megamerge. Very convenient for testing Nixpkgs PRs or applying them early to fix a blocking problem before the channel updates.
#| id: mega-remove
jj rebase
--source $mega
--onto "${mega}- ~ ${1}""jj mega remove" is the opposite of "jj mega add": it uses "jj rebase" to set the parents of the megamerge to no longer include the given revision. It's worth noting that for remove, the argument doesn't have to refer to a single revision. It can be an arbitrarily large revset, and any parents of the megamerge that are included in that revset will be removed.
#| id: mega-replace
jj rebase
--source $mega
--onto "${mega}- ~ ${1}"
--onto $2"jj mega replace" is essentially an add and remove in a single operation. The first argument is the revision to remove and the second is the one to add. Just as with "jj mega remove", the first argument can refer to multiple revisions.
#| id: mega-update
define mega2 deploy/${1}
define upstream ${1}@upstream
if { jj git fetch --all-remotes }
if { jj new $mega2 }
jj rebase
--source $mega2
--onto "${mega2}- ~ ::${upstream}"
--onto "${upstream}""jj mega update" is a sort of specialized "jj mega replace" reflecting an operation I do often with my forks: updating the megamerge to have the latest upstream changes. For this command, the -m option is ignored, and instead the first argument is expected to be the upstream bookmark name (master, main, nixos-unstable, etc.). The megamerge bookmark is then determined by prepending the "deploy/" prefix to that.
"jj mega update" does the additional steps of fetching remote changes and creating a new working copy on top of the megamerge bookmark. From there, it essentially does a "jj mega replace", removing any ancestors of the new upstream commit and adding that commit instead. This behavior is particularly nice because it means that if the megamerge contained commits that have now been merged upstream, they will get dropped from the megamerge automatically.
An example use of this would be to run "jj mega update nixos-unstable" in my clone of Nixpkgs. It would fetch remote changes then create a new working copy on top of deploy/nixos-unstable. Then it would remove the existing revision in the megamerge that was pointing at nixos-unstable@upstream previously and replace it with the current revision for nixos-unstable@upstream. If any other revisions in the megamerge are now included already in nixos-unstable@upstream, those will be dropped from the megamerge now too, since they aren't needed anymore.
programs.jujutsu.settings.aliases.pd = [ "git" "push" "--remote" "deploy" "--tracked" ];
Conventionally, my forks for dependencies are maintained on my own forge, and my clones of them use the name "deploy" for that remote. Whenever I've made changes to any of these bookmarks, I can use this "jj pd" alias to push all tracked bookmarks for that remote.
# programs.radicle.enable = true; # services.radicle.node.enable = true; services.radicle.node.lazy.enable = true;
I'm experimenting a bit with Radicle, a distributed P2P protocol for Git repositories. I don't currently think it's good enough to rely on, but it's definitely interesting, so I'll probably continue to push things there.
programs.radicle.settings.preferredSeeds = [ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@iris.radicle.network:58776" "z6Mkmqogy2qEM2ummccUthFEaaHvyYmYBYh3dbe9W4ebScxo@rosa.radicle.network:58776" ];
For some reason, these default seeds get overwritten with an empty list when configuring Radicle with home-manager, so they need to be set manually if you want them (and you probably do).
text/gemini;lang=en-USThis content has been proxied by September (UNKNO).