{ config, lib, pkgs, pkgs-unstable, ... }: { programs.jujutsu = { enable = true; package = pkgs-unstable.jujutsu; settings = { fsmonitor = { backend = "watchman"; fsmonitor.watchman.register-snapshot-trigger = true; }; snapshot = { max-new-file-size = "2MiB"; auto-update-stale = true; }; aliases = { a = [ "absorb" ]; dlog = [ "log" "-r" ]; l = [ "log" "-r" "(trunk()..@):: | (trunk()..@)-" ]; #l = ["log" "-r" "more()"]; fresh = [ "new" "trunk()" ]; # Figures out the closest bookmark # and pulls it up to the latest change that can be pushed. tug = [ "bookmark" "move" "--from" "closest_bookmark(@)" "--to" "closest_pushable(@)" ]; bl = [ "bookmark" "list" ]; blr = [ "bookmark" "list" "--revisions" "recent()" ]; c = [ "commit" ]; ci = [ "commit" "--interactive" ]; # commit interactive m = [ "describe" ]; mm = [ "describe" "--message" ]; d = [ "diff" "--stat" "--revisions" ]; dd = [ "diff" "--revisions" ]; files = [ "diff" "--name-only" "--revisions" ]; review-files = [ "diff" "--name-only" "--revisions" "review()" ]; dp = [ "describe" "@-" ]; # describe previous e = [ "edit" ]; para = [ "parallelize" "closest_tip()..@-" ]; patch = [ "show" "--git" "--template" "git_format_patch_email_headers" ]; pull = [ "git" "fetch" ]; push = [ "git" "push" ]; gf = [ "git" "fetch" ]; # git fetchq gi = [ "git" "init" "--colocate" ]; gp = [ "git" "push" ]; # git push i = [ "git" "init" "--colocate" ]; nb = [ "bookmark" "create" "-r @-" ]; # new bookmark (for creating bookmark to push) nc = [ "new" "-B" "@" "--no-edit" ]; # new change before r = [ "rebase" ]; rm = [ "rebase" "-d" "main" ]; # "re-main" s = [ "show" "--stat" ]; ss = [ "show" ]; sq = [ "squash" ]; sqi = [ "squash" "--interactive" ]; si = [ "squash" "--interactive" ]; sp = [ "show" "@-" ]; up = [ "util" "exec" "--" "sh" "-c" '' if [ $# == 0 ]; then jj bookmark move --from "closest_bookmark(@)" --to "closest_pushable(@)" else jj bookmark move --to "closest_pushable(@)" "$@" fi '' "" ]; upp = [ "util" "exec" "--" "sh" "-c" "jj up && jj git push" "" ]; track-github-PR = [ "util" "exec" "--" "sh" "-euxc" '' PR=$1 gh pr view --json headRepository,headRefName,headRepositoryOwner $PR --jq '" set_remote() { jj 2>/dev/null git remote add \"$1\" \"$2\" || jj git remote set-url \"$1\" \"$2\" } set_remote \(.headRepositoryOwner.login) git@github.com:\(.headRepositoryOwner.login)/\(.headRepository.name) jj git fetch --remote \(.headRepositoryOwner.login) --branch \(.headRefName) jj bookmark track --remote=\(.headRepositoryOwner.login) \(.headRefName) origin=$(gh repo view --json owner,name --jq .owner.login) set_remote \"$origin\" \"$(gh repo view --json sshUrl --jq .sshUrl)\" git fetch --force --update-head-ok \"$origin\" \"refs/pull/'"$PR"'/head:refs/remotes/$origin/pull/'"$PR"'\" "' | sh -euxs '' "jj-track-github-PR" ]; untrack-github-PR = [ "util" "exec" "--" "sh" "-euxc" '' PR=$1 gh pr view --json headRepository,headRefName,headRepositoryOwner $PR --jq '" jj bookmark forget \(.headRefName) || true origin=$(gh repo view --json owner,name --jq .owner.login) git for-each-ref --format \"delete %(refname)\" \\ \"refs/remotes/\(.headRepositoryOwner.login)/\(.headRefName)\" \\ \"refs/remotes/$origin/pull/'"$PR"'\" | tee /dev/stderr | git update-ref --stdin "' | sh -euxs '' "jj-untrack-github-PR" ]; # Get all open stacks of work. open = [ "log" "-r" "open()" ]; # Better name IMO. credit = [ "file" "annotate" ]; # Retrunk a series. Typically used as `jj retrunk -s ...` and notably can be # used with open: # - jj retrunk -s 'all:roots(open())' retrunk = [ "rebase" "-d" "trunk()" ]; # Retrunk the current stack of work. reheat = [ "rebase" "-d" "trunk()" "-s" "all:roots(trunk()..stack(@))" ]; # Take content from any change and move it into @. # - jj consume xyz path/to/file` consume = [ "squash" "--into" "@" "--from" ]; # Eject content from @ into any other change. # - jj eject xyz --interactive eject = [ "squash" "--from" "@" "--into" ]; # All operations o = [ "op" "log" ]; # All operations, with more whitespace oo = [ "op" "log" "-T" "builtin_op_log_comfortable" ]; abandon-empties = [ "abandon" "-r" "description(exact:'') ~ root()" ]; # Move $commit just after the bookmark, and then move $bookmark on top of it. # # "$bookmark+" works, because after rebase -A, the only child of $bookmark is $commit. # I do not use $commit itself because it might point to a # different commit after the rebase (e.g. @-). # # My workflow uses mega-merge: I write a commit or two, # and then move them to relevant branches with jj to. # As a bonus, bookmarks are kept up-to-date for easy pushing. # https://github.com/jj-vcs/jj/discussions/5568#discussioncomment-14289564 to = [ "util" "exec" "--" "sh" "-c" '' set -eux bookmark=$1 commit=''${2-} # default to latest non-ephemeral commit if [ -z "$commit" ]; then commit=$(jj log --no-graph -T change_id -r '@ ~ ephemeral') if [ -n "$commit" ]; then # Moving current commit, create a new one to preserve # current position in the commit DAG jj new else commit=@- fi fi jj rebase -r "$commit" -A "$bookmark" jj bookmark move -f "$bookmark" -t "$bookmark+" '' "jj-to" ]; }; revsets = { log = "default()"; }; revset-aliases = { # Focus current commit, trunk(), on local work and remote work by removing ancestors of trunk() "default()" = "@ | trunk() | ancestors_and_children(bookmarks() | tracked_remote_bookmarks()) ~ ..trunk()"; "active(rev)" = "(ancestors(rev) | descendants(rev)) ~ immutable()"; "ancestors_and_children(x)" = "..x | x::"; # Remote commits not merged in trunk() "review()" = "ancestors_and_children(@) ~ ..trunk()"; "review(x)" = "ancestors_and_children(remote_bookmarks(x)) ~ ..trunk()"; "reviews()" = "ancestors_and_children(remote_bookmarks()) ~ ..trunk()"; # "wip()" = "ancestors_and_children(bookmarks()) ~ ..(remote_bookmarks() | trunk())"; "wip(x)" = "trunk() | ..bookmarks(x) ~ ..(remote_bookmarks() | trunk())"; # Authored or committed by specified user "user(x)" = "author(x) | committer(x)"; "closest_tip()" = "heads(::@ & remote_bookmarks())"; # Those who would be abandoned if you check out something else ephemeral = "(empty() ~ merges() ~ root()) & description(exact:\" \")"; # show everything in the current set of branches off of trunk(): branch = "(coalesce(trunk(),root())..@)- | (coalesce(trunk(),root())..@)::"; # radicle "closest_bookmark(to)" = "heads(::to & bookmarks())"; "closest_pushable(to)" = "heads(::to & mutable() & ~description(exact:\" \") & (~empty() | merges()))"; "desc(x)" = "description(x)"; "pending()" = ".. ~ ::tags() ~ ::remote_bookmarks() ~ @ ~ private()"; "private()" = "description(glob:'wip:*') | description(glob:'private:*') | description(glob:'WIP:*') | description(glob:'PRIVATE:*') | conflicts() | (empty() ~ merges()) | description('substring-i:\"DO NOT MAIL\"')"; #"trunk()" = "main@rad"; #"immutable_heads()" = "tags()"; "immutable_heads()" = "present(trunk()) | tags() | ( untracked_remote_bookmarks() ~ untracked_remote_bookmarks(remote=glob:'rad') ~ untracked_remote_bookmarks(regex:'^patch(es)/',remote=glob:'rad'))"; #'default()' = 'coalesce(trunk(), root())::present(@) | ancestors(visible_heads(), 2)' # dev "bases" = "present(main@rad) | present(master@origin) | present(main@origin)"; "downstream(x,y)" = "(x::y) & y"; "branches" = "downstream(trunk(), bookmarks()) & mine()"; "branchesandheads" = "branches | (heads(trunk()::) & mine())"; "curbranch" = "latest(branches::@- & branches)"; "nextbranch" = "roots(@:: & branchesandheads)"; "more" = "log | ancestors(visible_heads(), 2)"; "unmerged()" = "bookmarks() & ~(trunk():: | trunk())"; "recent()" = "committer_date(after:\"1 months ago\")"; "recent(revset)" = "revset & recent()"; # stack(x, n) is the set of mutable commits reachable from 'x', with 'n' # parents. 'n' is often useful to customize the display and return set for # certain operations. 'x' can be used to target the set of 'roots' to traverse, # e.g. @ is the current stack. "stack()" = "ancestors(reachable(@, mutable()), 2)"; "stack(x)" = "ancestors(reachable(x, mutable()), 2)"; "stack(x, n)" = "ancestors(reachable(x, mutable()), n)"; # The current set of "open" works. It is defined as: # # - given the set of commits not in trunk, that are written by me, # - calculate the given stack() for each of those commits # # n = 1, meaning that nothing from `trunk()` is included, so all resulting # commits are mutable by definition. "open()" = "stack(trunk().. & mine(), 1)"; # the set of 'ready()' commits. defined as the set of open commits, but nothing # that is blacklisted or any of their children. # # often used with gerrit, which you can use to submit whole stacks at once: # # - jj gerrit send -r 'ready()' --dry-run "ready()" = "open() ~ blacklist()::"; }; template-aliases = { # Hide unnecessary bits to make the jj log more concise #"format_short_change_id(id)" = "id.shortest(4)" #"format_short_commit_id(id)" = "id.shortest(4)" "format_timestamp(timestamp)" = "timestamp.ago()"; #"format_short_signature(signature)" = "signature.email()" # Both name and email address #"format_short_signature(signature)" = "signature" # Username part of the email address #"format_short_signature(signature)" = "signature.email().local()" "format_short_signature(signature)" = "signature.name()"; }; git = { write-change-id-header = true; # Prevent pushing work in progress or anything explicitly labeled "private"; private-commits = "description(glob:'wip:*') | description(glob:'private:*')"; # Don't require --allow-new when pushing a new bookmark fetch = [ "origin" "rad" ]; push = "origin"; }; remotes = { origin.auto-track-bookmarks = "glob:*"; ju1m.auto-track-bookmarks = "glob:*"; }; ui = { paginate = "auto"; default-command = "log"; diff-editor = ":builtin"; # From https://github.com/julienvincent/hunk.nvim #diff-editor = ["nvim", "-c", "DiffEditor $left $right $output"]; show-cryptographic-signatures = true; }; merge-tools.diffconflicts = { program = "nvim"; merge-args = [ "-c" "let g:jj_diffconflicts_marker_length=$marker_length" "-c" "JJDiffConflicts!" "$output" "$base" "$left" "$right" ]; merge-tool-edits-conflict-markers = true; }; # # via @dubi steinkek in the jj discord # [merge-tools.gitpatch]; # program = "sh" # edit-args = ["-c", ''' # set -eu # rm -f "$right/JJ-INSTRUCTIONS" # git -C "$left" init -q # git -C "$left" add -A # git -C "$left" commit -q -m baseline --allow-empty # mv "$left/.git" "$right" # git -C "$right" add --intent-to-add --ignore-removal . # tell git to include new files in interactive patch mode # git -C "$right" add -p # git -C "$right" diff-index --quiet --cached HEAD && { echo "No changes done, aborting split."; exit 1; } # git -C "$right" commit -q -m split # git -C "$right" reset -q --hard # undo changes in modified files, remove added files # ''', # ]; # merge-args = ["-c", "echo gitpatch cannot be used as a diff tool"]; # diff-args = ["-c", "echo gitpatch cannot be used as a diff tool"]; }; }; home.packages = lib.mkIf config.programs.git.enable [ pkgs.watchman pkgs-unstable.gg-jj pkgs.jjui pkgs-unstable.lazyjj ]; xdg.configFile."jjui".source = jujutsu/jjui; programs.bash = lib.mkIf config.programs.git.enable { shellAliases = { je = "jj-edit"; jer = "jj-edit-review"; }; initExtra = '' jj-edit () { local revs=$1; shift; $EDITOR $(jj diff -r "$revs" --name-only "$@"); } jj-edit-review () { local revs=$1; shift; $EDITOR $(jj diff -r "review($revs)" --name-only "$@"); } ''; }; }