├── GNUmakefile ├── lib ├── exectool.sh ├── check-unfinished.sh ├── popfile.sh ├── editor.sh ├── ocolumns.awk ├── preview.sh ├── draft.sh ├── evolog.sh ├── common.sh ├── oplog.sh ├── reparent.sh ├── rebase.sh ├── gen-message.py ├── bookmarks.sh └── setup.sh ├── screencasts ├── slowtype.sh ├── revset.sh ├── intro.sh ├── bookmarks.sh ├── merging.sh ├── splitting.sh ├── render.sh ├── oplog.sh ├── rebasing.sh ├── megamerge.sh └── prepare.sh ├── sfx.sh ├── version.sh ├── .pre-commit-config.yaml ├── contrib ├── suspend-with-shell.el ├── jj-undirty.el ├── jj-am.sh └── jj-foreach.sh ├── tests ├── basics.sh └── utils.sh ├── preflight.sh ├── Makefile.mk ├── doc └── jj-fzf.1.md ├── README.md ├── NEWS.md ├── LICENSE └── jj-fzf /GNUmakefile: -------------------------------------------------------------------------------- 1 | include Makefile.mk 2 | -------------------------------------------------------------------------------- /lib/exectool.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | $EXECTOOL_CMD "$@" || : # avoid 'Warning: Tool exited with exit status: 1' 4 | # Hook to serve as a single-executable trampoline for e.g. `EXECTOOL_CMD="git diff" jj show --tool exectool.sh` 5 | -------------------------------------------------------------------------------- /screencasts/slowtype.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | # Read file and output character by character 6 | while IFS= read -r -n1 char; do 7 | printf "%s" "$char" 8 | sleep "$1" 9 | done < "$2" 10 | -------------------------------------------------------------------------------- /lib/check-unfinished.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | # Usage: check-unfinished.sh [FILE...] 6 | 7 | # enable color? 8 | tty -s && C="--color=always" || C= 9 | 10 | # check for keywords 11 | if 12 | grep -s $C '\bFIX''ME\b' "$@" /dev/null 13 | then 14 | exit 101 # matches indicate errors 15 | fi 16 | 17 | # exit status 18 | exit 0 19 | -------------------------------------------------------------------------------- /lib/popfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail #-x 3 | 4 | # Usage: popfile.sh 5 | TARGET="$1" 6 | 7 | # Consume files from $POPFILE_LIST, write into 8 | IFS='; ' read -r -a FILES <<< "${POPFILE_LIST-}" 9 | for f in "${FILES[@]}" ; do 10 | test -r "$f" && { 11 | cat "$f" > "$TARGET" 12 | rm "$f" 13 | exit 0 14 | } 15 | done 16 | 17 | # Consume files from $KEEPFILE_LIST, keepping 18 | IFS='; ' read -r -a FILES <<< "${KEEPFILE_LIST-}" 19 | for f in "${FILES[@]}" ; do 20 | test -r "$f" && { 21 | rm "$f" 22 | exit 0 23 | } 24 | done 25 | 26 | # Finally, empty 27 | echo -n > "$TARGET" 28 | 29 | exit 0 30 | -------------------------------------------------------------------------------- /lib/editor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | # File hash *before* editing 6 | PREHASH="$(sha256sum "$1" 2>/dev/null)" 7 | 8 | # Determine original editor 9 | JJ_EDITOR="${JJFZF_EDITOR-}" 10 | jj --no-pager --ignore-working-copy config get ui.editor 2>/dev/null 11 | test -z "$JJ_EDITOR" && 12 | JJ_EDITOR="$(jj --no-pager --ignore-working-copy config get ui.editor 2>/dev/null)" 13 | test -z "$JJ_EDITOR" && 14 | JJ_EDITOR="${VISUAL:-${EDITOR:-pico}}" 15 | 16 | # Edit files 17 | ERR=0 18 | "$JJ_EDITOR" "$@" || ERR=$? 19 | 20 | # Report "no-edit" as error 21 | if test $ERR == 0 ; then 22 | POSTHASH="$(sha256sum "$1" 2>/dev/null)" 23 | test "$PREHASH" == "$POSTHASH" && 24 | ERR=1 25 | fi 26 | 27 | exit $ERR 28 | -------------------------------------------------------------------------------- /sfx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail #-x 3 | ABSPATHSCRIPT=`readlink -f "$0"` && function die { echo "${ABSPATHSCRIPT##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 4 | 5 | SFXSHTMPDIR="$(mktemp -d -t sfxsh.XXXXXX)" 6 | trap 'rm -rf "$SFXSHTMPDIR"' EXIT 7 | 8 | if test " ${1-}" == ' --sfxsh-pack' ; then 9 | EXE="$2" TB="$3" && shift 3 10 | tar zcf "$TB".tmp "$@" 11 | ( cat "$0" 12 | echo '$SFXSHTMPDIR'/"${EXE#/}" '"$@"' 13 | echo 'exit' 14 | echo '#''__SFXSH_TAR__' 15 | cat "$TB".tmp 16 | ) > "$TB" 17 | chmod +x "$TB" 18 | rm -f "$TB".tmp 19 | ls -al "$TB" 20 | exit 0 21 | fi 22 | 23 | OFFSET=$(sed '/#''__SFXSH_TAR__/q' "$0" | wc -c) || 24 | die "failed to detect tarball" 25 | dd iseek=1 ibs="$OFFSET" if="$0" 2>/dev/null | ( cd $SFXSHTMPDIR && tar zxf - ) || 26 | die "failed to extract tarball" 27 | 28 | export SFXSHTMPDIR 29 | 30 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail 4 | 5 | # Commit information provided by git-archive in export-subst format string, see gitattributes(5) 6 | read VDESCRIBE VDATE <<<' v0.34.0-34-gfae5fde 2025-10-31 04:07:45 +0100 ' 7 | 8 | # Use baked-in version info if present 9 | ! [[ "$VDATE" =~ % ]] && 10 | echo "${VDESCRIBE#v} $VDATE" && exit 11 | 12 | # Use version info from git repository, needs non-shallow clones 13 | # Prefer exact tags (even if light, like nightly) over annotated tags 14 | cd $(dirname $(readlink -f "$0")) 15 | VDESCRIBE=$(git describe --exact-match --tags --match='v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || 16 | git describe --match='v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null) && 17 | echo "${VDESCRIBE#v} `git -P log -1 --pretty=%ci`" && exit 18 | 19 | # Fallback, unversioned 20 | echo "0.0.0-unversioned0 2001-01-01 01:01:01 +0000" 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 2 | 3 | # See https://pre-commit.com for more information 4 | default_stages: [pre-push] # only run on push 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v6.0.0 8 | # See https://pre-commit.com/hooks.html for more hooks 9 | hooks: 10 | # id: end-of-file-fixer 11 | - id: check-added-large-files 12 | - id: check-executables-have-shebangs 13 | - id: check-json 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: check-toml 17 | - id: check-xml 18 | - id: check-yaml 19 | - id: forbid-new-submodules 20 | - id: trailing-whitespace 21 | 22 | - repo: local 23 | hooks: 24 | - id: check-unfinished-work 25 | name: Check Unfinished Work 26 | entry: lib/check-unfinished.sh 27 | language: system 28 | types: [file] # Run for all file types 29 | pass_filenames: true 30 | -------------------------------------------------------------------------------- /contrib/suspend-with-shell.el: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 2 | 3 | ;; == suspend-with-shell == 4 | ;; Wrap `(suspend-emacs)` so that a subshell is executed with $SHELL pointing 5 | ;; to a script that will run `COMMAND`. This way, emacs can be suspended 6 | ;; to run another terminal process on the same tty, without using 7 | ;; `ioctl(TIOCSTI)` - which `(suspend-emacs)` relies on but is not available 8 | ;; in recent kernel versions. 9 | (defun suspend-with-shell (COMMAND) 10 | "Call (suspend-emacs) with $SHELL assigned to a script that will run COMMAND" 11 | (interactive) 12 | (let ((oldshell (getenv "SHELL")) 13 | (tfile (make-temp-file "emacssubshell")) 14 | (script (concat "#!/usr/bin/env bash\nset -Eeu #-x\n" COMMAND "\n")) 15 | (cannot-suspend 't)) ; force suspend-emacs to use sys_subshell 16 | (with-temp-file tfile 17 | (insert script)) 18 | (set-file-modes tfile #o700 'nofollow) 19 | ;; see sys_subshell() in https://github.com/emacs-mirror/emacs/blob/master/src/keyboard.c 20 | (setenv "SHELL" tfile) 21 | (suspend-emacs) ; this calls system($SHELL) 22 | (setenv "SHELL" oldshell) 23 | (delete-file tfile) 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /contrib/jj-undirty.el: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 2 | 3 | ;; == jj-undirty == 4 | ;; Update JJ repo after saving a buffer 5 | (defun jj-undirty() 6 | "Execute `jj status` to snapshot the current repository. 7 | This function checks if the current buffer resides in a JJ repository, 8 | and if so executes `jj status` while logging the command output to 9 | the '*jj-undirty*' buffer. 10 | This function is most useful as hook, to frequently snapshot the 11 | workgin copy and update the JJ op log after files have been modified: 12 | (add-hook 'after-save-hook 'jj-undirty)" 13 | (interactive) 14 | (when (locate-dominating-file "." ".jj") ; detect JJ repo 15 | (progn 16 | (let ((absfile (buffer-file-name)) 17 | (buffer (get-buffer-create "*jj-undirty*")) 18 | (process-connection-type nil)) ; use a pipe instead of a pty 19 | (with-current-buffer buffer 20 | (goto-char (point-max)) ; append to end of buffer 21 | (insert "\n# jj-undirty: after-save-hook: " absfile "\njj status\n") 22 | (start-process "jj status" buffer ; asynchronous snapshotting 23 | "jj" "--no-pager" "status" "--color=never") 24 | )))) 25 | ) 26 | 27 | ;; Detect JJ repo and snapshot on every save 28 | (add-hook 'after-save-hook 'jj-undirty) 29 | ;; (remove-hook 'after-save-hook 'jj-undirty) 30 | -------------------------------------------------------------------------------- /screencasts/revset.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | readonly SCREENCAST_SESSION=revset-demo 6 | source $(dirname $(readlink -f "${BASH_SOURCE[0]}"))/prepare.sh "$@" # for $TEMPD and funcs 7 | 8 | # == Config == 9 | export JJ_CONFIG=$(make_jj_config) 10 | clone_jj_repo $TEMPD/$SCREENCAST_SESSION 11 | 12 | D() ( K Down ) 13 | U() ( K Up ) 14 | Tab() ( K Tab ) 15 | 16 | # == SCRIPT == 17 | start_screencast $TEMPD/$SCREENCAST_SESSION \ 18 | 'jj-fzf' Enter M-h 19 | D; D; S 20 | 21 | # -- Revset -- 22 | X 'The "Revset >" prompt configures the `jj log` revision set' 23 | T 'description(faq)'; P 24 | 25 | K C-u; S 26 | T 'heads(immutable())'; P 27 | T '::'; P; D; S 28 | 29 | # -- Ctrl-U -- 30 | X 'Use Ctrl-U to clear the entire input field' 31 | K C-u; P 32 | 33 | T 'tags()'; P 34 | T '..'; P; D; S 35 | 36 | K C-u; S 37 | T 'author_date(before:2024-01-17)'; P 38 | 39 | # -- Alt+Enter -- 40 | X 'The current revset can be persisted with Alt-Enter' 41 | K M-Enter; P 42 | T '..'; P; 43 | K C-u; P 44 | 45 | # == EXIT == 46 | P 47 | stop_screencast 48 | 49 | # (cd $TEMPD/$SCREENCAST_SESSION && bash --norc ) 50 | 51 | jj --repository $TEMPD/$SCREENCAST_SESSION \ 52 | config get 'jj-fzf.log_revset' | 53 | grep -q 'author_date.*before.*2024-01-17' && 54 | echo ' OK ' "$0 passed" || 55 | die 'failed to validate screencast result, missing: author_date' 56 | -------------------------------------------------------------------------------- /lib/ocolumns.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env awk -f 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | 4 | BEGIN { # Defaults 5 | if (WIDTH < 1) WIDTH = 80 6 | } 7 | 8 | { # Foreach line 9 | words[wc++] = $0 10 | } 11 | 12 | # Print words with optimal column width 13 | END { 14 | # Try from max words down to 1 to fit layout 15 | for (cols = wc; cols >= 1; cols--) { 16 | rows = int((wc + cols - 1) / cols) 17 | # Reset maxlen array for columns 18 | for (c = 0; c < cols; c++) maxlen[c] = 0 19 | # Calculate max length per column 20 | for (i = 0; i < wc; i++) { 21 | # col = i % cols # index for row-major 22 | col = int(i / rows) # index for column-major 23 | wl = length(words[i]) 24 | if (wl > maxlen[col]) maxlen[col] = wl 25 | } 26 | # Compute total width needed, each column width + gap 27 | total = 0 28 | for (c = 0; c < cols; c++) 29 | total += (maxlen[c] += 1 * (c < cols - 1)) 30 | if (total <= WIDTH || cols == 1) break 31 | } 32 | # Print rows, fill with cols 33 | for (r = 0; r < rows; r++) { 34 | line = "" 35 | for (c = 0; c < cols; c++) { 36 | # i = r * cols + c # index for row-major 37 | i = c * rows + r # index for column-major 38 | if (i >= wc) continue 39 | fmt = "%-" maxlen[c] "s" 40 | line = line sprintf(fmt, words[i]) 41 | } 42 | sub(/[ \t]+$/, "", line) 43 | print line 44 | } 45 | } 46 | 47 | # for W in `seq 120 -1 10` ; do ( printf "%*s|\n" $W "" && tr ' ' \\n < wordlist | awk -v WIDTH=$W -f ocolumns.awk | sed 's/$/|/') ; done 48 | -------------------------------------------------------------------------------- /screencasts/intro.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=`readlink -f "$0"` 6 | SCRIPTDIR="${ABSPATHSCRIPT%/*}" 7 | 8 | 9 | # == functions and setup for screencasts == 10 | source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*} 11 | # fast_timings 12 | 13 | # SCRIPT 14 | make_repo IntroDemo gitdev jjdev 15 | start_asciinema IntroDemo 'jj-fzf' Enter 16 | P # S; T "jj-fzf"; Enter 17 | 18 | # FILTER 19 | X 'JJ-FZF shows and filters the `jj log`, hotkeys are used to run JJ commands' 20 | K Down; S; K Down; P; K Down; S; K Down; P; K Down; P; 21 | X 'The preview on the right side shows commit information and the content diff' 22 | K Up; P; K Up; S; K Up; P; K Up; S; K Up; P; 23 | X 'Type keywords to filter the log' 24 | T 'd'; S; T 'o'; S; T 'm'; S; T 'a'; S; T 'i'; S; T 'n'; S; P 25 | K BSpace 6; P 26 | 27 | # OP-LOG 28 | X 'Ctrl+O shows the operation log' 29 | K C-o; P 30 | K Down 11 31 | X 'Ctrl+D and Ctrl+L display diff or log' 32 | K C-d; P 33 | K Up 11 34 | K C-g; P 35 | 36 | # COMMIT / DESCRIBE 37 | # REBASE -r 38 | # BOOKMARK + DEL 39 | # PUSH 40 | 41 | # HELP 42 | X 'Ctrl+H shows the help for all hotkeys' 43 | K C-h; P 44 | K C-Down 11; P 45 | # T 'g'; S; T 'i'; S; T 't'; S; P; K C-u; P; P; K Down; P; K Down; P; P; 46 | K C-g; P 47 | 48 | 49 | # EXIT 50 | P 51 | stop_asciinema 52 | render_cast "$ASCIINEMA_SCREENCAST" 53 | #stop_asciinema && render_cast "$ASCIINEMA_SCREENCAST" && exit 54 | -------------------------------------------------------------------------------- /screencasts/bookmarks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | readonly SCREENCAST_SESSION=bookmarks-demo 6 | source $(dirname $(readlink -f "${BASH_SOURCE[0]}"))/prepare.sh "$@" # for $TEMPD and funcs 7 | 8 | # == Config == 9 | export JJ_CONFIG=$(make_jj_config) 10 | clone_jj_repo $TEMPD/$SCREENCAST_SESSION 11 | ( stdio_to_dev_null 12 | cd $TEMPD/$SCREENCAST_SESSION 13 | jj b c release-candidate -r qxttrqlv 14 | jj b t main@origin 15 | ) 16 | 17 | D() ( K Down ) 18 | U() ( K Up ) 19 | Tab() ( K Tab ) 20 | 21 | # == SCRIPT == 22 | start_screencast $TEMPD/$SCREENCAST_SESSION \ 23 | 'jj-fzf' Enter F11 M-h F11 24 | D; D; D 25 | 26 | # -- main -- 27 | X 'Use Alt-B to edit bookmarks or tags' 28 | D; D; S 29 | K M-b; S 30 | X 'Use Alt-B to move/create a bookmark: main' 31 | K M-b; P 32 | K Enter; P 33 | 34 | # -- Create Bookmark -- 35 | X 'Use Alt-B and Alt-D to delete a ref: release-candidate' 36 | U; U; U; S 37 | K M-b; S 38 | K M-d; P 39 | K Enter; P 40 | 41 | # -- tag -- 42 | X 'Use Alt-B and Alt-T to create a new tag: v0.28.2' 43 | U; S 44 | K M-b; S 45 | K M-t; S 46 | T 'v0.28.2'; P 47 | K Enter; P 48 | 49 | # == EXIT == 50 | P; D 51 | stop_screencast 52 | 53 | # (cd $TEMPD/$SCREENCAST_SESSION && bash --norc ) 54 | 55 | ( cd $TEMPD/$SCREENCAST_SESSION 56 | git tag -n1 | 57 | fgrep -q release-candidate && die 'failed to delete tag: release-candidate' 58 | git log -1 --format=%s main -- | 59 | fgrep -q 'builtin merge' || die 'failed to create bookmark: main' 60 | git log -1 --format=%s v0.28.2 | 61 | fgrep -q 'release: 0.28.2' || die 'failed to tag v0.28.2' 62 | ) 63 | 64 | printf ' %-8s %s\n' OK "$SCREENCAST_SESSION passed" 65 | -------------------------------------------------------------------------------- /screencasts/merging.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail # -x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=`readlink -f "$0"` 6 | SCRIPTDIR="${ABSPATHSCRIPT%/*}" 7 | 8 | 9 | # == functions and setup for screencasts == 10 | source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*} 11 | # fast_timings 12 | 13 | # SCRIPT 14 | make_repo -3tips MergingDemo gitdev jjdev 15 | start_asciinema MergingDemo 'jj-fzf' Enter 16 | 17 | # GOTO rev 18 | X 'To create a merge commit, pick the first commit to be merged' 19 | Q0 "trunk"; S 20 | 21 | # MERGE-2 22 | X 'Alt+M starts the Merge dialog' 23 | K M-m; P 24 | K 'Down'; K 'Down' 25 | Q "jjdev" 26 | X 'Tab selects another revision to merge with' 27 | K Tab; P 28 | X 'Enter starts the text editor to describe the merge' 29 | K Enter; P 30 | K C-k; P; K C-x; S # nano 31 | X 'The newly created merge commit is now the working copy' 32 | 33 | # UNDO 34 | X 'Alt+Z will undo the last operation (the merge)' 35 | K M-z ; P 36 | X 'The repository is back to 3 unmerged branches' 37 | 38 | # MERGE-3 39 | X 'Select a revision to merge' 40 | K Down; K Down; K Down 41 | Q0 "gitdev"; S 42 | X 'Alt+M starts the Merge dialog, now for an octopus merge' 43 | K M-m; P 44 | K Down; Q0 "trunk"; S 45 | K Tab; S 46 | K Down; Q0 "jjdev"; S 47 | X 'Tab again selects the third revision' 48 | K Tab; S 49 | X 'Enter starts the text editor to describe the merge' 50 | K Enter; P 51 | K C-k; P; K C-x; S # nano 52 | X 'The newly created merge commit is now the working copy' 53 | X 'Ctrl+D starts the text editor to alter the description' 54 | K C-d; P 55 | K C-k 16 56 | T "Merge 'gitdev' and 'jjdev' into 'trunk'"; P 57 | K C-x; S # nano 58 | X 'This is an Octopus merge, a commit can have any number of parents' 59 | P; P 60 | 61 | # EXIT 62 | P 63 | stop_asciinema 64 | render_cast "$ASCIINEMA_SCREENCAST" 65 | -------------------------------------------------------------------------------- /contrib/jj-am.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | VERSION=0.2.0 6 | 7 | # == Help == 8 | show_help() 9 | { 10 | cat <<-__EOF__ 11 | Usage: $SCRIPTNAME [OPTIONS...] PATCHFILE... 12 | 13 | Apply one or more patch files (from git-format-patch) to a jj repository. 14 | 15 | Options: 16 | -h, --help Display this help and exit 17 | --version Display version information and exit 18 | Arguments: 19 | PATCHFILE Path to a patch file containing commit message and diff 20 | __EOF__ 21 | } 22 | 23 | # == Parse Options == 24 | MBOXES=() 25 | while test $# -ne 0 ; do 26 | case "$1" in \ 27 | --version) echo "$SCRIPTNAME $VERSION"; exit ;; 28 | -h|--help) show_help; exit 0 ;; 29 | -*) die "unknown option: $1" ;; 30 | *) MBOXES+=("$1") ;; 31 | esac 32 | shift 33 | done 34 | 35 | # == Functions == 36 | # Create temporary dir, assigns $TEMPD 37 | temp_dir() 38 | { 39 | test -n "${TEMPD:-}" || { 40 | TEMPD="`mktemp --tmpdir -d $SCRIPTNAME-XXXXXX`" || die "mktemp failed" 41 | trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM 42 | echo "$$" > $TEMPD/$SCRIPTNAME.pid 43 | } 44 | } 45 | # Create new commit 46 | jj_commit() 47 | ( 48 | # collect commit infor from header 49 | HEADER="$1" BODY="$(<"$2")" PATCH="$3" 50 | AUTHOR="$(sed -nr '/^Author: /{ s/^[^:]*: //; p; q; }' < $HEADER)" 51 | EMAIL="$(sed -nr '/^Email: /{ s/^[^:]*: //; p; q; }' < $HEADER)" 52 | DATE="$(sed -nr '/^Date: /{ s/^[^:]*: //; p; q; }' < $HEADER)" 53 | DATE="$(date --rfc-3339=ns -d "$DATE")" 54 | SUBJECT="$(sed -nr '/^Subject: /{ s/^[^:]*: //; p; q; }' < $HEADER)" 55 | export JJ_TIMESTAMP="$DATE" 56 | test -z "$BODY" && NL='' || NL=$'\n\n' 57 | ARGS=( 58 | --config-toml "user.name=\"$AUTHOR\"" 59 | --config-toml "user.email=\"$EMAIL\"" 60 | --message="$SUBJECT$NL$BODY" 61 | ) 62 | # create commit 63 | jj new "${ARGS[@]}" 64 | # try patch 65 | patch -p1 < "$PATCH" 66 | ) 67 | 68 | # == Process == 69 | temp_dir # for $TEMPD 70 | for mbox in "${MBOXES[@]}" ; do 71 | echo "Apply: ${mbox##*/}" 72 | rm -f "$TEMPD/header" "$TEMPD/body" "$TEMPD/patch" 73 | git mailinfo -b -u --encoding=POSIX.UTF-8 "$TEMPD/body" "$TEMPD/patch" > "$TEMPD/header" < "$mbox" 74 | jj_commit "$TEMPD/header" "$TEMPD/body" "$TEMPD/patch" 75 | done 76 | # snapshot last patch 77 | jj status 78 | -------------------------------------------------------------------------------- /contrib/jj-foreach.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | 6 | show_help() 7 | { 8 | cat <<-__EOF__ 9 | Usage: ${BASH_SOURCE[0]##*/} [OPTIONS...] [--] ... 10 | 11 | Run shell for each commit in . 12 | Use \`--restore-descendants\` to run commands without affecting descendants. 13 | 14 | Options: 15 | -h, --help Display this help and exit 16 | -E Ignore errors when running 17 | --restore-descendants 18 | Preserve the content when rebasing descendants 19 | Arguments: 20 | revset Revisions to process 21 | command Shell command to execute 22 | __EOF__ 23 | } 24 | 25 | # == Parse Options == 26 | ONERR=false 27 | REVSET= 28 | RESTORE_DESCENDANTS= 29 | while test $# -ne 0 ; do 30 | case "$1" in \ 31 | -E) ONERR=true ;; 32 | -h|--help) show_help; exit 0 ;; 33 | --restore-descendants) RESTORE_DESCENDANTS=--restore-descendants ;; 34 | --) shift ; break ;; 35 | -*) die "unknown option: $1" ;; 36 | *) test -z "$REVSET" && REVSET="$1" || break ;; 37 | esac 38 | shift 39 | done 40 | test -n "$REVSET" || { 41 | echo "${BASH_SOURCE[0]##*/}: missing " >&2 42 | show_help 43 | exit 1 44 | } 45 | test -n "$*" || die "missing " 46 | # COMMAND == "$@" 47 | 48 | # == failsafe == 49 | START_OP=$(jj op log -n1 --no-graph -T 'self.id().short()') 50 | echo ">> Command to undo script effects:" >&2 51 | echo " jj op restore $START_OP" >&2 52 | 53 | # == Save Workgion Copy == 54 | AT_HEAD_ID=$(jj --no-pager --ignore-working-copy log --color=never --no-graph -T change_id -r @) 55 | 56 | # == find commit IDs == 57 | readarray -t COMMITIDS < <( jj --no-pager --ignore-working-copy log --no-graph --color=never -T 'commit_id ++ "\n"' -r "$REVSET" ) 58 | 59 | # == run commands == 60 | jj new @ # keeps $AT_HEAD_ID alive even if empty 61 | for CID in "${COMMITIDS[@]}" ; do 62 | ( set -xe 63 | # prepare for changes 64 | jj new "$CID" 65 | # run command, then integrate or abandon changes 66 | ("$@") || $ONERR && 67 | jj restore --from @ --to @- $RESTORE_DESCENDANTS || 68 | jj abandon @ 69 | ) 70 | done 71 | ERR="$?" 72 | 73 | # == Restore Workgion Copy == 74 | jj edit "$AT_HEAD_ID" 75 | 76 | # == Recovery Msg == 77 | if test "$ERR" == 0 ; then 78 | echo "# Rewrite done, exit_status=$ERR" >&2 79 | else 80 | echo "# Rewrite failed, exit_status=$ERR" >&2 81 | fi 82 | echo "# To rewind, use:" >&2 83 | echo " jj op restore $START_OP" >&2 84 | -------------------------------------------------------------------------------- /screencasts/splitting.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail # -x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=`readlink -f "$0"` 6 | SCRIPTDIR="${ABSPATHSCRIPT%/*}" 7 | 8 | 9 | # == functions and setup for screencasts == 10 | source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*} 11 | # fast_timings 12 | 13 | # SCRIPT 14 | make_repo -squashall SplittingDemo gitdev jjdev 15 | start_asciinema SplittingDemo 'jj-fzf' Enter 16 | 17 | X 'When the working copy has lots of changes in lots of files...' 18 | 19 | # SPLIT FILES 20 | X 'Alt+F can split the current revision into one commit per file' 21 | K M-f; P 22 | K Down; P; K Down; P; K Down; P 23 | K Up ; P; K Up ; P; K Up ; P 24 | 25 | # DESCRIBE 26 | K Down; P 27 | X 'Ctrl+D opens the text editor to describe the commit' 28 | K C-d; S; K End; P 29 | T 'marker left by jj'; P 30 | K C-x; P # nano 31 | 32 | # ABANDON 33 | K Down; P 34 | X 'Alt+A abandons a commit' 35 | K M-a; P 36 | 37 | # SPLIT INTERACTIVELY 38 | K Home; P 39 | X 'Alt+I starts `jj split` interactively' 40 | X 'Use Mouse Clicks to explore the interactive editor' 41 | K M-i 42 | P 43 | tmux send-keys -H 1b 5b 4d 20 24 21 1b 5b 4d 23 24 21 # FILE 44 | P 45 | tmux send-keys -H 1b 5b 4d 20 2a 21 1b 5b 4d 23 2a 21 # EDIT 46 | P 47 | tmux send-keys -H 1b 5b 4d 20 32 21 1b 5b 4d 23 32 21 # SELECT 48 | P 49 | tmux send-keys -H 1b 5b 4d 20 3a 21 1b 5b 4d 23 3a 21 # VIEW 50 | P 51 | tmux send-keys -H 1b 5b 4d 20 3a 21 1b 5b 4d 23 3a 21 # VIEW (hides) 52 | P 53 | T 'F'; P 54 | K Down 55 | K Down 56 | K Enter 57 | K Enter 58 | K Enter 59 | K Enter 60 | K Enter 61 | K Enter; P 62 | T 'ac'; P 63 | X 'With the diff split up, each commit can be treated individually' 64 | 65 | # DESCRIBE 66 | K Down; P 67 | K C-d; S; K End; P 68 | T 'add brief description'; P 69 | K C-x; P # nano 70 | 71 | # DESCRIBE 72 | K Up; P 73 | K C-d; S; K End; P 74 | T 'add front-matter + date'; P 75 | K C-x; P # nano 76 | 77 | # DIFF-EDIT 78 | X 'Alt+E starts `jj diffedit` to select diff hunks to keep' 79 | K M-e; P; 80 | K F; K a; K Down 3; K Space; P 81 | K c; P 82 | K C-d; S; K End; P 83 | K BSpace 7; P 84 | K C-x; P # nano 85 | 86 | # UNDO 87 | X 'Or, use Alt+Z Alt+Z to undo the last 2 steps and keep the old front-matter' 88 | K M-z ; P 89 | K M-z ; P 90 | 91 | # NEW 92 | K Home 93 | X 'Create a new, empty change with Ctrl+N to edit the next commit' 94 | K C-n; P 95 | 96 | # EXIT 97 | P 98 | stop_asciinema 99 | render_cast "$ASCIINEMA_SCREENCAST" 100 | -------------------------------------------------------------------------------- /screencasts/render.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | die() { echo "${0##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 4 | 5 | [[ "${BASH_SOURCE[0]}" = "${BASH_SOURCE[0]#/}" ]] && 6 | SCREENCASTSDIR="$PWD/${BASH_SOURCE[0]}" || SCREENCASTSDIR="${BASH_SOURCE[0]}" 7 | export SCREENCASTSDIR="${SCREENCASTSDIR%/*}" 8 | 9 | # Stop recording and render screencast output files 10 | render_screencast() 11 | { 12 | set -Eeuo pipefail # -x 13 | CASTFILE="$1" 14 | BASENAME="${CASTFILE%.cast}" 15 | # find "$BASENAME"* -printf "%8kk %p\n" 16 | test -r "$BASENAME.cast" || 17 | die "render_screencast: missing $BASENAME.cast" 18 | rm -f $BASENAME.mp4 $BASENAME.webp $BASENAME.gif $BASENAME.apng 19 | printf ' %-8s %s\n' RENDER $BASENAME 20 | # sed '$,/"\[exited]/d' "$BASENAME.cast" 21 | # asciinema-agg 22 | local ARGS=( 23 | # --idle-time-limit 1 24 | # --fps-cap 60 25 | # --renderer resvg 26 | --renderer fontdue # good in agg-1.4.3 27 | --font-family "Fira Code Retina" --font-dir /usr/share/fonts/truetype/firacode/ 28 | #--font-family "DejaVu Sans Mono" --font-dir /usr/share/fonts/truetype/dejavu/ 29 | #--font-family "Noto Mono" --font-dir /usr/share/fonts/truetype/noto/ 30 | #--font-family "Noto Sans Mono" --font-dir /usr/share/fonts/truetype/noto/ 31 | --font-dir $PWD 32 | --font-size 17 33 | --theme asciinema 34 | --speed 1 35 | ) 36 | test -z "$MAX_IDLE" || 37 | ARGS+=( --idle-time-limit "$MAX_IDLE" ) 38 | ( set -e -x 39 | agg "${ARGS[@]}" "$BASENAME.cast" "$BASENAME.gif" 40 | gif2webp "$BASENAME.gif" -min_size -metadata all -o "$BASENAME.webp" & p=$! 41 | # -preset placebo -preset veryslow -x264opts opencl 42 | ffmpeg -loglevel warning -stats -hwaccel auto -i "$BASENAME.gif" \ 43 | -c:v libx264 -crf 24 -tune animation -preset slower \ 44 | -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \ 45 | -y "$BASENAME.mp4" & f=$! 46 | wait -f $p && wait -f $f || exit $? 47 | ) 48 | command -V notify-send >/dev/null 2>&1 && 49 | notify-send -e -i system-run -t 5000 "Screencast ready: $BASENAME" 50 | find "$BASENAME"* -printf "%8kk %p\n" 51 | } 52 | 53 | # == Setup & Options == 54 | source "${ABSPATHSCRIPT%/*}"/lib/setup.sh # preflight.sh common.sh 55 | jjfzf_tempd # assigns $JJFZF_TEMPD 56 | MAX_IDLE= 57 | CASTS=() 58 | while test $# -ne 0 ; do 59 | case "$1" in \ 60 | -i) shift; MAX_IDLE="$1" ;; 61 | *) CASTS+=( "$1" ) ;; 62 | esac 63 | shift 64 | done 65 | test ${#CASTS[@]} -ge 1 || 66 | CASTS=($SCREENCASTSDIR/*.cast) 67 | 68 | # == Process == 69 | for f in "${CASTS[@]}" ; do 70 | render_screencast "$f" 71 | ls -l "${f%.cast}"* 72 | done 73 | -------------------------------------------------------------------------------- /lib/preview.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | ABSPATHSCRIPT=$(readlink -f "$0") # Resolve symlinks to find installdir 5 | 6 | # == Imports == 7 | # Import jj_show_diff and helpers 8 | source "${BASH_SOURCE[0]%/*}"/common.sh 9 | 10 | # == Preview Rendering == 11 | # Pattern to match revisions 12 | REVPAT='^[^a-z()0-9]*([k-xyz]{7,})([?]*)\ ' # line start, ignore --graph, parse revision letters, catch '??'-postfix 13 | # Pattern to match operation ids 14 | OPRPAT='^[^a-z0-9]*([0-9a-f]{9,})[?]*\ ' # line start, ignore --graph, parse hex letters, space separator 15 | case "${1-}" in 16 | preview_revision) 17 | # Render preview if revision is found 18 | if [[ " $2 " =~ $REVPAT ]] ; then # match beginning of jj log line 19 | REVISION="${BASH_REMATCH[1]}" 20 | if [[ "${BASH_REMATCH[2]}" == '??' ]] ; then # divergent change_id 21 | # https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id 22 | jj --no-pager --ignore-working-copy show -T builtin_log_oneline -r "${BASH_REMATCH[1]}" 2>&1 || : 23 | echo 24 | REVISION=$(echo " $2 " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || exit 0 # find likely commit id 25 | fi 26 | { jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$REVISION" 27 | jj_show_diff --color=always -T '"\n"' -r "$REVISION" 28 | } 2>&1 | head -n 3000 29 | fi # else no valid revision 30 | ;; 31 | preview_oplog) 32 | [[ " $2 " =~ $OPRPAT ]] && { 33 | jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always op log --no-graph -n 1 -T builtin_op_log_comfortable 34 | jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log -s -r .. # -T builtin_log_oneline 35 | } 36 | ;; 37 | preview_opshow) 38 | [[ " $2 " =~ $OPRPAT ]] && { 39 | jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always op log --no-graph -n 1 -T builtin_op_log_comfortable 40 | jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log --no-graph -s -r "@" 41 | jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always show -T ' "\n" ' -r "@" 42 | } 43 | ;; 44 | preview_oppatch) 45 | [[ " $2 " =~ $OPRPAT ]] && { 46 | jj --no-pager --ignore-working-copy --color=always op show -p "${BASH_REMATCH[1]}" 47 | } | head -n 3000 48 | ;; 49 | preview_opdiff) 50 | [[ " $2 " =~ $OPRPAT ]] && { 51 | jj --no-pager --ignore-working-copy --color=always op diff -f "${BASH_REMATCH[1]}" -t @ 52 | } 53 | ;; 54 | preview_evolog) 55 | [[ " $2 " =~ ' '$BIGHEXPAT' ' ]] && { 56 | jj --no-pager --ignore-working-copy evolog --color=always -n1 -p -T 'builtin_log_detailed(commit)' -r "${BASH_REMATCH[1]}" | 57 | head -n 3000 58 | } 59 | ;; 60 | esac 61 | 62 | # == Done == 63 | exit 0 64 | -------------------------------------------------------------------------------- /tests/basics.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | SCRIPTNAME="${0##*/}" && SCRIPTDIR="$(readlink -f "$0")" && SCRIPTDIR="${SCRIPTDIR%/*}" 5 | 6 | [[ " $* " =~ -x ]] && set -x 7 | 8 | source $SCRIPTDIR/utils.sh 9 | 10 | # == TESTS == 11 | test-functions-fail-early() 12 | ( 13 | cd_new_repo 14 | # Check `jj-fzf describe` does not continue with $EDITOR 15 | # once an invalid change_id has been encountered. 16 | export JJ_CONFIG='' EDITOR='echo ERRORINERROR' 17 | OUT="$(set +x; jj-fzf describe 'zzzzaaaa' 2>&1)" && E=$? || E=$? 18 | assert_nonzero $E 19 | assert1error "$OUT" 20 | ! grep -Eq 'ERRORINERROR' <<<"$OUT" || 21 | die "${FUNCNAME[0]}: detected nested invocation, output:"$'\n'"$(echo "$OUT" | sed 's/^/> /')" 22 | ) 23 | TESTS+=( test-functions-fail-early ) 24 | 25 | test-edit-new() 26 | ( 27 | cd_new_repo 28 | mkcommits 'Ia' 'Ib' 'Ia ->Ic' 'Ib|Ic ->Id' 29 | assert_commit_count $((2 + 4)) 30 | git tag IMMUTABLE `get_commit_id Id` && jj_status 31 | assert_commit_count $((2 + 5)) 32 | mkcommits A B 'A ->C' 'B|C ->D' 33 | assert_commit_count $((2 + 5 + 4)) 34 | jj-fzf edit 'C' >$DEVERR 2>&1 35 | assert_commit_count $((2 + 5 + 4)) 36 | assert_@ `get_commit_id C` && assert_@- `get_commit_id A` 37 | jj-fzf edit 'Ic' >$DEVERR 2>&1 38 | assert_commit_count $((2 + 5 + 4 + 1)) 39 | assert_@- `get_commit_id Ic` 40 | jj-fzf new '@' >$DEVERR 2>&1 41 | assert_commit_count $((2 + 5 + 4 + 1 + 1)) 42 | assert_commits_eq @-- `get_commit_id Ic` 43 | ) 44 | TESTS+=( test-edit-new ) 45 | 46 | test-undo-undo-redo() 47 | ( 48 | cd_new_repo 49 | mkcommits A B 'A ->C' 'B|C ->D' 'E' 50 | assert_commit_count $((2 + 5)) 51 | ( jj new -m U1 && jj new -m U2 && jj new -m U3 ) >$DEVERR 2>&1 52 | assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2` 53 | jj-fzf undo >$DEVERR 2>&1 && assert_commit_count $((2 + 5 + 2)) 54 | jj-fzf undo >$DEVERR 2>&1 && assert_commit_count $((2 + 5 + 1)) 55 | assert_@ `get_commit_id U1` && assert_@- `get_commit_id E` 56 | jj-fzf redo >$DEVERR 2>&1 57 | jj-fzf redo >$DEVERR 2>&1 58 | assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2` 59 | jj new >$DEVERR 2>&1 60 | assert_commit_count $((2 + 5 + 3 + 1)) 61 | jj-fzf undo >$DEVERR 2>&1 62 | assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2` 63 | jj-fzf redo >$DEVERR 2>&1 64 | assert_commit_count $((2 + 5 + 3 + 1)) 65 | ! jj-fzf redo >$DEVERR 2>&1 # NOP 66 | assert_commit_count $((2 + 5 + 3 + 1)) 67 | jj-fzf undo >$DEVERR 2>&1 68 | assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2` 69 | ) 70 | TESTS+=( test-undo-undo-redo ) 71 | 72 | # TODO: test-add-parent 73 | # TODO: test-multi-rebase 74 | 75 | # == RUN == 76 | temp_dir 77 | for TEST in "${TESTS[@]}" ; do 78 | $TEST 79 | printf ' %-7s %s\n' OK "$TEST" 80 | done 81 | tear_down 82 | -------------------------------------------------------------------------------- /screencasts/oplog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | readonly SCREENCAST_SESSION=oplog-demo 6 | source $(dirname $(readlink -f "${BASH_SOURCE[0]}"))/prepare.sh "$@" # for $TEMPD and funcs 7 | H=40 8 | 9 | # == Config == 10 | export JJ_CONFIG=$(make_jj_config) 11 | clone_jj_repo $TEMPD/$SCREENCAST_SESSION 12 | ( cd $TEMPD/$SCREENCAST_SESSION 13 | jj b t main@origin 14 | jj new -m Merge qylkzstz wvormzwm 15 | jj new 16 | jj abandon 17 | echo -e '\n## DemoSection' >> README.md 18 | jj st 19 | echo -e '\nThis MESSAGE was added for the oplog demo.' >> README.md 20 | jj describe -m 'README.md: add MESSAGE to DemoSection' 21 | ) > $TEMPD/bookmarks.log 2>&1 22 | 23 | SNAP1=$(cd $TEMPD/$SCREENCAST_SESSION && jj --ignore-working-copy log --no-graph -T commit_id -r @) 24 | 25 | # screencast_shell_setup && $SCREENCAST_SHELL ; exit 26 | 27 | D() ( K Down ) 28 | U() ( K Up ) 29 | Tab() ( K Tab ) 30 | 31 | # 1234567890123456789012345678901234567890123456789012345678901234567890123456 32 | 33 | # == SCRIPT == 34 | start_screencast $TEMPD/$SCREENCAST_SESSION \ 35 | 'jj-fzf' Enter 36 | 37 | # -- oplog -- 38 | X "Press Ctrl-O to view the operations behind the top commit to README.md" 39 | K C-o; S 40 | D; S 41 | X "Press Enter to view the details of the selected operation" 42 | K Enter; P; K q; S 43 | D; S 44 | X "These two operations added 'MESSAGE' and 'DemoSection' to the top commit" 45 | U; P 46 | D; P 47 | 48 | # -- inject -- 49 | X "Press Alt-J to inject the earlier operation as a separate commit" 50 | K M-j ; P 51 | D; P 52 | K C-d 53 | T 'README.md: add DemoSection'; S; K C-x; S 54 | X "The earlier snapshot has been turned into its own commit" 55 | 56 | # -- undo -- 57 | U; K C-n; S 58 | X 'Press Alt-Z to undo the most recent `jj new` command' 59 | K M-z; S; K M-z; P 60 | 61 | # -- redo -- 62 | X "Oops, that was too much undo, let's inspect the oplog and redo" 63 | K C-o; S 64 | X "Undo steps are indicated by the '⋯' marker in the oplog" 65 | D; S; D; S; D; P 66 | X 'Press Alt-Y to redo the `jj describe` step and restore the commit message' 67 | K M-y; P; K C-g; P 68 | 69 | SNAP2=$(cd $TEMPD/$SCREENCAST_SESSION && jj --ignore-working-copy log --no-graph -T commit_id -r @) 70 | 71 | # -- restore -- 72 | X "Enough trying, let's restore the repository to the initial state" 73 | K C-o; S 74 | D;D;D;D; D;D;D; P 75 | X 'Press Alt-R to restore the repository to the selected operation' 76 | K M-r; S; K C-g; P 77 | X "Repo restored - no commits were harmed in this demonstration" 78 | P 79 | 80 | # == EXIT == 81 | P; T ' ' 82 | stop_screencast 83 | 84 | SNAP3=$(cd $TEMPD/$SCREENCAST_SESSION && jj --ignore-working-copy log --no-graph -T commit_id -r @) 85 | 86 | ( cd $TEMPD/$SCREENCAST_SESSION 87 | test -n "$SNAP1" -a -n "$SNAP2" -a -n "$SNAP3" || 88 | die 'test snapshots failed' 89 | test "$SNAP1" != "$SNAP2" || 90 | die 'SNAP1 -> SNAP2 failed to make progress' 91 | test "$SNAP1" == "$SNAP3" || 92 | die 'SNAP1 -> SNAP3 failed to restore' 93 | ) 94 | 95 | printf ' %-8s %s\n' OK "$SCREENCAST_SESSION passed" 96 | -------------------------------------------------------------------------------- /lib/draft.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | ABSPATHSCRIPT=$(readlink -f "$0") # Resolve symlinks to find installdir 5 | 6 | # == Imports == 7 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 8 | 9 | # == Options == 10 | KEEP=false 11 | HINT=false 12 | COMMIT= 13 | while test $# -ne 0 ; do 14 | case "$1" in \ 15 | --keep) KEEP=true ;; 16 | --hint) HINT=true ;; 17 | *) COMMIT="$1" ; break ;; 18 | esac 19 | shift 20 | done 21 | 22 | # jj config hint 23 | if $HINT ; then 24 | cat >&2 <<\__EOF 25 | # For commit message generation of a non-merge commit, jj-fzf used to list 26 | # edited files, add Signed-off-by and append a diff. In newer JJ versions, 27 | # this can all be configured via `jj config` templates. Note that using 28 | # commit_trailers may interfere with empty descriptiopn editing. 29 | # See also: https://jj-vcs.github.io/jj/latest/config/#default-description 30 | # The following config is similar to the old jj-fzf describe command: 31 | __EOF 32 | cat <<\__EOF 33 | [templates] 34 | draft_commit_description = ''' 35 | concat( 36 | coalesce( 37 | description, 38 | if(diff.files(), 39 | diff.files().map(|e| e.path().display()).join(', ') ++ ": \n") 40 | ++ default_commit_description ++ "\n" ++ 41 | format_signed_off_by_trailer(self) 42 | , "\n"), 43 | surround( 44 | "\nJJ: This commit contains the following changes:\n", "", 45 | indent("JJ: ", diff.stat(72)), 46 | ), 47 | "JJ: ignore-rest\n\n", 48 | diff.git(), 49 | ) 50 | ''' 51 | __EOF 52 | exit 0 53 | fi 54 | 55 | # == Draft Merge Commit Message == 56 | test -n "$COMMIT" || 57 | die "Missing commit" 58 | JJ='jj --no-pager --ignore-working-copy --color=never' 59 | 60 | # Keep existing description 61 | DESCRIPTION="$($JJ log --no-graph -r "$COMMIT" -T 'description')" 62 | if $KEEP && test -n "$DESCRIPTION" ; then 63 | printf "%s\n" "$DESCRIPTION" 64 | exit 0 65 | fi 66 | 67 | find_first_bookmark() 68 | ( 69 | $JJ log --no-graph -T 'concat(separate(" ",bookmarks), " ", change_id)' -r "$1" | 70 | awk '{print $1;}' 71 | ) 72 | 73 | # List parents 74 | PARENTS=( $($JJ log --no-graph -T 'commit_id ++ "\n"' -r "$COMMIT-" --reversed) ) 75 | 76 | # Output merge message 77 | if test "${#PARENTS[@]}" -ge 2 ; then 78 | FORK_POINT=$($JJ show --no-patch -r " fork_point( $COMMIT- ) " -T commit_id) 79 | 80 | if test "${#PARENTS[@]}" -eq 2 ; then 81 | echo "Merge branch '$(find_first_bookmark ${PARENTS[1]})' into '$(find_first_bookmark ${PARENTS[0]})'" 82 | else 83 | echo "Merge branches:" "${PARENTS[@]}" 84 | fi 85 | for c in "${PARENTS[@]}"; do 86 | test "$c" == "$FORK_POINT" && 87 | continue 88 | if test "${#PARENTS[@]}" -eq 2 ; then 89 | echo -e "\n* Branch commit log:" # "$c ^$FORK_POINT" 90 | else 91 | echo -e "\n* Branch '$(find_first_bookmark $c)' commit log:" 92 | fi 93 | $JJ log --no-graph -r "$FORK_POINT..$c" -T '"\x0c"++description++"\n"' | 94 | sed '/^\([A-Z][a-z0-9-]*-by\|Cc\):/d' | # strip Signed-off-by: 95 | sed '/^$/d ; s/^/\t/ ; s/^\t\f$/ (no description)/ ; s/^\t\f/ /' || : 96 | done 97 | # echo && $JJ log --no-graph -r "$COMMIT" -T ' format_signed_off_by_trailer(self) ' 98 | fi 99 | 100 | exit 0 101 | -------------------------------------------------------------------------------- /screencasts/rebasing.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=`readlink -f "$0"` 6 | SCRIPTDIR="${ABSPATHSCRIPT%/*}" 7 | 8 | # == functions and setup for screencasts == 9 | source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*} 10 | # fast_timings 11 | 12 | # CLONE REPO 13 | ( rm -rf dest 14 | git clone --no-hardlinks --single-branch --branch trunk $(cd $SCRIPTDIR && git rev-parse --git-dir) dest 15 | cd dest 16 | git update-ref refs/remotes/origin/trunk 97d796b 17 | git reset --hard 5265ff6 18 | jj git init --colocate 19 | jj b s trunk -r 97d796b --allow-backwards 20 | jj new -r f2c149e 21 | jj abandon 5265ff6 22 | jj b c splittingdemo -r 9325d16 23 | jj b c diffedit -r 685fd50 24 | jj b c homebrew-fixes -r c1512f4 25 | jj rebase -r splittingdemo -d f3b860c # -> -A -B 685fd50 26 | jj rebase -s homebrew-fixes- -d 8f18758 27 | jj new @- 28 | ) 29 | 30 | # SCRIPT 31 | start_asciinema dest 'jj-fzf' Enter 32 | 33 | # REBASE -r -A 34 | X 'To rebase commits, navigate to the target revision' 35 | K Down 10; P # splittingdemo 36 | X 'Alt+R starts the Rebase dialog' 37 | K M-r; P 38 | X 'Alt+B: --branch Alt+R: --revisions Alt+S: --source' 39 | K M-b; P; K M-s; P; K M-r; P; K M-b; P; K M-s; P; K M-r; P 40 | X 'Select destination revision' 41 | K Down 3; P # diffedit 42 | X 'Ctrl+A: --insert-after Ctrl+B: --insert-before Ctrl+D: --destination' 43 | K C-b; P; K C-a; P; K C-d; P; K C-b; P; K C-a; P 44 | X 'Enter: run `jj rebase` to rebase with --revisions --insert-after' 45 | K Enter; P 46 | X 'Revision "splittingdemo" was inserted *after* "diffedit"' 47 | P; P 48 | 49 | # UNDO 50 | X 'To start over, Alt+Z will undo the last rebase' 51 | K M-z; P 52 | P; P 53 | 54 | # REBASE -r -B 55 | X 'Alt+R starts the Rebase dialog' 56 | K M-r; P 57 | X 'Alt+B: --branch Alt+R: --revisions Alt+S: --source' 58 | K M-b; P; K M-s; P; K M-r; P; K M-b; P; K M-s; P; K M-r; P 59 | X 'Select destination revision' 60 | K Down 3; P # diffedit 61 | X 'Ctrl+A: --insert-after Ctrl+B: --insert-before Ctrl+D: --destination' 62 | K C-a; P; K C-b; P; K C-d; P; K C-a; P; K C-b; P 63 | X 'Enter: run `jj rebase` to rebase with --revisions --insert-before' 64 | K Enter; P 65 | X 'Revision "splittingdemo" was inserted *before* "diffedit"' 66 | P; P 67 | 68 | # REBASE -b -d 69 | X 'Select the "homebrew-fixes" bookmark to rebase' 70 | K Down 7; P # homebrew-fixes 71 | X 'Alt+R starts the Rebase dialog' 72 | K M-r; P 73 | X 'Keep `jj rebase --branch --destination` at its default' 74 | K Down; P # @- 75 | X 'Enter: rebase "homebrew-fixes" onto HEAD@git' 76 | K Enter PageUp; P 77 | X 'The "homebrew-fixes" branch was moved on top of HEAD@git' 78 | P; P 79 | 80 | # REBASE -s -d 81 | X 'Or, select a "homebrew-fixes" ancestry commit to rebase' 82 | K PageUp; K Down; P # homebrew-fixes- 83 | X 'Alt+R starts the Rebase dialog' 84 | K M-r; P 85 | X 'Use Alt+S for `jj rebase --source --destination` to rebase a subtree' 86 | K Down 9; P # @- 87 | K M-s; P 88 | X 'Enter: rebase the "homebrew-fixes" subtree onto "merge-commit-screencast"' 89 | K Enter; P 90 | K Down 7; P 91 | X 'The rebase now moved the "homebrew-fixes" parent commit and its descendants' 92 | P; P 93 | 94 | # EXIT 95 | P 96 | stop_asciinema 97 | render_cast "$ASCIINEMA_SCREENCAST" 98 | -------------------------------------------------------------------------------- /lib/evolog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | echo 'DIFF=1' > $JJFZF_TEMPD/evolog.env 11 | PRINTOUT= 12 | while test $# -ne 0 ; do 13 | case "$1" in \ 14 | --help-bindings) PRINTOUT="$1" ;; 15 | -x) set -x ;; 16 | *) break ;; 17 | esac 18 | shift 19 | done 20 | 21 | # == Config == 22 | TITLE='Evolution Log' 23 | export JJFZF_EVOLOG_SRC=$(jj --no-pager --ignore-working-copy log --no-graph -T commit_id -r "${1-@}") 24 | LONG_IDS="--config=template-aliases.'format_short_change_id(id)'='id.shortest(32)'" 25 | FOOTER="$TITLE for Change ID:"$'\n' 26 | FOOTER="$FOOTER"$(jj --no-pager --ignore-working-copy log --no-graph $LONG_IDS $JJFZF_COLOR -T 'format_short_change_id_with_hidden_and_divergent_info(self)' -r "${1-@}") 27 | jjfzf_log_detailed # for preview, assigns $JJFZF_LOG_DETAILED_CONFIG 28 | B=() H=() 29 | 30 | # == Bindings == 31 | RELOAD="reload-sync(cat $JJFZF_TEMPD/jjfzf_list)" # see jjfzf_load 32 | 33 | # Inject 34 | H+=( 'Alt-J: Inject the selected commit as historic diff while preserving the input revision.' ) 35 | B+=( --bind "alt-j:execute( jjfzf_run +x +e jjfzf_inject --diff '$JJFZF_EVOLOG_SRC' {2} )+$RELOAD+close+close+close" ) 36 | 37 | # Enter 38 | H+=( 'Enter: Show evolution of the change ID in the input revision up to the currently selected commit.' ) 39 | B+=( --bind 'enter:execute( jjfzf_evolog_info {2} | $JJFZF_PAGER )' ) 40 | 41 | # == Header Help == 42 | HEADER_HELP=$(printf "%s\n" "${H[@]}" | jjfzf_bold_keys) 43 | B+=( --header "$HEADER_HELP" ) 44 | 45 | # == jjfzf_evolog_info == 46 | # Show evolog info, diff and @ history 47 | jjfzf_evolog_info() 48 | ( 49 | set -Eeuo pipefail #-x 50 | COMMITID="$1" 51 | jj --no-pager --ignore-working-copy $JJFZF_COLOR evolog -p -r "$COMMITID" | 52 | sed '3001q' 53 | ) 54 | export -f jjfzf_evolog_info 55 | 56 | # == jjfzf_evolog0 == 57 | # Show `jj evolog` with 0-termination and extractable commit id 58 | jjfzf_evolog0() 59 | ( 60 | set -Eeuo pipefail 61 | JJEVOLOG="jj --no-pager --ignore-working-copy evolog -r $JJFZF_EVOLOG_SRC" 62 | TMPL=" '¸'++stringify(commit.commit_id().short(32))++'¸¸' ++ builtin_evolog_compact " 63 | $JJEVOLOG $JJFZF_COLOR -T "$TMPL" | 64 | sed '/¸¸/s/^/\x00/ ; 1s/^\x00//' 65 | ) 66 | export -f jjfzf_evolog0 67 | 68 | # == PRINTOUT == 69 | [[ "$PRINTOUT" == --help-bindings ]] && { 70 | for h in "${H[@]}" ; do 71 | echo "$h" | 72 | sed -r 's/^([^ ]+): *([^ ]+) *(.*)/\n### _\1_: **\2**\n\2 \3/' 73 | done 74 | echo 75 | exit 0 76 | } 77 | 78 | # == fzf == 79 | FZF_ARGS+=( 80 | --color=border:green,label:green 81 | --border-label "-[ ${TITLE^^} — JJ-FZF ]-" 82 | --preview-label " Evolution Info " 83 | --footer "${FOOTER}" 84 | --bind 'focus:+transform-ghost( R={2} && echo -n "${R:0:18}" )' 85 | --prompt 'EVOLOG > ' 86 | ) 87 | jjfzf_status 88 | export JJFZF_LOAD_LIST=jjfzf_evolog0 89 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 90 | jjfzf_load --stdout | 91 | fzf +m "${B[@]}" "${FZF_ARGS[@]}" \ 92 | --read0 '-d¸' --accept-nth=2 --with-nth '{1} {4..}' \ 93 | --preview 'jjfzf_evolog_info {2}' 94 | -------------------------------------------------------------------------------- /preflight.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | 4 | # == Strict mode & restore env == 5 | if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then # regular bash script 6 | set -Eeuo pipefail # strict mode 7 | __preflightish_fixenv='' 8 | # Enable debugging 9 | [[ " $* " =~ " -x " ]] && 10 | set -x 11 | else # sourced script 12 | __preflightish_fixenv=$'unset __preflightish_fixenv\n' 13 | # Enter strict mode, but beware to restore shopts on RETURN 14 | [[ $- == *e* ]] || __preflightish_fixenv="$__preflightish_fixenv"$' set +o errexit \n' 15 | set -e # Exit immediately on errors 16 | [[ $- == *u* ]] || __preflightish_fixenv="$__preflightish_fixenv"$' set +o nounset \n' 17 | set -u # Treat unset variables as error 18 | shopt -qo errtrace || __preflightish_fixenv="$__preflightish_fixenv"$' set +o errtrace \n' 19 | set -E # Any trap on ERR is inherited by functions and subshells 20 | shopt -qo pipefail || __preflightish_fixenv="$__preflightish_fixenv"$' set +o pipefail \n' 21 | set -o pipefail # Return value of a pipeline is 0 only if all commands in the pipeline exit 0 22 | fi 23 | 24 | # == __preflightish_die == 25 | __preflightish_die() { echo "$0: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 26 | __preflightish_fixenv="$__preflightish_fixenv"$' unset -f __preflightish_die \n' 27 | 28 | # == __preflightish_le == 29 | # Fast version comparison in pure bash: returns 0 if $1 <= $2 30 | __preflightish_le() 31 | { # compare V1.M1.P1 <= V2.M2.P2 for each segment 32 | local IFS=. 33 | local -a a=($1) b=($2) 34 | # pad shorter array with zeros 35 | while ((${#a[@]} < ${#b[@]})); do a+=(0); done 36 | while ((${#b[@]} < ${#a[@]})); do b+=(0); done 37 | # compare segments 38 | for ((i=0; i<${#a[@]}; i++)); do 39 | ((10#${a[i]} < 10#${b[i]})) && return 0 40 | ((10#${a[i]} > 10#${b[i]})) && return 1 41 | done 42 | return 0 43 | } 44 | 45 | # == __preflightish_require == 46 | __preflightish_require() # VERSION COMMAND [ARGS...] 47 | { 48 | local VV REQ="$1" && shift 49 | command -v "$1" > /dev/null 2>&1 && 50 | VV=$("$@" | sed 's/^[^0-9]*//; s|\([0-9.]*\).*|\1|') && 51 | __preflightish_le "$REQ" "$VV" || 52 | __preflightish_die "Failed to find '$1' >= $REQ in \$PATH" 53 | } 54 | 55 | # == Bash == 56 | # bash 5.1 introduced $SRANDOM 57 | bash -c '[[ -n ${SRANDOM+set} ]]' || 58 | __preflightish_die "Failed to detect 'bash' >= 5.1 in \$PATH" 59 | [[ "`bash -c 'set -o'`" =~ emacs ]] || 60 | __preflightish_die "The 'bash' executable lacks interactive readline support" 61 | 62 | # == sed == 63 | if ! declare -F sed >/dev/null; then # ignore existing sed() compat func 64 | if ! sed --version 2>/dev/null | grep -Fq 'GNU sed' ; then 65 | # sed is not GNU 66 | if gsed --version 2>/dev/null | grep -Fq 'GNU sed' ; then 67 | # use gsed instead of sed 68 | sed() { gsed "$@"; } 69 | export -f sed 70 | else 71 | __preflightish_die "Failed to find GNU sed as 'sed' or 'gsed'" 72 | fi 73 | fi 74 | fi 75 | 76 | # == awk == 77 | command -v "awk" > /dev/null 2>&1 && 78 | test $(awk 'BEGIN{print(123)}') == 123 || 79 | __preflightish_die "Failed to find usable 'awk' executable in \$PATH" 80 | 81 | # == Jujutsu == 82 | __preflightish_require "0.34" jj --version --ignore-working-copy 83 | 84 | # == fzf == 85 | __preflightish_require "0.65.2" fzf --version 86 | 87 | # == python3 == 88 | __preflightish_require "3.9" python3 --version 89 | 90 | # == column == 91 | command -v "column" > /dev/null 2>&1 || 92 | __preflightish_die "Failed to find the 'column' executable in \$PATH" 93 | 94 | # == Success == 95 | [[ "${BASH_SOURCE[0]}" == "$0" ]] && 96 | echo " OK All preflight.sh checks passed" 97 | eval "$__preflightish_fixenv" # Restore shell options 98 | true # otherwise exit status from above could apply 99 | -------------------------------------------------------------------------------- /tests/utils.sh: -------------------------------------------------------------------------------- 1 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 2 | 3 | # == Check Dependencies == 4 | jj --help >/dev/null 5 | PATH="$SCRIPTDIR/..:$PATH" # ensure jj-fzf is in $PATH 6 | jj-fzf --version >/dev/null 7 | 8 | # == VARIABLE Setup == 9 | export JJ_FZF_ERROR_DELAY=0 # instant errors for testing 10 | TEMPD= 11 | 12 | # == OPTIONS == 13 | DEVERR=/dev/null 14 | [[ " $* " =~ -x ]] && { 15 | PS4="+ \${BASH_SOURCE[0]##*/}:\${LINENO}: " 16 | DEVERR=/dev/stderr 17 | set -x 18 | } 19 | [[ " $* " =~ -v ]] && 20 | DEVERR=/dev/stderr 21 | 22 | # == Utils == 23 | die() 24 | { 25 | local R=$'\033[31m' Z=$'\033[0m' 26 | [ -n "$*" ] && 27 | echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}:${FUNCNAME[1]}: $R**ERROR**:$Z ${*:-aborting}" >&2; 28 | exit 127 29 | } 30 | die-() # die, using *caller* as origin 31 | { 32 | local R=$'\033[31m' Z=$'\033[0m' 33 | [ -n "$*" ] && 34 | echo "${BASH_SOURCE[2]}:${BASH_LINENO[1]}:${FUNCNAME[2]}: $R**ERROR**:$Z ${*:-aborting}" >&2; 35 | exit 127 36 | } 37 | temp_dir() 38 | { 39 | test -n "$TEMPD" || { 40 | TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed" 41 | trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM 42 | echo "$$" > $TEMPD/jjfzf-tests.pid 43 | } 44 | } 45 | 46 | # == Repository == 47 | tear_down() 48 | ( 49 | REPO="${1:-repo}" 50 | test -n "$TEMPD" && 51 | rm -rf $TEMPD/$REPO 52 | ) 53 | clear_repo() 54 | ( 55 | REPO="${1:-repo}" 56 | test -n "$TEMPD" || die "missing TEMPD" 57 | cd $TEMPD/ 58 | rm -rf $TEMPD/$REPO 59 | mkdir $TEMPD/$REPO 60 | cd $TEMPD/$REPO 61 | git init >$DEVERR 2>&1 62 | jj git init --colocate >$DEVERR 2>&1 63 | echo "$PWD" 64 | ) 65 | cd_new_repo() 66 | { 67 | RP=$(clear_repo "$@") 68 | cd "$RP" 69 | } 70 | mkcommits() 71 | ( # Create empty test commits with bookamrks 72 | while test $# -ne 0 ; do 73 | P=@ && [[ "$1" =~ (.+)-\>(.+) ]] && 74 | P="${BASH_REMATCH[1]}" C="${BASH_REMATCH[2]}" || C="$1" 75 | shift 76 | jj --no-pager new -m="$C" -r all:"$P" 77 | jj bookmark set -r @ "$C" 78 | done >$DEVERR 2>&1 # mkcommits A B 'A|B ->C' 79 | ) 80 | get_commit_id() 81 | ( 82 | REF="$1" 83 | COMMIT_ID=$(jj --ignore-working-copy log --no-graph -T commit_id -r "description(exact:\"$REF\n\")" 2>/dev/null) && 84 | test -n "$COMMIT_ID" || 85 | COMMIT_ID=$(jj --ignore-working-copy log --no-graph -T commit_id -r "$REF") || exit 86 | echo "$COMMIT_ID" 87 | ) 88 | get_change_id() 89 | ( 90 | COMMIT_ID=$(get_commit_id "$@") 91 | UNIQUECHANGE='if(self.divergent(), "", change_id)' 92 | # only allow non-divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id 93 | CHANGE_ID=$(jj --ignore-working-copy log --no-graph -T "$UNIQUECHANGE" -r " $COMMIT_ID ") || exit 94 | echo "$CHANGE_ID" 95 | ) 96 | commit_count() 97 | ( 98 | R="${1:-::}" 99 | jj --ignore-working-copy log --no-graph -T '"\n"' -r "$R" | wc -l 100 | ) 101 | jj_log() 102 | ( 103 | jj --ignore-working-copy log -T builtin_log_oneline -r :: 104 | ) 105 | jj_status() 106 | ( 107 | jj status >$DEVERR 2>&1 108 | ) 109 | 110 | # == Assertions == 111 | assert_commit_count() 112 | ( 113 | V="$1" 114 | C="$(commit_count "${2:-::}")" 115 | test "$C" -eq "$V" || 116 | die- "assert_commit_count: mismatch: $C == $V" 117 | ) 118 | assert_@() 119 | ( 120 | V="$1" 121 | C="$(get_change_id '@')" 122 | test "$C" == "$V" && return 123 | C="$(get_commit_id '@')" 124 | test "$C" == "$V" && return 125 | die- "assert_@: mismatch: $C == $V" 126 | ) 127 | assert_@-() 128 | ( 129 | V="$1" 130 | C="$(get_change_id '@-')" 131 | test "$C" == "$V" && return 132 | C="$(get_commit_id '@-')" 133 | test "$C" == "$V" && return 134 | die- "assert_@-: mismatch: $C == $V" 135 | ) 136 | assert_commits_eq() 137 | ( 138 | U="$1" 139 | V="$2" 140 | C="$(get_commit_id "$U")" 141 | D="$(get_commit_id "$V")" 142 | test "$C" == "$D" || 143 | die- "assert_commits_eq: mismatch: $C == $D" 144 | ) 145 | assert_nonzero() 146 | { 147 | V="$1" 148 | test 0 != "$V" || 149 | die- "assert_nonzero: mismatch: 0 != $V" 150 | } 151 | assert_zero() 152 | { 153 | V="$1" 154 | test 0 == "$V" || 155 | die- "assert_zero: mismatch: 0 == $V" 156 | } 157 | assert0error() 158 | { 159 | ! grep -Eq '\bERROR:' <<<"$*" || 160 | die- "assert0error: unexpected ERROR message: $*" 161 | } 162 | assert1error() 163 | { 164 | grep -Eiq '\bERROR:' <<<"$*" || 165 | die- "assert1error: missing mandatory ERROR message: $*" 166 | } 167 | 168 | # == Errors == 169 | bash_error() 170 | { 171 | local code="$?" D=$'\033[2m' Z=$'\033[0m' 172 | echo "$D${BASH_SOURCE[1]}:${BASH_LINENO[0]}:${FUNCNAME[1]}:trap: exit status: $code$Z" >&2 173 | exit "$code" 174 | } 175 | trap 'bash_error' ERR 176 | -------------------------------------------------------------------------------- /lib/common.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | 4 | # == Install Path == 5 | [[ "${BASH_SOURCE[0]}" = "${BASH_SOURCE[0]#/}" ]] && 6 | ABSPATHLIB="$PWD/${BASH_SOURCE[0]}" || ABSPATHLIB="${BASH_SOURCE[0]}" 7 | ABSPATHLIB="${ABSPATHLIB%/*}" 8 | 9 | # == JJFZF_PRIVATE == 10 | JJFZF_PRIVATE_CONFIG="" 11 | # Try to read a revset name from git.private-commits, then add a marker "🌟" to all "private" commits in logs 12 | if JJFZF_PRIVATE="$(jj config get --ignore-working-copy --no-pager git.private-commits 2>/dev/null)" && 13 | [[ "$JJFZF_PRIVATE" =~ ^[.a-z_()-]+$ ]] ; then 14 | JJFZF_PRIVATE_CONFIG="--config=template-aliases.'format_short_commit_id(id)'='format_short_id(id) ++ if(self.contained_in(\"$JJFZF_PRIVATE\") && ! immutable, label(\"committer\", \" 🌟\"))'" 15 | else 16 | JJFZF_PRIVATE='' # only supports unquoted revset names 17 | fi 18 | export JJFZF_PRIVATE_CONFIG JJFZF_PRIVATE 19 | 20 | # == JJ_FZF_SHOWDETAILS == 21 | # extended version of builtin_log_detailed; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml 22 | JJ_FZF_SHOWDETAILS=' 23 | concat( 24 | builtin_log_oneline, 25 | "Change ID: " ++ self.change_id() ++ "\n", 26 | "Commit ID: " ++ commit_id ++ "\n", 27 | "Flags: ", separate(" ", 28 | if(immutable, label("node immutable", "immutable")), 29 | if(hidden, label("hidden", "hidden")), 30 | if(divergent, label("divergent", "divergent")), 31 | if(conflict, label("conflict", "conflict")), 32 | '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', 'private')), }"' 33 | ) ++ "\n", 34 | surround("Refs: ", "\n", separate(" ", local_bookmarks, remote_bookmarks, tags)), 35 | "Parents: " ++ self.parents().map(|c| " " ++ c.change_id()) ++ "\n", 36 | "Author: " ++ format_detailed_signature(author) ++ "\n", 37 | "Committer: " ++ format_detailed_signature(committer) ++ "\n\n", 38 | indent(" ", 39 | coalesce(description, label(if(empty, "empty"), description_placeholder) ++ "\n")), 40 | "\n", 41 | )' 42 | 43 | # == Hex number patterns == 44 | BIGHEXPAT='\b([0-9a-f]{18,})\b' # long hexadecimal pattern 45 | # Find any hex pattern, 7 digits or longer 46 | HEX7PAT='\ ([0-9a-f]{7,})\ ' # space enclosed hexadecimal pattern 47 | 48 | # == Oneline == 49 | # TODO: have a JJ command that allows to query for the builtin_log_oneline template 50 | # Copied from jj/cli/src/config/templates.toml in jj-0.33, changes: 51 | # - print commit.commit_id before tags, etc 52 | # - print long form commit id (24 characters) 53 | ONELINE_COMMIT_BIGHEX='concat( 54 | if(commit.root(), 55 | format_root_commit(commit), 56 | label( 57 | separate(" ", 58 | if(commit.current_working_copy(), "working_copy"), 59 | if(commit.immutable(), "immutable", "mutable"), 60 | if(commit.conflict(), "conflicted"), 61 | ), 62 | concat( 63 | separate(" ", 64 | format_short_change_id_with_hidden_and_divergent_info(commit), 65 | format_short_signature_oneline(commit.author()), 66 | format_timestamp(commit_timestamp(commit)), 67 | commit.commit_id().short(20), 68 | commit.bookmarks(), 69 | commit.tags(), 70 | commit.working_copies(), 71 | if(commit.git_head(), label("git_head", "git_head()")), 72 | if(commit.conflict(), label("conflict", "conflict")), 73 | if(config("ui.show-cryptographic-signatures").as_boolean(), 74 | format_short_cryptographic_signature(commit.signature())), 75 | if(commit.empty(), label("empty", "(empty)")), 76 | if(commit.description(), 77 | commit.description().first_line(), 78 | label(if(commit.empty(), "empty"), description_placeholder), 79 | ), 80 | ) ++ "\n", 81 | ), 82 | ) 83 | ) )' 84 | 85 | # == Diff rendering == 86 | # Show commit diff according to jj-fzf.diff-mode 87 | jj_show_diff() 88 | { 89 | local COLOR && [[ " $* " =~ --color=always ]] && COLOR=--color=always || COLOR=--color=never 90 | # Use git-diff (for --git and --word-diff) which has better heuristics for informative hunk headers than jj-0.33 91 | case "$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true)" in 92 | diff-b) 93 | export EXECTOOL_CMD='git -P diff --no-index --diff-algorithm=histogram -b '"$COLOR" 94 | jj show --no-pager --ignore-working-copy "$@" --tool "$ABSPATHLIB/exectool.sh" 95 | ;; 96 | word-b) 97 | export EXECTOOL_CMD='git -P diff --no-index --diff-algorithm=histogram -b --word-diff '"$COLOR" 98 | jj show --no-pager --ignore-working-copy "$@" --tool "$ABSPATHLIB/exectool.sh" 99 | ;; 100 | *) 101 | jj show --no-pager --ignore-working-copy "$@" 102 | ;; 103 | esac 104 | } 105 | -------------------------------------------------------------------------------- /lib/oplog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | echo 'DIFF=1' > $JJFZF_TEMPD/oplog.env 11 | PRINTOUT= 12 | while test $# -ne 0 ; do 13 | case "$1" in \ 14 | --help-bindings) PRINTOUT="$1" ;; 15 | -x) set -x ;; 16 | *) break ;; 17 | esac 18 | shift 19 | done 20 | 21 | # == Config == 22 | TITLE='Operation Log' 23 | jjfzf_log_detailed # for preview, assigns $JJFZF_LOG_DETAILED_CONFIG 24 | B=() H=() 25 | 26 | # == Bindings == 27 | RELOAD="reload-sync(cat $JJFZF_TEMPD/jjfzf_list)" # see jjfzf_load 28 | 29 | # Inject 30 | H+=( 'Alt-J: Inject working copy of the selected operation as historic commit before @' ) 31 | B+=( --bind "alt-j:execute( jjfzf_op_inject {2} ; jjfzf_load_and_status )+$RELOAD+close+close+close" ) 32 | jjfzf_op_inject() 33 | ( 34 | set -Eeuo pipefail 35 | COMMIT="$(jj --no-pager --ignore-working-copy --at-op "$1" show --tool true -T commit_id -r @)" 36 | jjfzf_inject --tree @ "$COMMIT" 37 | ) 38 | export -f jjfzf_op_inject 39 | 40 | # Restore operation 41 | H+=( 'Alt-R: Restore repository to the selected operation via `jj op restore`' ) 42 | B+=( --bind "alt-r:execute( jjfzf_run jj --no-pager op restore {2} )+$RELOAD+down" ) 43 | 44 | # Revert operation 45 | H+=( 'Alt-V: Revert the effects of the selected operation via `jj op revert`' ) 46 | B+=( --bind "alt-v:execute( jjfzf_run jj --no-pager op revert {2} )+$RELOAD+down" ) 47 | 48 | # Redo 49 | H+=( 'Alt-Y: Redo the last undo operation (marked `⋯`)' ) 50 | B+=( --bind "alt-y:execute( jjfzf_run jj --no-pager redo )+$RELOAD+down" ) 51 | 52 | # Undo 53 | H+=( 'Alt-Z: Undo the next operation (not already marked `⋯`)' ) 54 | B+=( --bind "alt-z:execute( jjfzf_run jj --no-pager undo )+$RELOAD+down" ) 55 | 56 | # Enter 57 | H+=( 'Enter: Info browser for the selected operation' ) 58 | B+=( --bind 'enter:execute( jjfzf_op_info {2} | $JJFZF_PAGER )' ) 59 | 60 | # == Header Help == 61 | HEADER_HELP=$(printf "%s\n" "${H[@]}" | jjfzf_bold_keys) 62 | B+=( --header "$HEADER_HELP" ) 63 | 64 | # == jjfzf_op_info == 65 | # Show operation info, diff and @ history 66 | jjfzf_op_info() 67 | ( 68 | set -Eeuo pipefail #-x 69 | OPID="$1" 70 | jj --no-pager --ignore-working-copy $JJFZF_COLOR op show -p "$OPID" 71 | echo 72 | echo 73 | echo "jj --at-operation=$OPID log -p -r ..@" 74 | jj --no-pager --ignore-working-copy $JJFZF_COLOR --at-operation="$OPID" log -p -r ..@ | 75 | sed '3001q' 76 | ) 77 | export -f jjfzf_op_info 78 | 79 | # == jjfzf_operation_id_resolve == 80 | # Resolve operations by following undo/redo steps 81 | jjfzf_operation_id_resolve() 82 | ( 83 | op_id="${1-@}" 84 | while :; do 85 | next=$( 86 | jj --no-pager --ignore-working-copy op show --color=never --no-graph --no-op-diff -T "self.id() ++ ' ' ++ self.description().first_line()" "$op_id" | 87 | # Detect "restore to operation" indirection 88 | sed -rn 's/.*\brestore to operation ([0-9a-f]{32,}).*/\1/p' 89 | ) 90 | [ -z "$next" ] && break # Followed all indirections 91 | op_id="$next" 92 | done 93 | echo "$op_id" 94 | ) 95 | export -f jjfzf_operation_id_resolve 96 | 97 | # == jjfzf_oplog0 == 98 | # Show `jj op log` but mark undone operations with '⋯' 99 | jjfzf_oplog0() 100 | ( 101 | set -Eeuo pipefail 102 | JJOPLOG="jj --no-pager --ignore-working-copy op log" 103 | # Determine range of undo operations 104 | LAST_OPID=$(jjfzf_operation_id_resolve @) 105 | TMPL=" '¸'++stringify(self.id().short(32))++'¸¸' ++ builtin_op_log_compact " 106 | if test "$LAST_OPID" != @ ; then 107 | $JJOPLOG $JJFZF_COLOR -T "$TMPL" | 108 | sed -r "1,/${LAST_OPID:0:32}¸¸/{ /${LAST_OPID:0:32}¸¸/! s/([@~◆×○])/⋯/ }" # ⮌ ⋯ ⤺↶ 109 | else 110 | $JJOPLOG $JJFZF_COLOR -T "$TMPL" 111 | fi | 112 | sed '/¸¸/s/^/\x00/ ; 1s/^\x00//' 113 | ) 114 | export -f jjfzf_oplog0 115 | 116 | # == PRINTOUT == 117 | [[ "$PRINTOUT" == --help-bindings ]] && { 118 | for h in "${H[@]}" ; do 119 | echo "$h" | 120 | sed -r 's/^([^ ]+): *([^ ]+) *(.*)/\n### _\1_: **\2**\n\2 \3/' 121 | done 122 | echo 123 | exit 0 124 | } 125 | 126 | # == fzf == 127 | FZF_ARGS+=( 128 | --color=border:blue,label:blue 129 | --border-label "-[ ${TITLE^^} — JJ-FZF ]-" 130 | --preview-label " Operation Info " 131 | --footer "${TITLE}" 132 | --bind 'focus:+transform-ghost( R={2} && echo -n "${R:0:18}" )' 133 | --prompt 'OPLOG > ' 134 | ) 135 | jjfzf_status 136 | export JJFZF_LOAD_LIST=jjfzf_oplog0 137 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 138 | jjfzf_load --stdout | 139 | fzf +m "${B[@]}" "${FZF_ARGS[@]}" \ 140 | --read0 '-d¸' --accept-nth=2 --with-nth '{1} {4..}' \ 141 | --preview 'jjfzf_op_info {2}' 142 | -------------------------------------------------------------------------------- /screencasts/megamerge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail # -x 4 | SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=`readlink -f "$0"` 6 | SCRIPTDIR="${ABSPATHSCRIPT%/*}" 7 | 8 | # == functions and setup for screencasts == 9 | source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*} 10 | # fast_timings 11 | 12 | # CLONE REPO 13 | DIR=MegaMergeDemo 14 | ( rm -rf $DIR 15 | set -x 16 | git clone --no-hardlinks --single-branch --branch trunk $(cd $SCRIPTDIR && git rev-parse --git-dir) $DIR 17 | cd $DIR 18 | git update-ref refs/remotes/origin/trunk f2c149e 19 | git tag -d `git tag` 20 | # git reset --hard f2c149e 21 | jj git init --colocate 22 | jj b s trunk -r f2c149e --allow-backwards 23 | jj bookmark track trunk@origin 24 | jj new -r f2c149e 25 | jj b c two-step-duplicate-and-backout -r 7d3dae8 26 | jj abandon b19d586:: && jj rebase -s bf7fd9d -d f2c149e 27 | jj b c bug-fixes -r f93824e 28 | jj abandon 56a3cbb:: && jj rebase -s bed3bcd -d f2c149e 29 | jj abandon 249a167:: # jj b c screencast-scripts -r 69fd52e 30 | # jj abandon 4951884:: && jj rebase -s 249a167 -d f2c149e 31 | jj abandon 5cf1278:: # jj b c readme-screencasts -r 8c3d950 32 | # jj abandon 66eb19d:: && jj rebase -s 5cf1278 -d f2c149e 33 | jj b c homebrew-fixes -r c1512f4 34 | jj abandon 5265ff6:: 35 | jj new @- 36 | ) 37 | 38 | # SCRIPT 39 | start_asciinema $DIR 'jj-fzf' Enter 40 | X 'The "Mega-Merge" workflow operates on a selection of feature branches' 41 | 42 | # FIRST NEW 43 | X 'Use Ctrl+N to create a new commit based on a feature branch' 44 | K PageUp Down; P 45 | K C-n; P 46 | X 'Use Ctrl+D to give the Mega-Merge head a unique marker' 47 | K C-d 48 | T $'= = = = = = = =\n'; P; 49 | K C-x; P # nano 50 | 51 | # ADD PARENTS 52 | X 'Alt+P starts the Parent editor for the selected commit' 53 | K M-p; P 54 | X 'Alt+A and Alt+D toggle between adding and deleting parents' 55 | K M-d; P; K M-a; P; K M-d; P; K M-a; P 56 | X 'Pick branches and use Tab to add parents' 57 | #K Down; K Tab; P # readme-screencasts 58 | Q "two-step-duplicate-and-backout"; K Tab; P 59 | Q "bug-fixes"; K Tab; P 60 | Q "homebrew-fixes"; K Tab; P 61 | X 'Enter: run `jj rebase` to add the selected parents' 62 | K Enter; P 63 | X 'The working copy now contains 3 feature branches' 64 | 65 | # NEW COMMIT 66 | X 'Ctrl+N starts a new commit' 67 | K C-n; P 68 | X 'Ctrl+Z starts a subshell' 69 | K C-z; P 70 | T '(echo; echo "## Multi-merge") >>README.md && exit'; P; K Enter; P 71 | X 'Alt+C starts the text editor and creates a commit' 72 | K M-c; K End; P 73 | T 'start multi-merge section'; P 74 | K C-x; P # nano 75 | 76 | # ADD BRANCH 77 | K PageUp; K Down 2 78 | X 'Alt+N: Insert a new parent (adds a branch to merge commits)' 79 | K M-n; P 80 | Q "\ @\ " 81 | X 'Alt+B: Assign/move a bookmark to a commit' 82 | K M-b; T 'cleanup-readme'; P; K Enter 83 | 84 | # REBASE before 85 | K PageUp; P 86 | X 'Alt+R allows rebasing a commit into a feature branch' 87 | K M-r; 88 | X 'Use Alt+R and Ctrl+B to rebase a single revision before another' 89 | K M-r; P 90 | Q "\ @\ " # "cleanup-readme" 91 | K C-b; P 92 | X 'Enter: rebase with `jj rebase --revisions --insert-before`' 93 | K Enter; P 94 | 95 | # SQUASH COMMIT 96 | K PageUp 97 | X 'Ctrl+N starts a new commit' 98 | K C-n; P 99 | X 'Ctrl+Z starts a subshell' 100 | K C-z; P 101 | T '(echo; echo "Alt+P enables the Multi-Merge workflow.") >>README.md && exit'; P; K Enter; P 102 | K C-d End; P 103 | T 'describe Alt+P'; P 104 | K C-x; S # nano 105 | X 'The working copy changes can be squashed into a branch' 106 | Q "cleanup-readme"; P 107 | X 'Alt+W: squash the contents of the working copy into the selected revision' 108 | K M-w; P; P; 109 | X 'The commit now contains the changes from the working copy' 110 | K PageUp; P; 111 | X 'The working copy is now empty' 112 | 113 | # UPSTREAM-MERGE 114 | X "Let's merge the new branch into upstream and linearize history" 115 | Q "cleanup-readme"; P 116 | X 'Alt+M: start merge dialog' 117 | K M-m Down 3; P 118 | X 'Alt+U: upstream merge - add tracked bookmark to merge parents' 119 | K M-u; P 120 | X 'Enter: edit commit message and create upstream merge' 121 | K Enter; P; K C-k; P; K C-x; P # nano 122 | 123 | # REBASE MegaMerge head 124 | K PageUp; K Down 2; P 125 | X 'Alt+R: rebase the Mega-Merge head onto the working copy' 126 | K M-r; P 127 | X "Alt+P: simplify-parents after rebase to remove old parent edges" 128 | K M-p; P 129 | X "Enter: rebase onto 'trunk' and also simplify parents" 130 | K Enter; P 131 | 132 | # NEW 133 | K PageUp 134 | X 'Use Ctrl+N to prepare the next commit' 135 | K C-n; P 136 | 137 | # OUTRO 138 | X "The new feature can be pushed with 'trunk' and the Mega-Merge head is rebased" 139 | P; P 140 | 141 | # EXIT 142 | P 143 | stop_asciinema 144 | render_cast "$ASCIINEMA_SCREENCAST" 145 | ffmpeg -ss 00:02:32 -i megamerge.mp4 -frames:v 1 -q:v 2 -y megamerge230.jpg 146 | -------------------------------------------------------------------------------- /lib/reparent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | echo > $JJFZF_TEMPD/reparent.env 11 | PRINTOUT= 12 | while test $# -ne 0 ; do 13 | case "$1" in \ 14 | -x) set -x ;; 15 | --help-bindings) PRINTOUT="$1" ;; 16 | *) break ;; 17 | esac 18 | shift 19 | done 20 | 21 | # == Config == 22 | TITLE='Change Parents' 23 | export JJFZF_REPARENT_SRC="${1-@}" 24 | echo "OP='|'" >> $JJFZF_TEMPD/reparent.env 25 | echo 'SP=false' >> $JJFZF_TEMPD/reparent.env 26 | echo 'II=' >> $JJFZF_TEMPD/reparent.env 27 | 28 | # == Bindings == 29 | B=() H=() 30 | 31 | H+=( 'Alt-A: Add currently selected revisions as new parents' ) 32 | B+=( --bind "alt-a:execute-silent( sed 's/^OP=.*/OP=\"|\"/' -i $JJFZF_TEMPD/reparent.env )+refresh-preview" ) 33 | 34 | H+=( 'Alt-D: Delete selected revisions from list of existing parents' ) 35 | B+=( --bind "alt-d:execute-silent( sed 's/^OP=.*/OP=\"~\"/' -i $JJFZF_TEMPD/reparent.env )+refresh-preview" ) 36 | 37 | H+=( 'Alt-I: Ignore-immutable permits rebasing immutable commits' ) 38 | B+=( --bind "alt-i:execute-silent( sed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $JJFZF_TEMPD/reparent.env )+refresh-preview" ) 39 | 40 | H+=( 'Alt-P: Simplify-parents of the revision (after any rebasing)' ) 41 | B+=( --bind "alt-p:execute-silent( sed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $JJFZF_TEMPD/reparent.env )+refresh-preview" ) 42 | 43 | B+=( --bind 'enter:execute( jjfzf_handle_reparenting RUN {+2} )+close+close+close' ) 44 | B+=( --input-label " Enter: Run Reparenting Commands " ) 45 | 46 | # == jjfzf_reparent_list == 47 | # Determine new parent revset 48 | jjfzf_reparent_revset() 49 | ( 50 | set -Eeuo pipefail 51 | source $JJFZF_TEMPD/reparent.env 52 | MODIFY_PARENTS="( $(jjfzf_ccrevs "$@") )" 53 | OLD_PARENTS_REVSET="($JJFZF_REPARENT_SRC-)" 54 | if test "$OP" == '|' ; then 55 | NEW_PARENTS_REVSET="($MODIFY_PARENTS | $OLD_PARENTS_REVSET) ~ $JJFZF_REPARENT_SRC" 56 | else 57 | NEW_PARENTS_REVSET="$OLD_PARENTS_REVSET ~ $MODIFY_PARENTS" 58 | fi 59 | echo "$NEW_PARENTS_REVSET" 60 | ) 61 | export -f jjfzf_reparent_revset 62 | 63 | # == jjfzf_handle_reparenting == 64 | # Perform reparenting or print reparenting plan 65 | jjfzf_handle_reparenting() 66 | ( 67 | set -Eeuo pipefail 68 | source $JJFZF_TEMPD/reparent.env 69 | # Preview operation or run it 70 | if test "$1" == PREVIEW ; then 71 | PREVIEW=true 72 | pecho() { echo "$@" ; } 73 | run() { true ; } 74 | else 75 | test "$1" == RUN || exit 127 76 | PREVIEW=false 77 | pecho() { true ; } 78 | run() { jjfzf_run "$@" ; } 79 | fi 80 | shift # eat preview arg 81 | # determine old and new parents 82 | OLD_PARENTS_REVSET="($JJFZF_REPARENT_SRC-)" 83 | OLD_PARENTS_LIST=( $(jjfzf_chronological_change_ids "$OLD_PARENTS_REVSET") ) 84 | NEW_PARENTS_REVSET="$(jjfzf_reparent_revset "$@")" 85 | DEST_LIST=( $(jjfzf_chronological_change_ids "coalesce( $NEW_PARENTS_REVSET, root() )") ) 86 | pecho 87 | # reparent revisions 88 | if test " ${OLD_PARENTS_LIST[*]}" != " ${DEST_LIST[*]}" ; then 89 | pecho jj rebase $II --source "$JJFZF_REPARENT_SRC" "${DEST_LIST[@]/#/-d}" 90 | run +n jj rebase $II --source "$JJFZF_REPARENT_SRC" "${DEST_LIST[@]/#/-d}" 91 | else 92 | pecho "# No rebase needed:" "$JJFZF_REPARENT_SRC" 93 | fi 94 | pecho 95 | # simplify-parents 96 | if $SP; then 97 | pecho jj simplify-parents $II -r "$JJFZF_REPARENT_SRC" 98 | run +n jj simplify-parents $II -r "$JJFZF_REPARENT_SRC" 99 | else 100 | pecho 101 | fi 102 | if $PREVIEW ; then 103 | pecho 104 | pecho "ADD PARENTS:" 105 | if test "$OP" == '|' ; then 106 | jj --no-pager --ignore-working-copy log $JJFZF_COLOR --no-graph -T builtin_log_oneline -r "($NEW_PARENTS_REVSET) ~ ($OLD_PARENTS_REVSET)" --reversed | 107 | sed "s/^/+ /" 108 | fi 109 | pecho 110 | pecho "REMOVE PARENTS:" 111 | if test "$OP" != '|' ; then 112 | jj --no-pager --ignore-working-copy log $JJFZF_COLOR --no-graph -T builtin_log_oneline -r "($OLD_PARENTS_REVSET) & ~ ($NEW_PARENTS_REVSET)" --reversed | 113 | sed "s/^/- /" 114 | fi 115 | fi 116 | ) 117 | export -f jjfzf_handle_reparenting 118 | 119 | # == jjfzf_mark_revs == 120 | # Add marker to input revisions 121 | jjfzf_log0_marked_revs() 122 | ( 123 | set -Eeuo pipefail #-x 124 | echo > $JJFZF_TEMPD/reparent.sed 125 | echo "/$JJFZF_REPARENT_SRC/s/(¸¸)/\1 /" >> $JJFZF_TEMPD/reparent.sed 126 | jjfzf_log0 | 127 | sed -r -f $JJFZF_TEMPD/reparent.sed 128 | ) 129 | export -f jjfzf_log0_marked_revs 130 | 131 | # == jjfzf_header == 132 | # Help text 133 | export JJFZF_HELP=$(printf "%s\n" "${H[@]}" | jjfzf_bold_keys) 134 | jjfzf_header() 135 | ( 136 | set -Eeuo pipefail #-x 137 | echo "$JJFZF_HELP" 138 | ) 139 | export -f jjfzf_header 140 | 141 | # == PRINTOUT == 142 | [[ "$PRINTOUT" == --help-bindings ]] && { 143 | for h in "${H[@]}" ; do 144 | echo "$h" | 145 | sed -r 's/^([^ ]+): *([^ ]+) *(.*)/\n### _\1_: **\2**\n\2 \3/' 146 | done 147 | echo 148 | exit 0 149 | } 150 | 151 | # == fzf == 152 | FZF_ARGS+=( 153 | --color=border:yellow,label:yellow 154 | --border-label "-[ ${TITLE^^} — JJ-FZF ]-" 155 | --preview-label " Command " 156 | --bind 'focus:+transform-ghost( R={2} && echo -n "${R:0:12}" )' 157 | --prompt 'Parent > ' 158 | --bind "start,resize,alt-h:+transform-header: jjfzf_header " 159 | --bind "load:+toggle-preview-wrap" 160 | --footer "${TITLE}" 161 | ) 162 | test -z "${FZF_POS-}" || 163 | FZF_ARGS+=( --bind "load:+pos($FZF_POS)+unbind(load)" ) 164 | jjfzf_status 165 | export JJFZF_LOAD_LIST=jjfzf_log0_marked_revs 166 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 167 | jjfzf_load --stdout | 168 | fzf -m "${FZF_ARGS[@]}" "${B[@]}" \ 169 | --read0 '-d¸' --accept-nth=2 --with-nth '{1} {4..}' \ 170 | --preview 'jjfzf_handle_reparenting PREVIEW {+2}' 171 | -------------------------------------------------------------------------------- /lib/rebase.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | echo > $JJFZF_TEMPD/rebase.env 11 | PRINTOUT= 12 | while test $# -ne 0 ; do 13 | case "$1" in \ 14 | -x) set -x ;; 15 | --help-bindings) PRINTOUT="$1" ;; 16 | *) break ;; 17 | esac 18 | shift 19 | done 20 | 21 | # == Config == 22 | TITLE='Rebase & Duplicate' 23 | ARGS=("$@") 24 | export JJFZF_CREVS="$(jjfzf_ccrevs "$@")" # OR-combined revisions 25 | echo 'DP=' >> $JJFZF_TEMPD/rebase.env 26 | echo 'CH=' >> $JJFZF_TEMPD/rebase.env 27 | echo 'TO=--destination' >> $JJFZF_TEMPD/rebase.env 28 | echo 'SP=false' >> $JJFZF_TEMPD/rebase.env 29 | echo 'II=' >> $JJFZF_TEMPD/rebase.env 30 | echo 'WORDS=' >> $JJFZF_TEMPD/rebase.env 31 | if [[ $JJFZF_CREVS =~ \| ]] ; then 32 | echo 'FR=--revisions' >> $JJFZF_TEMPD/rebase.env # multi revs 33 | else 34 | echo 'FR=--source' >> $JJFZF_TEMPD/rebase.env # single rev 35 | fi 36 | jjfzf_status # snapshot dirty working tree 37 | B=() H=() 38 | 39 | # == Bindings == 40 | PDUP='change-prompt(Duplicate Target > )' 41 | PRBS='change-prompt(Rebase Target > )' 42 | B+=( --bind "start:$PRBS" ) 43 | 44 | H+=( "Alt-D: Duplicate — copies the specified revisions" ) 45 | B+=( --bind "alt-d:$PDUP+execute-silent( sed 's/^CH=.*/CH=/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--revisions/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 46 | 47 | H+=( "Alt-C: Children — duplicate the revisions with descendants" ) 48 | B+=( --bind "alt-c:$PDUP+execute-silent( sed 's/^CH=.*/CH=::/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--source/ ' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 49 | 50 | H+=( "Alt-B: Branch — rebase whole branches relative to destination's ancestors" ) 51 | B+=( --bind "alt-b:$PRBS+execute-silent( sed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--branch/ ' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 52 | 53 | H+=( "Alt-S: Source — rebase a revision together with descendants" ) 54 | B+=( --bind "alt-s:$PRBS+execute-silent( sed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--source/ ' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 55 | 56 | H+=( "Alt-R: Revision — rebase only given revisions, moves descendants onto parent" ) 57 | B+=( --bind "alt-r:$PRBS+execute-silent( sed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--revisions/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 58 | 59 | H+=( 'Alt-P: Simplify-Parents of the revisions after rebasing' ) 60 | B+=( --bind "alt-p:execute-silent( sed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 61 | 62 | H+=( 'Alt-I: Ignore-Immutable permits rebasing immutable commits' ) 63 | B+=( --bind "alt-i:execute-silent( sed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 64 | 65 | H+=( "Ctrl-A: After — pick the target to insert after" ) 66 | B+=( --bind "ctrl-a:execute-silent( sed 's/^TO=.*/TO=--insert-after/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 67 | 68 | H+=( "Ctrl-B: Before — pick the target to insert before" ) 69 | B+=( --bind "ctrl-b:execute-silent( sed 's/^TO=.*/TO=--insert-before/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 70 | 71 | H+=( "Ctrl-D: Destination — pick the target to rebase onto" ) 72 | B+=( --bind "ctrl-d:execute-silent( sed 's/^TO=.*/TO=--destination/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 73 | 74 | H+=( "Ctrl-W: Word-level merging — merge words not lines" ) 75 | B+=( --bind "ctrl-w:execute-silent( sed 's/^WORDS=-.*/WORDS=x/; s/^WORDS=$/WORDS=--config=merge.hunk-level=word/; s/^WORDS=x.*/WORDS=/' -i $JJFZF_TEMPD/rebase.env )+refresh-preview" ) 76 | 77 | # == Header Help == 78 | HEADER_HELP=$(printf "%s\n" "${H[@]}" | jjfzf_bold_keys) 79 | B+=( --header "$HEADER_HELP" ) 80 | 81 | # == jjfzf_rebase_enter == 82 | # Perform rebase or duplicate and simplify-parents 83 | jjfzf_rebase_enter() 84 | ( 85 | set -Eeuo pipefail #-x 86 | TARGET="$1" 87 | source $JJFZF_TEMPD/rebase.env 88 | # duplicate revisions 89 | if test -n "$DP" ; then 90 | jjfzf_run +n jj duplicate $II $WORDS $TO "$TARGET" -r "$JJFZF_CREVS$CH" 91 | else # rebase revisions 92 | jjfzf_run +n jj rebase $II $WORDS $TO "$TARGET" $FR "$JJFZF_CREVS" 93 | fi 94 | # simplify-parents 95 | if $SP; then 96 | jjfzf_run +n jj simplify-parents -r "$JJFZF_CREVS" 97 | fi 98 | ) 99 | export -f jjfzf_rebase_enter 100 | B+=( --bind 'enter:become( jjfzf_rebase_enter {2} )' ) 101 | B+=( --input-label " Enter: Run Rebase Commands " ) 102 | 103 | # == jjfzf_rebase_plan == 104 | # Planning of unconfirmed JJ command 105 | jjfzf_rebase_plan() 106 | ( 107 | set -Eeuo pipefail #-x 108 | TARGET="$1" 109 | source $JJFZF_TEMPD/rebase.env 110 | echo 111 | test -z "$DP" || { 112 | echo "jj duplicate $II $WORDS \\" 113 | echo " $TO $TARGET \\" 114 | echo " -r '$JJFZF_CREVS'$CH" 115 | } 116 | test -n "$DP" || { 117 | echo "jj rebase $II $WORDS \\" 118 | echo " $TO $TARGET \\" 119 | echo " $FR '$JJFZF_CREVS'" 120 | } 121 | echo 122 | test $SP == true && 123 | echo "jj simplify-parents -r '$JJFZF_CREVS'" 124 | echo 125 | T="${TO#--}" && echo "${T^^}:" && # TO 126 | jjfzf_oneline_graph -r "$TARGET" | sed q 127 | echo 128 | F="${FR#--}" && echo "${F^^}:" && # FROM 129 | jjfzf_oneline -r "$JJFZF_CREVS" | sed -r 's/^/ /' 130 | echo 131 | echo "COMMON:" && 132 | jjfzf_oneline_graph -r "heads( ::($JJFZF_CREVS) & ::$TARGET)" | sed q 133 | ) 134 | export -f jjfzf_rebase_plan 135 | B+=( --preview 'jjfzf_rebase_plan {2}' ) 136 | B+=( --preview-label " Commands " ) 137 | 138 | # == jjfzf_mark_revs == 139 | # Add marker to input revisions 140 | jjfzf_log0_marked_revs() 141 | ( 142 | set -Eeuo pipefail #-x 143 | echo > $JJFZF_TEMPD/rebase.sed 144 | for a in "${ARGS[@]}" ; do 145 | echo "/$a/s/(¸¸)/\1 /" >> $JJFZF_TEMPD/rebase.sed 146 | done 147 | jjfzf_log0 | 148 | sed -r -f $JJFZF_TEMPD/rebase.sed 149 | ) 150 | export -f jjfzf_log0_marked_revs 151 | 152 | # == PRINTOUT == 153 | [[ "$PRINTOUT" == --help-bindings ]] && { 154 | for h in "${H[@]}" ; do 155 | echo "$h" | 156 | sed -r 's/^([^ ]+): *([^ ]+) *(.*)/\n### _\1_: **\2**\n\2 \3/' 157 | done 158 | echo 159 | exit 0 160 | } 161 | 162 | # == fzf == 163 | FZF_ARGS+=( 164 | --color=border:magenta,label:magenta 165 | --border-label "-[ ${TITLE^^} — JJ-FZF ]-" 166 | --bind "load:+toggle-preview-wrap" 167 | --footer "${TITLE}" 168 | --bind 'focus:+transform-ghost( R={2} && echo -n "${R:0:12}" )' 169 | ) 170 | test -z "${FZF_POS-}" || 171 | FZF_ARGS+=( --bind "load:+pos($FZF_POS)+unbind(load)" ) 172 | jjfzf_status 173 | export JJFZF_LOAD_LIST=jjfzf_log0_marked_revs 174 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 175 | jjfzf_load --stdout | 176 | fzf +m "${FZF_ARGS[@]}" "${B[@]}" \ 177 | --read0 '-d¸' --accept-nth=2 --with-nth '{1} {4..}' 178 | -------------------------------------------------------------------------------- /Makefile.mk: -------------------------------------------------------------------------------- 1 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 2 | 3 | all: 4 | SHELL := /usr/bin/env bash -o pipefail 5 | version_full != ./version.sh 6 | version_bits := $(subst _, , $(subst -, , $(subst ., , $(version_full)))) 7 | PREFIX ?= /usr/local 8 | BINDIR ?= ${PREFIX}/bin 9 | SHAREDIR ?= $(PREFIX)/share 10 | MANDIR ?= $(SHAREDIR)/man 11 | PKGVERSION := $(word 1, $(version_bits)).$(word 2, $(version_bits)) 12 | LIBEXEC ?= libexec/jj-fzf-$(PKGVERSION) 13 | PRJDIR ?= $(PREFIX)/$(LIBEXEC) 14 | CLEANFILES := *.tmp 15 | CLEANDIRS := 16 | Q := $(if $(findstring 1, $(V)),, @) 17 | QGEN = @echo ' GEN ' $@ 18 | QSKIP := $(if $(findstring s,$(MAKEFLAGS)),: ) 19 | QECHO = @QECHO() { Q1="$$1"; shift; QR="$$*"; QOUT=$$(printf ' %-8s ' "$$Q1" ; echo "$$QR") && $(QSKIP) echo "$$QOUT"; }; QECHO 20 | 21 | # == Check presence of dependencies == 22 | check-deps: preflight.sh jj-fzf 23 | $(QGEN) 24 | $Q ./preflight.sh 25 | $Q ./jj-fzf --version >/dev/null || { echo "$@: ERROR: failed to start ./jj-fzf as \`bash\` script" >&2; false; } 26 | .PHONY: check-deps 27 | all check: check-deps 28 | 29 | # == CmdRunReplace == 30 | define CmdRunReplace 31 | /^!!!!/ { 32 | cmd = substr($$0, 5) 33 | while (( (cmd " 2>&1 || echo __CMDRR_ERROR__=$$?") | getline line) > 0) { print line } 34 | close(cmd) 35 | next 36 | } 37 | { print } 38 | endef 39 | 40 | # == doc/jj-fzf.1 == 41 | doc/jj-fzf.1: doc/jj-fzf.1.md Makefile.mk jj-fzf $(wildcard lib/*) 42 | $(file > doc/cmdrr.awk, $(CmdRunReplace)) 43 | $(QGEN) 44 | $Q TEMPD="`mktemp -d`" && cd "$$TEMPD" \ 45 | && jj git init 2>/dev/null && ln -s $(abspath .)/* . \ 46 | && awk -f $(abspath doc/cmdrr.awk) $(abspath $<) > $(abspath doc/jj-fzf+cmds.1.md) \ 47 | && cd / && rm -r -f "$$TEMPD" # jj-fzf needs a .jj repo to run 48 | $Q ! grep -B3 -Fn '__CMDRR_ERROR__' doc/jj-fzf+cmds.1.md /dev/null 49 | $Q grep -iq 'alt-r.*rebase' doc/jj-fzf+cmds.1.md || { echo 'doc/jj-fzf+cmds.1.md: missing Alt-R'; false; } 50 | $Q pandoc $(man/markdown-flavour) -s -p \ 51 | -M date="$(word 2, $(version_full))" \ 52 | -M footer="jj-fzf-$(word 1, $(version_full))" \ 53 | doc/jj-fzf+cmds.1.md -t man -o $@.tmp 54 | $Q rm -f doc/cmdrr.awk doc/keys.tmp doc/jj-fzf.1.tmp.md && mv $@.tmp $@ 55 | man/markdown-flavour := -f markdown+autolink_bare_uris+emoji+lists_without_preceding_blankline-smart 56 | CLEANFILES += doc/jj-fzf.1 doc/*.tmp* 57 | all: doc/jj-fzf.1 58 | 59 | # == jj-fzf-help.md == 60 | # Man page for the jj-fzf wiki 61 | doc/jj-fzf.1.gfm.md: doc/jj-fzf.1 62 | pandoc $(man/markdown-flavour) -s -p \ 63 | -M date="$(word 2, $(version_full))" \ 64 | -M footer="jj-fzf-$(word 1, $(version_full))" \ 65 | doc/jj-fzf+cmds.1.md -t gfm -o $@ 66 | 67 | # == SCRIPTS == 68 | SHELLSCRIPTS := jj-fzf $(wildcard *.sh lib/*.sh) 69 | 70 | # == tests == 71 | tests-basics.sh: 72 | $Q tests/basics.sh 73 | .PHONY: tests-basics.sh 74 | 75 | # == shellcheck == 76 | shellcheck-warning: $(SHELLSCRIPTS) $(LIBSCRIPTS) 77 | $(QGEN) 78 | $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } 79 | shellcheck -W 3 -S warning -e SC2178,SC2207,SC2128 $(SHELLSCRIPTS) $(LIBSCRIPTS) 80 | shellcheck-error: 81 | $(QGEN) 82 | $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } 83 | shellcheck -W 3 -S error $(SHELLSCRIPTS) $(LIBSCRIPTS) 84 | check-help: 85 | $(QGEN) 86 | $Q ./jj-fzf --help | grep -qF jj-fzf || { echo "$@: ERROR: failed to render \`./jj-fzf --help\`" >&2; false; } 87 | check: check-deps check-help shellcheck-error tests-basics.sh 88 | 89 | # == test == 90 | test: test-screencasts 91 | .PHONY: test test-screencasts 92 | SCREENCAST.SCRIPTS := oplog.sh bookmarks.sh revset.sh 93 | define TEST_SCREENCAST 94 | test-screencast-$1: screencasts/$1 95 | $$(QECHO) RUN $$< 96 | $Q cd screencasts && ./$1 --hide --fast 97 | .PHONY: test-screencast-$1 98 | test-screencasts: test-screencast-$1 99 | endef 100 | $(foreach F, $(SCREENCAST.SCRIPTS), $(eval $(call TEST_SCREENCAST,$F))) 101 | 102 | # == install & uninstall == 103 | PRJ_INSTALL_FILES := $(wildcard README.md NEWS.md jj-fzf *.sh) 104 | LIB_INSTALL_FILES := $(wildcard lib/*.awk lib/*.py lib/*.sh) 105 | install: all 106 | $(QGEN) 107 | mkdir -p $(DESTDIR)$(PRJDIR)/doc $(DESTDIR)$(PRJDIR)/lib $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 108 | install -c $(PRJ_INSTALL_FILES) $(DESTDIR)$(PRJDIR) 109 | install -c $(LIB_INSTALL_FILES) $(DESTDIR)$(PRJDIR)/lib 110 | @ # Note, .gitattributes:export-subst + git archive + tar are used to hardcode version in $(PRJDIR)/version.sh 111 | test ! -e .gitattributes || git archive HEAD version.sh | tar xC $(DESTDIR)$(PRJDIR) 112 | install -c doc/jj-fzf.1 $(DESTDIR)$(PRJDIR)/doc 113 | ln -sf ../../../$(LIBEXEC)/doc/jj-fzf.1 $(DESTDIR)$(MANDIR)/man1/ 114 | ln -sf ../$(LIBEXEC)/jj-fzf $(DESTDIR)$(BINDIR)/jj-fzf 115 | installcheck: 116 | $(QGEN) 117 | $Q $(DESTDIR)$(BINDIR)/jj-fzf --version >/dev/null \ 118 | || { echo "$@: ERROR: failed to start $(DESTDIR)$(BINDIR)/jj-fzf" >&2; false; } 119 | $Q man $(DESTDIR)$(PRJDIR)/doc/jj-fzf.1 > $@.tmp \ 120 | && grep -qF jj-fzf $@.tmp && rm -f $@.tmp \ 121 | || { echo "$@: ERROR: failed to render $(DESTDIR)$(PRJDIR)/doc/jj-fzf.1" >&2; false; } 122 | uninstall: 123 | $(QGEN) 124 | rm -r -f $(DESTDIR)$(PRJDIR) $(DESTDIR)$(BINDIR)/jj-fzf $(DESTDIR)$(MANDIR)/man1/jj-fzf.1 125 | 126 | # == distcheck == 127 | distcheck: 128 | @$(eval distversion != git describe --match='v[0-9]*.[0-9]*.[0-9]*' | sed 's/^v//') 129 | @$(eval distname := jj-fzf-$(distversion)) 130 | $(QECHO) MAKE $(distname).tar.zst 131 | $Q test -n "$(distversion)" || { echo -e "#\n# $@: ERROR: no dist version, is git working?\n#" >&2; false; } 132 | $Q git describe --dirty | grep -qve -dirty || echo -e "#\n# $@: WARNING: working tree is dirty\n#" 133 | $Q rm -r -f artifacts/ && mkdir -p artifacts/ 134 | $Q # Generate ChangeLog with ^^-prefixed records. Tab-indent commit bodies, kill whitespaces and multi-newlines 135 | $Q git log --abbrev=13 --date=short --first-parent HEAD \ 136 | --pretty='^^%ad %an # %h%n%n%B%n' > artifacts/ChangeLog \ 137 | && sed 's/^/ /; s/^ ^^// ; s/[[:space:]]\+$$// ' -i artifacts/ChangeLog \ 138 | && sed '/^\s*$$/{ N; /^\s*\n\s*$$/D }' -i artifacts/ChangeLog 139 | $Q # Generate and compress artifacts/jj-fzf-*.tar.zst 140 | $Q git archive --prefix=$(distname)/ --add-file artifacts/ChangeLog -o artifacts/$(distname).tar HEAD 141 | $Q rm -f artifacts/$(distname).tar.zst && zstd --ultra -22 --rm artifacts/$(distname).tar && ls -lh artifacts/$(distname).tar.zst 142 | $Q T=`mktemp -d` && cd $$T && tar xf $(abspath artifacts/$(distname).tar.zst) \ 143 | && cd jj-fzf-$(distversion) \ 144 | && nice make all -j`nproc` \ 145 | && make PREFIX=$$T/inst install \ 146 | && make PREFIX=$$T/inst installcheck -j`nproc` \ 147 | && (set -x && $$T/inst/bin/jj-fzf --version) \ 148 | && make PREFIX=$$T/inst uninstall \ 149 | && (set -x && $$PWD/jj-fzf --version) \ 150 | && cd / && rm -r "$$T" 151 | $Q echo "Archive ready: artifacts/$(distname).tar.zst" | sed '1h; 1s/./=/g; 1p; 1x; $$p; $$x' 152 | CLEANDIRS += artifacts 153 | 154 | # == artifacts/jj-fzf.sfx == 155 | artifacts/jj-fzf.sfx: all 156 | $(QGEN) 157 | $Q rm -rf xinst/ 158 | $Q $(MAKE) install DESTDIR=xinst/ 159 | $Q cd xinst/ && $(abspath sfx.sh) --sfxsh-pack /usr/local/bin/jj-fzf $(abspath $@) * 160 | $Q rm -rf xinst/ 161 | $Q echo "SFX archive ready: $@" | sed '1h; 1s/./=/g; 1p; 1x; $$p; $$x' 162 | 163 | # == artifacts/jj-fzf.1.gz == 164 | artifacts/jj-fzf.1.gz: doc/jj-fzf.1 165 | $(QGEN) 166 | $Q cp doc/jj-fzf.1 artifacts/jj-fzf.1 167 | $Q gzip -9 artifacts/jj-fzf.1 168 | $Q echo "Man page ready: $@" | sed '1h; 1s/./=/g; 1p; 1x; $$p; $$x' 169 | 170 | # == clean == 171 | clean: 172 | rm -f $(CLEANFILES) 173 | rm -f -r $(CLEANDIRS) 174 | .PHONY: clean 175 | 176 | -------------------------------------------------------------------------------- /doc/jj-fzf.1.md: -------------------------------------------------------------------------------- 1 | % JJ-FZF(1) | jj-fzf Manual Page 2 | 3 | # NAME 4 | jj-fzf - Terminal interface for the `jj` version control system based on fzf 5 | 6 | # SYNOPSIS 7 | **jj-fzf** [*OPTIONS*] \ 8 | **jj-fzf** *COMMAND* [*ARGUMENTS*...] 9 | 10 | # OPTIONS 11 | 12 | **--version** 13 | : Print version information. 14 | 15 | **--help** 16 | : Print brief usage information. 17 | 18 | **--man** 19 | : Browse this man page. 20 | 21 | **--no-preview** 22 | : Hide the preview window. 23 | 24 | **-c**, **+c** 25 | : Start as a commit picker, **-c** picks a single commit, **+c** picks multiple commits. 26 | 27 | **-r**, **+r** 28 | : Start as a revision (change ID) picker, **-r** picks a single revision, **+r** picks multiple revisions. 29 | 30 | **-s** 31 | : Start as a revset picker, returns the edited / current revset expression. 32 | 33 | # DESCRIPTION 34 | 35 | **jj-fzf** is a text-based user interface for the `jj` version control system, 36 | built on top of the fuzzy finder `fzf`. **jj-fzf** centers around the `jj log` 37 | graph view, providing previews of `jj diff` or `jj evolog` for each revision. 38 | Several key bindings are available for actions such as squashing, swapping, 39 | rebasing, splitting, branching, committing, or abandoning revisions. A 40 | separate view for the operations log, `jj op log`, allows fast previews of 41 | diffs and commit histories of past operations and enabling undo of previous 42 | actions. The available hotkeys are displayed on-screen for easy 43 | discoverability. The commands and key bindings can also be found in the man 44 | page (displayed with `jj-fzf --man`) and are documented in the **jj-fzf** wiki. 45 | 46 | ## JJ LOG VIEW 47 | 48 | The `jj log` view in **jj-fzf** displays a list of revisions with commit 49 | information on each line. Each entry contains the following elements: 50 | 51 | `@` 52 | : Marks the working copy 53 | 54 | `○` 55 | : Indicates a mutable commit, a commit that has not yet been pushed 56 | 57 | `◆` 58 | : Indicates an immutable commit, that has been pushed or tagged 59 | 60 | `Change ID` 61 | : The (mostly unique) identifier to track this change across commits 62 | 63 | `Username` 64 | : The abbreviated username of the author 65 | 66 | `Date` 67 | : The day when the commit was authored 68 | 69 | `Commit ID` 70 | : The unique hash for this commit and its meta data 71 | 72 | `Refs` 73 | : Any tags or bookmarks associated with the revisions 74 | 75 | `Message` 76 | : A brief description of the changes made in the revisions 77 | 78 | Note, in `jj`, the set of immutable commits can be configured via 79 | the `revset-aliases."immutable_heads()"` config setting. 80 | 81 | ## PREVIEW WINDOW 82 | 83 | The preview window on the right displays detailed information for the 84 | currently selected revisions. The meaning of the preview items are as follows: 85 | 86 | **First Line** 87 | : The `jj log -T builtin_log_oneline` output for the selected commit 88 | 89 | **Commit ID** 90 | : The unique identifier for the Git commit 91 | 92 | **Change ID** 93 | : The `jj` revision identifier for this revisions 94 | 95 | **Parents** 96 | : A list of parent revisions (more than one for merge commits) 97 | 98 | **Tags** / **Bookmarks** 99 | : Tags and bookmarks (similar to branch names) for this revisions 100 | 101 | **Author** 102 | : The author of the revision, including name and email, timestamp 103 | 104 | **Committer** 105 | : The committer, including name and email, timestamp 106 | 107 | **Message** 108 | : Detailed message describing the changes made in the revision 109 | 110 | **File List** 111 | : A list of files modified by this revision 112 | 113 | **Diff** 114 | : A `jj diff` view of changes introduced by the revision 115 | 116 | # COMMAND EXECUTION 117 | 118 | For all repository-modifying commands, **jj-fzf** prints the actual `jj` commands 119 | executed to stderr. The output aids users in learning how to use `jj` directly 120 | to achieve the desired effects. This output can also be useful when debugging and 121 | helps users determine which actions they might wish to undo. 122 | 123 | Most commands can also be run directly from the command line. The supported 124 | commands are the same as the key bindings listed below (e.g., `abandon`, 125 | `squash`, etc.). The arguments are typically one or more commit or change IDs. 126 | 127 | # KEY BINDINGS 128 | 129 | Most **jj-fzf** commands operate on the current revision under the fzf pointer 130 | and/or a set of previously selected revisions (use _Tab_ or _Shift-Tab_ to change 131 | selection). All dialogs can be closed at any point with _Escape_. 132 | 133 | ## KEY BINDINGS FOR JJ-FZF 134 |   135 | !!!! ./jj-fzf --help-bindings 136 | 137 | ## KEY BINDINGS FOR BOOKMARKS & TAGS 138 | 139 | The "Bookmarks & Tags" dialog (_Alt-B_) displays bookmarks and their states. 140 | Since `jj` tracks bookmarks locally and on remotes (like `@origin`), a 141 | bookmark can exist in several states. The dialog simplifies this by showing a 142 | single, most significant state for each bookmark and only takes `@origin` 143 | as remote into consideration: 144 | 145 | `[Deleted]` 146 | : The bookmark is deleted locally but is still tracked on a remote, the deletion still needs to be pushed to the remote. 147 | 148 | `[Conflicted]` 149 | : The local and remote bookmarks have diverged and need to be resolved by moving the bookmark. 150 | 151 | `[Tracked]` 152 | : The bookmark exists locally and is tracking the bookmark at the remote. 153 | 154 | `[Untracked]` 155 | : The bookmark exists locally and on a remote, but is not tracked. 156 | 157 | `[Local]` 158 | : The bookmark exists only locally, but not on a remote. 159 | 160 | `[Remote]` 161 | : The bookmark exists only on a remote. 162 | 163 | Consequently, only a subset of the key bindings will have an effect on bookmarks in certain states. 164 |   165 | !!!! lib/bookmarks.sh --help-bindings 166 | 167 | ## KEY BINDINGS FOR THE EVOLOG 168 |   169 | !!!! lib/evolog.sh --help-bindings 170 | 171 | ## KEY BINDINGS FOR THE OPERATION LOG 172 |   173 | !!!! lib/oplog.sh --help-bindings 174 | 175 | ## KEY BINDINGS FOR CHANGE PARENTS 176 |   177 | !!!! lib/reparent.sh --help-bindings 178 | 179 | ## KEY BINDINGS FOR REBASE 180 |   181 | !!!! lib/rebase.sh --help-bindings 182 | 183 | # CONFIGURATION 184 | 185 | The default set of revisions for the main **jj-fzf** log view is configured via 186 | `jj-fzf.log_revset`, with a fallback to `revsets.log` (the standard `jj log` 187 | revset). To use a different revset, type it into the **jj-fzf** query field which 188 | will live update the log view. To persist the revset in the repository's local 189 | `jj-fzf.log_revset` configuration, press _Alt-Enter_. 190 | 191 | The default commit display template for the log view is configured via 192 | `jj-fzf.log_template`, with a fallback to `templates.log` (the standard `jj log` 193 | template). For example, to configure one-line display as the default, use: 194 | `jj config set --user jj-fzf.log_template builtin_log_oneline` 195 | 196 | The `jj-fzf.log-mode` configuration setting stores whether each commit in the 197 | log view also includes a two letter file type diff. 198 | 199 | The configuration setting `jj-fzf.show-keys` determines if an **fzf** header 200 | is shown that displays active key bindings. 201 | Pre-generated commit messages for `jj describe` are provided as a temporary config 202 | value in `template-aliases.default_commit_description`. 203 | 204 | If an `aliases.push` command is configured to run `jj-pre-push` and the 205 | workspace contains a `.pre-commit-config.yaml` file, pushing will use 206 | `jj push` so any **pre-commit** hooks are executed first. 207 | 208 | 209 | ## LLM CONFIGURATION 210 | !!!! lib/gen-message.py --llm-help 211 | 212 | # SEE ALSO 213 | 214 | For screencasts, workflow suggestions or feature requests, visit the 215 | **jj-fzf** project page at: \ 216 | https://github.com/tim-janik/jj-fzf 217 | 218 | For revset expressions, see: \ 219 | https://martinvonz.github.io/jj/latest/revsets 220 | 221 | For using `default_commit_description` in `draft_commit_description` customization, see: \ 222 | https://jj-vcs.github.io/jj/latest/config/#default-description 223 | 224 | For **pre-commit** hooks via `jj-pre-push`, see: \ 225 | https://github.com/acarapetis/jj-pre-push 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![License][mpl2-badge]][mpl2-url] 3 | [![Issues][issues-badge]][issues-url] 4 | [![Irc][irc-badge]][irc-url] 5 | 6 | 7 | JJ-FZF 8 | ====== 9 | 10 | 11 | ## About jj-fzf 12 | 13 | `JJ-FZF` is a text UI for the [Jujutsu VCS](https://jj-vcs.github.io/jj/latest/) `jj` based on [fzf](https://junegunn.github.io/fzf/). 14 | All modification commands are printed on stderr to help users in learning the [jj CLI](https://jj-vcs.github.io/jj/latest/cli-reference/). 15 | 16 | ### Feature Set 17 | 18 | * Edit the current [revset](https://jj-vcs.github.io/jj/latest/revsets/) (list of commits) in the fzf input field with live reload of the `jj log`. 19 | * Complex [rebase](https://github.com/tim-janik/jj-fzf?tab=readme-ov-file#rebasing-commits) commands just need `Alt-R` and cursor keys. 20 | * Use `Alt-P` for a dialog to edit or simplify the parents in a [merge](https://github.com/tim-janik/jj-fzf?tab=readme-ov-file#merging-commits) commit. 21 | * [Splitting](https://github.com/tim-janik/jj-fzf?tab=readme-ov-file#splitting-commits) commits needs a single key press. `Alt-F` splits commits by file, `Alt-I` uses the [`jj split`](https://jj-vcs.github.io/jj/latest/cli-reference/#jj-split) command in interactive mode. 22 | * First class [Mega-Merge](https://github.com/tim-janik/jj-fzf?tab=readme-ov-file#mega-merge-workflow) support: `Ctrl-N` starts a new branch, `Alt-N` inserts a new empty commit, `Alt-P` edits merged branches, `Alt-O` absorbs fixes into related commits of merged branches. 23 | * Commits can be [squashed](https://jj-vcs.github.io/jj/latest/cli-reference/#jj-squash) (combined into a single commit) from arbitrary points in the ancestry with `Alt-Q`. 24 | * A dedicated browser (`Ctrl-T`) shows the evolution of each revision ([change_id](https://jj-vcs.github.io/jj/latest/glossary/#change-id)) and allows to inject (`Alt-J`) historic versions of a revision as a new commit without affecting the working copy. 25 | * Key bindings are easily discoverable in an onscreen area and via `Ctrl-H` or the `jj-fzf.1` manual page. 26 | * At any point the [oplog](https://jj-vcs.github.io/jj/latest/operation-log/) can be opened with `Ctrl-O` to understand recent modifications, browse the working copy of a previous operation and restore the repository to an arbitrary earlier snapshot. 27 | * Use `Alt-J` in the oplog to "inject" past snapshots of a repository as newly created historic commits after the fact without affecting the working copy. 28 | * Snapshots are usually created with commands like `jj status`, [Watchman](https://jj-vcs.github.io/jj/latest/config/#watchman) or upon `Save` in Emacs by using the [contrib/jj-undirty.el](https://github.com/tim-janik/jj-fzf/blob/trunk/contrib/jj-undirty.el) script. 29 | * The shortcuts for repository wide undo/redo are `Alt-Z` and `Alt-Y`. The operation log view (`Ctrl-O`) reflects the current state of the undo stack by marking past undo operations with `⋯`. 30 | 31 | The main view centers around `jj log` and allows editing of the revset that is currently being displayed. 32 | Next to it is a preview window that shows details, message and diff for the current revision. 33 | Cursor keys change the current revision and (`Shift-`)`Tab` selects commits. 34 | Enter can be used to browse the commit history, or to confirm if `jj-fzf` was started as a selector. 35 | Various `Ctrl` and `Alt` key bindings are provided to quickly perform actions such as abandon, squash, merge, rebase, split, branch, undo or redo of a commit and more. 36 | The commands and key bindings can also be displayed with `jj-fzf --help` and are documented in the wiki: [jj-fzf-help](https://github.com/tim-janik/jj-fzf/wiki/jj-fzf-help) 37 | 38 | ## Installation 39 | 40 | There are several ways to install and use `jj-fzf`: 41 | 42 | * Download the [latest](https://github.com/tim-janik/jj-fzf/releases/latest/) release tarball. 43 | Extract, then run `make all` and `make install PREFIX=~/.local` under a suitable prefix to run `jj-fzf` from `$PATH` and have `jj-fzf.1` in `$MANPATH`. 44 | * Download [jj-fzf.sfx](https://github.com/tim-janik/jj-fzf/releases/latest/download/jj-fzf.sfx), rename to `jj-fzf` and mark it executable to run it directly. 45 | * Download [jj-fzf.1.gz](https://github.com/tim-janik/jj-fzf/releases/latest/download/jj-fzf.1.gz), install it under e.g. `~/.local/share/man/man1/jj-fzf.1.gz`. 46 | 47 | Internally, `jj-fzf` uses tools like python3, awk, sed and grep with GNU tool semantics. 48 | 49 | 50 | ## Usage 51 | 52 | Start `jj-fzf` in any `jj` repository and study the keybindings. 53 | Various `jj` commands are accessible through `Alt` and `Ctrl` key bindings. 54 | The query prompt can be used to type a new revset to be displayed by `jj log`. 55 | The preview window shows commit details and diff information. 56 | When a key binding is pressed to modify the history, the actual `jj` command is displayed on stderr with its arguments. 57 | Quit `jj-fzf` with `Escape` or suspend it with `Ctrl-Z` to look at the execution trail. 58 | 59 | 60 | ## Demo Screencasts 61 | 62 | ### UI Introduction 63 | 64 | The intro screen cast shows the `jj log` view, the commit diff preview window and a brief glimpse of the oplog (`Ctrl-O`). 65 | 66 | ![JJ-FZF Intro](https://github.com/user-attachments/assets/a4e248d1-15ef-4967-bc8a-35783da45eaa) 67 | **JJ-FZF Introduction:** [Asciicast](https://asciinema.org/a/684019) [MP4](https://github.com/user-attachments/assets/1dcaceb0-d7f0-437e-9d84-25d5b799fa53) 68 | 69 | 70 | ### Splitting Commits 71 | 72 | This screencast demonstrates how to handle large changes in the working copy using `jj-fzf`. 73 | It begins by splitting individual files into separate commits (`Alt-F`), then interactively splits (`Alt-I`) a longer diff into smaller commits. 74 | Diffs can also be edited using the diffedit command (`Alt-E`) to select specific hunks. 75 | Throughout, commit messages are updated with the describe command (`Ctrl-D`), 76 | and all changes can be undone step by step using `Alt-Z`. 77 | 78 | ![Splitting Commits](https://github.com/user-attachments/assets/d4af7859-180e-4ecf-872c-285fbf72c81f) 79 | **Splitting Commits:** [Asciicast](https://asciinema.org/a/684020) [MP4](https://github.com/user-attachments/assets/6e1a837d-4a36-4afd-ad7e-d1ce45925011) 80 | 81 | ### Merging Commits 82 | 83 | This screencast demonstrates how to merge commits using the `jj-fzf` command-line tool. 84 | It begins by selecting a revision to base the merge commit on, then starts the merge dialog with `Alt-M`. 85 | For merging exactly 2 commits, `jj-fzf` suggests a merge commit message and opens the text editor before creating the commit. 86 | More commits can also be merged, and in such cases, `Ctrl-D` can be used to describe the merge commit afterward. 87 | 88 | ![Mergin Commits](https://github.com/user-attachments/assets/47be543f-4a20-42a2-929b-e9c53ad1f896) 89 | **Mergin Commits:** [Asciicast](https://asciinema.org/a/685133) [MP4](https://github.com/user-attachments/assets/7d97f37f-c623-4fdb-a2de-8860bab346a9) 90 | 91 | ### Rebasing Commits 92 | 93 | This screencast demonstrates varies ways of rebasing commits (`Alt-R`) with `jj-fzf`. 94 | It begins by rebasing a single revision (`Alt-R`) before (`Ctrl-B`) and then after (`Ctrl-A`) another commit. 95 | After that, it moves on to rebasing an entire branch (`Alt-B`), including its descendants and ancestry up to the merge base, using `jj rebase --branch --destination `. 96 | Finally, it demonstrates rebasing a subtree (`Alt-S`), which rebases a commit and all its descendants onto a new commit. 97 | 98 | ![Rebasing Commits](https://github.com/user-attachments/assets/d2ced4c2-79ec-4e7c-b1e0-4d0f37d24d70) 99 | **Rebasing Commits:** [Asciicast](https://asciinema.org/a/684022) [MP4](https://github.com/user-attachments/assets/32469cab-bdbf-4ecf-917d-e0e1e4939a9c) 100 | 101 | ### "Mega-Merge" Workflow 102 | 103 | This screencast demonstrates the [Mega-Merge](https://ofcr.se/jujutsu-merge-workflow) workflow, which allows to combine selected feature branches into a single "Mega-Merge" commit that the working copy is based on. 104 | It begins by creating a new commit (`Ctrl-N`) based on a feature branch and then adds other feature branches as parents to the commit with the parent editor (`Alt-P`). 105 | As part of the workflow, new commits can be squashed (`Alt-W`) or rebased (`Alt-R`) into the existing feature branches. 106 | To end up with a linear history, the demo then shows how to merge a single branch into `master` and rebases everything else to complete a work phase. 107 | 108 | ![Mega-Merge Workflow](https://github.com/user-attachments/assets/f944afa2-b6ea-438d-802b-8af83650a65f) 109 | **Mega-Merge:** [Asciicast](https://asciinema.org/a/685256) [MP4](https://github.com/user-attachments/assets/eb1a29e6-b1a9-47e0-871e-b2db5892dbf1) 110 | 111 | 112 | ## Contrib Directory 113 | 114 | The `contrib/` directory contains additional tools or scripts that complement the main jj-fzf functionality. 115 | These scripts are aimed at developers and provide useful utilities for working with jj. 116 | 117 | * **jj-am.sh:** A very simple script that allows to apply patches to a jj repository. 118 | `Usage: ~/jj-fzf/contrib/jj-am.sh [format-patch-file...]` 119 | 120 | * **jj-undirty.el:** A simple Emacs lisp script that automatically runs `jj status` every time a buffer is saved to snapshot file modifications. 121 | `Usage: (load (expand-file-name "~/jj-fzf/contrib/jj-undirty.el"))` 122 | This will install an after-save-hook that calls `jj-undirty` to snapshot the changes in a saved buffer in a jj repository. 123 | 124 | * **suspend-with-shell.el:** A simple Emacs lisp script that allows to suspend Emacs with a custom command. 125 | ``` 126 | Usage: 127 | ;; Suspend emacs with a custom command, without using `ioctl(TIOCSTI)` 128 | (load (expand-file-name "~/jj-fzf/contrib/suspend-with-shell.el")) 129 | ;; Suspend emacs and start jj-fzf on Ctrl-T 130 | (global-set-key (kbd "C-t") (lambda () (interactive) (suspend-with-shell "jj-fzf"))) 131 | ``` 132 | 133 | 134 | ## License 135 | 136 | This application is licensed under 137 | [MPL-2.0](https://github.com/tim-janik/jj-fzf/blob/master/LICENSE). 138 | 139 | 140 | ## Star History 141 | 142 | [![Star History Chart](https://api.star-history.com/svg?repos=tim-janik/jj-fzf&type=Timeline)](https://star-history.com/#tim-janik/jj-fzf) 143 | 144 | 145 | 146 | [irc-badge]: https://img.shields.io/badge/Live%20Chat-Libera%20IRC-blueviolet?style=for-the-badge 147 | [irc-url]: https://web.libera.chat/#Anklang 148 | [issues-badge]: https://img.shields.io/github/issues-raw/tim-janik/jj-fzf.svg?style=for-the-badge 149 | [issues-url]: https://github.com/tim-janik/jj-fzf/issues 150 | [mpl2-badge]: https://img.shields.io/static/v1?label=License&message=MPL-2&color=9c0&style=for-the-badge 151 | [mpl2-url]: https://github.com/tim-janik/jj-fzf/blob/master/LICENSE 152 | 153 | -------------------------------------------------------------------------------- /lib/gen-message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -B 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | import sys, os, re, argparse, json, subprocess 4 | import urllib.request, urllib.parse 5 | from typing import Generator, Dict, Any 6 | from pathlib import Path 7 | 8 | DEBUG = False 9 | DEBUG_PROMPT = False 10 | DUP_OUTPUT = False 11 | 12 | # LLM setup help, repeated in the manual page 13 | LLM_HELP = ''' 14 | Commit messages can be generated using different Large Language Models (LLMs). 15 | The LLM is chosen based on environment variables, in the following order of precedence: 16 | 17 | 1. Generic llama.cpp-compatible API: 18 | Set LLM_API_BASE to the base URL of your API endpoint. 19 | Optionally, set LLM_API_KEY if the API requires an authorization key. 20 | Example: 21 | ``` 22 | export LLM_API_BASE="http://llm-server.local:8080/v1" 23 | export LLM_API_KEY="your-api-key" # optional 24 | ``` 25 | 2. Google Gemini: 26 | Set GEMINI_API_KEY to your Google AI Studio API key. 27 | Get a free key from: https://aistudio.google.com/ 28 | Example: 29 | ``` 30 | export GEMINI_API_KEY="AI-gemini-api-key" 31 | ``` 32 | 33 | 3. OpenAI: 34 | Set OPENAI_API_KEY to your OpenAI API key. 35 | Optionally, set OPENAI_API_BASE to use a different endpoint 36 | (e.g., for Azure OpenAI or other compatible services). 37 | Example: 38 | ``` 39 | export OPENAI_API_KEY="sk-openai-api-key" 40 | # Optionally, to use a different endpoint: 41 | export OPENAI_API_BASE="https://api.llm-server.local/v1" 42 | ``` 43 | 44 | 4. Local llama.cpp server (default): 45 | If none of the above are set, a connection attempt is made to a local 46 | llama.cpp server at http://localhost:8080/v1. 47 | You can set LLM_API_KEY if your local server requires it. 48 | For more info on llama.cpp server: 49 | 50 | https://github.com/ggml-org/llama.cpp/blob/master/tools/server 51 | ''' 52 | 53 | def configure_model_stream(): 54 | chcompl = '/chat/completions' 55 | if os.environ.get ('LLM_API_BASE'): 56 | base, key = os.environ['LLM_API_BASE'], os.environ.get ('LLM_API_KEY', '') 57 | llm_name = f"llama.cpp-compatible API at {base}" 58 | stream_fn = lambda p: stream_text (base + chcompl, key, 'llama.cpp', p) 59 | return stream_fn, llm_name 60 | elif os.environ.get ('GEMINI_API_KEY'): 61 | # https://ai.google.dev/gemini-api/docs/models 62 | model = 'gemini-2.0-flash-lite' # 'gemini-1.5-flash-latest' 63 | llm_name = f"Google Gemini model '{model}'" 64 | stream_fn = lambda p: gemini_stream (os.environ['GEMINI_API_KEY'], model, p) 65 | return stream_fn, llm_name 66 | elif os.environ.get ('OPENAI_API_KEY'): 67 | model = 'gpt-3.5-turbo' # 'gpt-4.1-nano' 'gpt-4o-mini' 68 | base = os.environ.get ('OPENAI_API_BASE', 'https://api.openai.com/v1') 69 | llm_name = f"OpenAI model '{model}' at {base}" 70 | stream_fn = lambda p: stream_text (base + chcompl, os.environ['OPENAI_API_KEY'], model, p) 71 | return stream_fn, llm_name 72 | llm_name = "local llama.cpp server at http://localhost:8080/v1" 73 | stream_fn = lambda p: stream_text ('http://localhost:8080/v1' + chcompl, os.environ.get ('LLM_API_KEY', ''), 'llama.cpp', p) 74 | return stream_fn, llm_name 75 | 76 | def stream_text (chat_url, api_key, model, prompt): 77 | """Stream text from the llama.cpp API""" 78 | headers = { 'Content-Type': 'application/json' } 79 | if api_key: 80 | headers['Authorization'] = f'Bearer {api_key}' 81 | if DEBUG: 82 | print ("ENDPOINT:", chat_url, 'with Authorization' if api_key else 'without key', file = sys.stderr) 83 | data = { 84 | 'model': model, 85 | 'messages': [{'role': 'user', 'content': prompt}], 86 | 'reasoning_format': 'none', 87 | 'chat_template_kwargs': { 'enable_thinking': False }, 88 | 'stream': True 89 | } 90 | json_data = json.dumps (data).encode ('utf-8') 91 | req = urllib.request.Request (chat_url, data = json_data, headers = headers) 92 | try: 93 | response = urllib.request.urlopen (req) 94 | except Exception as e: 95 | yield f"ERROR: Failed to connect to endpoint: {chat_url} ({e})" 96 | return 97 | for line in response: 98 | line = line.decode ('utf-8').strip() 99 | if not line: 100 | continue 101 | if line.startswith ('data: '): 102 | data_str = line[6:] # Remove 'data: ' prefix 103 | if data_str == '[DONE]': 104 | break 105 | try: 106 | obj = json.loads (data_str) # parse JSON fully 107 | except json.JSONDecodeError: 108 | continue # skip malformed JSON 109 | if 'choices' in obj and len (obj['choices']) > 0: 110 | choice = obj['choices'][0] 111 | if ('delta' in choice and 'content' in choice['delta'] and 112 | choice['delta']['content']): 113 | yield choice['delta']['content'] 114 | response.close() 115 | 116 | def gemini_stream (api_key: str, model: str, prompt: str) -> Generator[str, None, None]: 117 | """Stream text from the Gemini API""" 118 | GOOGAPI_URL = "https://generativelanguage.googleapis.com/v1beta" 119 | url = f"{GOOGAPI_URL}/models/{model}:streamGenerateContent?key={api_key}" 120 | data: Dict[str, Any] = { "contents": [{"parts": [{"text": prompt}]}] } 121 | json_data = json.dumps (data).encode ('utf-8') 122 | req = urllib.request.Request (url, data = json_data, headers = {'Content-Type': 'application/json'}) 123 | with urllib.request.urlopen (req) as response: 124 | buffer, decoder = "", json.JSONDecoder() 125 | for chunk_bytes in iter (lambda: response.read (512), b''): 126 | buffer = (buffer + chunk_bytes.decode ('utf-8')).lstrip (' \n\r,') 127 | if buffer.startswith ('['): # enter JSON list 128 | buffer = buffer[1:].lstrip (' \n\r,') 129 | while buffer: 130 | try: 131 | obj, idx = decoder.raw_decode (buffer) # parse valid JSON object 132 | except json.JSONDecodeError: 133 | break # buffer too short 134 | buffer = buffer[idx:].lstrip (' \n\r,') # skip over JSON object 135 | candidates = obj.get ('candidates', []) 136 | if candidates: 137 | content = candidates[0].get ('content', {}) 138 | parts = content.get ('parts', []) 139 | if parts and 'text' in parts[0]: 140 | yield parts[0]['text'] 141 | if DEBUG and buffer.strip() and buffer.strip() != ']': 142 | print (f"\nWarning: unprocessed JSON: {buffer.strip()}", file = sys.stderr) 143 | 144 | def generate_commit_message (commit_hash, max_count=99): 145 | """Generate a commit message for the given commit hash""" 146 | llm_stream, llm_name = configure_model_stream() 147 | use_jj = locate_dominating_file (".", ".jj") != None 148 | if DEBUG: 149 | print (f"EXEC: Running `{'jj' if use_jj else 'git'} log ...`", file = sys.stderr) 150 | try: 151 | # Commands to list example history and diff 152 | if use_jj: 153 | vcs_log = """jj log --no-pager --no-graph --color=never --stat """ 154 | vcs_log += """-T '"\n========\nAuthor: "++coalesce(self.author().name(),self.author().email())++"\n\n"++description++"\n"' """ 155 | vcs_diff = vcs_log + f"""--git -r '{commit_hash}' """ 156 | vcs_log += f"""-n {max_count} -r '..{commit_hash}' --reversed """ 157 | else: 158 | vcs_log = """git -P log --no-color --stat --format='%n========%nAuthor: %an%n%n%B' """ 159 | vcs_diff = vcs_log + f"""-p '{commit_hash}^!' """ 160 | vcs_log += f"""-n {max_count} '{commit_hash}' --reverse """ 161 | # Get the diff for the specific commit 162 | diff = subprocess.check_output (vcs_diff, shell = True, text = True) 163 | # Get example messages from recent commits 164 | examples = subprocess.check_output (vcs_log, shell = True, text = True) 165 | except subprocess.CalledProcessError as e: 166 | print (f"{sys.argv[0]}: Failed to execute git command: {e}", file = sys.stderr) 167 | sys.exit (2) 168 | constructed_prompt = ( 169 | "======== EXAMPLES ========\n" + 170 | examples + "\n\n" + 171 | "======== COMMIT & DIFF ========\n" + 172 | diff + "\n\n" + 173 | "======== REQUEST ========\n" + 174 | "Generate a brief commit title, a separate empty line and a suitable commit message body for the above commit & diff.\n" + 175 | "Focus on *why* something is done, especially for complex logic, rather than *what* is done. Generate nothing else.\n" + 176 | "/no-think" + 177 | "\n" 178 | ) 179 | if DEBUG: 180 | print (f"LLM: {llm_name}", file = sys.stderr) 181 | if DEBUG_PROMPT: 182 | print ("PROMPT:", file = sys.stderr) 183 | print (constructed_prompt, file = sys.stderr) 184 | # Path('/tmp/prompt.txt').write_text (constructed_prompt) # 'a' 185 | iterable = llm_stream (constructed_prompt) 186 | if DEBUG: 187 | print ("HTTP: Starting LLM request", file = sys.stderr) 188 | tidy_print (iterable) 189 | 190 | def output (text_to_print: str): 191 | """Writes the given text to standard output and flushes.""" 192 | text_to_print = re.sub (r' +(?=\n)', '', text_to_print) 193 | sys.stdout.write (text_to_print) 194 | sys.stdout.flush() 195 | if DUP_OUTPUT: 196 | sys.stderr.write (text_to_print) 197 | 198 | def tidy_print (iterable): 199 | """Remove reasoning blocks from iterable text and add terminating newline.""" 200 | # Read up to the first reasoning tag 201 | buffer = "" 202 | for chunk in iterable: 203 | if DEBUG: sys.stderr.write (chunk) 204 | buffer += chunk 205 | buffer = buffer.lstrip() 206 | if len (buffer) >= 12: # min size to detect thinking 207 | break 208 | # Keep reading until reasoning is done 209 | while (think := buffer.find ('')) >= 0 and (chunk := next (iterable, None)): 210 | if DEBUG: sys.stderr.write (chunk) 211 | buffer += chunk 212 | if (closing := buffer.find ('')) > think: 213 | buffer = buffer[closing + len (''):] 214 | break 215 | # Normal text output 216 | buffer = buffer.lstrip() 217 | if len (buffer) > 0: 218 | output (buffer) 219 | for chunk in iterable: 220 | buffer = chunk 221 | output (buffer) 222 | if not buffer.endswith ('\n'): 223 | output ('\n') 224 | 225 | def locate_dominating_file (start, name): 226 | p = Path (start).resolve() 227 | while p != p.parent: 228 | if (p / name).exists(): 229 | return str (p) 230 | p = p.parent 231 | return None 232 | 233 | def main(): 234 | if '--llm-help' in sys.argv: 235 | print (LLM_HELP.strip()) 236 | sys.exit (0) # used for man page generation 237 | 238 | parser = argparse.ArgumentParser ( 239 | description = 'Generate a commit message using an LLM.', 240 | epilog = 'LLM Configuration:' + LLM_HELP, 241 | formatter_class = argparse.RawDescriptionHelpFormatter 242 | ) 243 | parser.add_argument ('commit_hash', help = 'The hash of the commit to generate a message for.') 244 | parser.add_argument ('--max-count', type=int, default=19, help = 'The maximum number of recent commits to use as examples.') 245 | parser.add_argument ('--dup-output', action = 'store_true', help = 'Duplicate output to stderr.') 246 | parser.add_argument ('--debug-prompt', action = 'store_true', help = 'Print the generated prompt to stderr.') 247 | parser.add_argument ('-x', '--debug', action = 'store_true', help = 'Debug operational status to stderr.') 248 | args = parser.parse_args() 249 | global DEBUG, DEBUG_PROMPT, DUP_OUTPUT 250 | DEBUG = args.debug 251 | DEBUG_PROMPT = args.debug_prompt 252 | DUP_OUTPUT = args.dup_output 253 | 254 | generate_commit_message (args.commit_hash, args.max_count) 255 | 256 | if __name__ == '__main__': 257 | main() 258 | -------------------------------------------------------------------------------- /screencasts/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | die() { echo "${0##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 4 | 5 | [[ "${BASH_SOURCE[0]}" = "${BASH_SOURCE[0]#/}" ]] && 6 | SCREENCASTSDIR="$PWD/${BASH_SOURCE[0]}" || SCREENCASTSDIR="${BASH_SOURCE[0]}" 7 | export SCREENCASTSDIR="${SCREENCASTSDIR%/*}" 8 | # Add jj-fzf to $PATH 9 | PATH="$SCREENCASTSDIR/..:$PATH" 10 | 11 | # == Options == 12 | SCREENCAST_WINDOW=false 13 | SCREENCAST_SPEED=normal 14 | SCREENCAST_HIDE=false 15 | for arg in "$@"; do 16 | case "$arg" in 17 | -x) set -x ;; 18 | --fast) SCREENCAST_SPEED=fast ;; 19 | --window) SCREENCAST_WINDOW=true ;; 20 | --hide) SCREENCAST_HIDE=true ;; 21 | --help) cat <<-__EOF 22 | Usage: ${0##*/} [OPTIONS...] 23 | Options: 24 | --window Run screencast in dedicated terminal window 25 | --hide Hide screencast output during run 26 | --fast Reduce replay timings, might cause race conditions 27 | __EOF 28 | exit 0 ;; 29 | esac 30 | done 31 | [[ "$SCREENCAST_WINDOW$SCREENCAST_HIDE" == *true* ]] && 32 | SCREENCAST_FIXTTY=false || SCREENCAST_FIXTTY=true 33 | 34 | # == Config == 35 | test -n "${SCREENCAST_SESSION-}" || die "missing SCREENCAST_SESSION name" 36 | TEMPD=$(mktemp --tmpdir -d screencasts.XXXXXX) && 37 | trap "rm -rf '$TEMPD'" 0 || die "mktemp failed" 38 | echo "$$" > $TEMPD/$SCREENCAST_SESSION.pid 39 | readonly SCREENCAST_ABSPATH=$(readlink -f "./$SCREENCAST_SESSION") 40 | export SCREENCAST_SESSION SCREENCAST_ABSPATH 41 | export JJ_EMAIL=jane.doe@example.com 42 | export JJ_USER="Jane Doe" 43 | export JJ_CONFIG=/dev/null # per default, ignore user config 44 | 45 | # == Timings == 46 | W=120 H=30 # cols rows 47 | Py=26 # Y for 3 line text popup 48 | Z=0.9 # gnome-terminal zoom 49 | sync=0.250 # synchronizing delay, dont shorten 50 | blink=0.034 # minimum time for next frame 51 | slow_timings() 52 | { 53 | k=0.7 # control key delay 54 | p=3 # user pause for reading/study 55 | s=0.9 # short pause (at max 1sec) 56 | w=0.04 # info delay 57 | t=0.07 # typing delay 58 | } 59 | slow_timings # default 60 | 61 | # Use fast timings for debugging 62 | fast_timings() 63 | { 64 | k=$sync 65 | p=$sync 66 | s=$sync # use $sync as minimum 67 | w=0.004 68 | t=0.007 69 | } 70 | [[ "$SCREENCAST_SPEED" == fast ]] && fast_timings 71 | 72 | # == deps == 73 | test -z "${TMUX-}" || die "this session must be started outside tmux" 74 | SCREENCAST_DEPS=( nano tmux script asciinema pv ) 75 | for cmd in "${SCREENCAST_DEPS[@]}" ; do 76 | command -V $cmd >/dev/null || 77 | die "missing command: $cmd" 78 | done 79 | asciinema --version >/dev/null || 80 | die "failed: asciinema --version" 81 | printf ' %-8s %s\n' OK Dependencies 82 | 83 | # == Screencast functions == 84 | # rtrim, then count chars 85 | crtrim() 86 | ( 87 | V="$*" 88 | V="${V%"${V##*[![:space:]]}"}" 89 | echo "${#V}" 90 | ) 91 | 92 | # type text 93 | T() 94 | { 95 | txt="$*" 96 | for (( i=0; i<${#txt}; i++ )); do 97 | chr="${txt:$i:1}" 98 | if test "$chr" == ';'; then 99 | tmux send-keys -t $SCREENCAST_SESSION -H $(printf %x "'$chr'") 100 | else 101 | tmux send-keys -t $SCREENCAST_SESSION -l "$chr" 102 | fi 103 | sleep $t 104 | done 105 | } 106 | 107 | # send key 108 | K() 109 | ( 110 | while test $# -ge 1 ; do 111 | KEY="$1"; shift 112 | [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] && 113 | { REPEAT="$1"; shift; } || REPEAT=1 114 | for (( i=0 ; i<$REPEAT; i++ )); do 115 | tmux send-keys -t $SCREENCAST_SESSION "$KEY" 116 | DK="${KEY/C-/Ctrl-}" && DK="${DK/M-/Alt-}" 117 | # [[ "$DK" == "$KEY" ]] && [[ "$DK" != "Enter" ]] && sk=$k || sk=$(echo "2 * $k" | bc -l) 118 | field=20 && len=${#DK} && pad=$(printf '%*s' $(( (field - len) / 2 )) '') 119 | tmux display-popup -t $SCREENCAST_SESSION -E -B -y$Py -h1 -w$field \ 120 | "tput smso && printf '%-$field""s' '$pad$DK' | column && tput civis; sleep $k && exit" 121 | sleep $blink 122 | done 123 | done 124 | ) 125 | 126 | Enter() { K "Enter" ; } 127 | 128 | # synchronize (with other programs) 129 | S() 130 | { sleep $s ; } 131 | 132 | # pause (for user to observe) 133 | P() 134 | { sleep $p ; } 135 | 136 | # Display message with pause 137 | X() 138 | { 139 | echo " $*" > $TEMPD/xmsg 140 | local pause=$p # $(echo "`crtrim "$*"` * $w + $p" | bc -l) 141 | tmux display-popup -t $SCREENCAST_SESSION -E -y$Py -h3 -w80 \ 142 | "$SCREENCASTSDIR/slowtype.sh $w $TEMPD/xmsg && tput civis && sleep $pause && exit" 143 | rm $TEMPD/xmsg 144 | sleep $k 145 | } 146 | 147 | # kill-line + type-text + kill-line 148 | Q() 149 | { K C-U; T "$*"; K C-U; sleep $sync; } # fzf-query + Ctrl+U 150 | 151 | # Q without delays 152 | Q0() 153 | { tmux send-keys -t $SCREENCAST_SESSION C-U; tmux send-keys -t $SCREENCAST_SESSION -l "$*"; tmux send-keys -t $SCREENCAST_SESSION C-U; } 154 | 155 | # == Recording == 156 | # Discard stdout and stderr unless `set -x` was set 157 | stderr_to_dev_null() { [[ $- == *x* ]] || exec 2>/dev/null; } 158 | stdout_to_dev_null() { [[ $- == *x* ]] || exec >/dev/null; } 159 | stdio_to_dev_null() { [[ $- == *x* ]] || exec >/dev/null 2>&1; } 160 | stdin_discard() ( set +x; rest=1 ; while test -n "$rest" ; do read -t 0.1 -n1 rest || : ; done ) 161 | 162 | # Configure bash and nano for screencasts 163 | screencast_shell_setup() 164 | { 165 | if test -z "${SCREENCAST_SHELL-}" ; then 166 | # Simplify nano exit to Ctrl+X without 'y' confirmation 167 | echo -e "set saveonexit" > $TEMPD/nanorc 168 | echo -e "#!/usr/bin/env bash\nexec nano --rcfile $TEMPD/nanorc \"\$@\"" > $TEMPD/nano 169 | chmod +x $TEMPD/nano 170 | # Setup clean shell env 171 | echo "export HISTFILE=/dev/null" > $TEMPD/bashrc 172 | echo "PS1='\[\033[01;34m\]\W\[\033[00m\]\$ '" >> $TEMPD/bashrc 173 | echo "export EDITOR=$TEMPD/nano" >> $TEMPD/bashrc 174 | echo "export JJFZF_SHELL='/usr/bin/env bash --init-file $TEMPD/bashrc -i'" >> $TEMPD/bashrc 175 | echo 'echo "$$" ' ">$TEMPD/bash-i.pid" >> $TEMPD/bashrc 176 | echo "PS1='\s<\W>$ '" >> $TEMPD/bashrc 177 | echo "cd $TEMPD/$SCREENCAST_SESSION" >> $TEMPD/bashrc 178 | export SCREENCAST_SHELL="bash --init-file $TEMPD/bashrc -i" 179 | fi 180 | } 181 | 182 | # Find PID of asciinema for the current $SCREENCAST_SESSION 183 | find_asciinema_pid() 184 | { 185 | ps --no-headers -eo pid=,comm=,args= | 186 | awk -v session="$SCREENCAST_SESSION" ' 187 | $0 ~ /asciinema.*\/python.*\/asci[i]nema rec.* tmux attach-session/ && $0 ~ session { 188 | if (firstpid == "" || $1 < firstpid) firstpid = $1 189 | } 190 | END { if (firstpid != "") print firstpid } 191 | ' 192 | } 193 | 194 | # Start recording with asciinema in a dedicated terminal, using $W x $H, etc 195 | start_screencast() # start_screencast [send-keys..] 196 | { 197 | export TERM=xterm-256color # best agg compatibility 198 | printf ' %-8s %s\n' START $SCREENCAST_ABSPATH 199 | local DIR="$(readlink -f "${1:-.}")" ; shift 200 | screencast_shell_setup 201 | # stert new screencast session 202 | tmux kill-session -t $SCREENCAST_SESSION 2>/dev/null || : 203 | ( cd "$DIR" 204 | # export JJ_CONFIG=/dev/null 205 | tmux new-session -s $SCREENCAST_SESSION -P -d -x $W -y $H $SCREENCAST_SHELL 206 | ) >$TEMPD/session 207 | printf ' %-8s %s\n' TMUX "$SCREENCAST_SESSION" 208 | tmux set-option -t $SCREENCAST_SESSION status off 209 | tmux set-option -t $SCREENCAST_SESSION allow-rename off 210 | tmux send-keys -t $SCREENCAST_SESSION "exec $SCREENCAST_SHELL"$'\n' 211 | while ! test -r $TEMPD/bash-i.pid ; do sleep 0.1 ; done 212 | tmux resize-window -t $SCREENCAST_SESSION -x $W -y $H ; sleep 0.1 213 | tmux send-keys -t $SCREENCAST_SESSION $'clear\n' ; sleep 0.1 214 | while [ $# -gt 0 ] ; do 215 | tmux send-keys -t $SCREENCAST_SESSION "$1" 216 | shift 217 | done 218 | sleep $sync 219 | # start asciinema in bg, so this script continues 220 | RECORDER_C="asciinema rec --overwrite $SCREENCAST_ABSPATH.cast --cols $W --rows $H -c " 221 | TMUX_ATTACH_RO="tmux attach-session -t $SCREENCAST_SESSION -f read-only" 222 | ( set -e 223 | if $SCREENCAST_WINDOW ; then 224 | gnome-terminal --geometry $W"x"$H -t "$SCREENCAST_SESSION -- asciinema" --zoom $Z -- \ 225 | $RECORDER_C "$TMUX_ATTACH_RO" 226 | elif $SCREENCAST_HIDE ; then 227 | script -Enever -O $TEMPD/script.log -c \ 228 | "stty rows $H cols $W && $RECORDER_C '$TMUX_ATTACH_RO' " | 229 | pv -b -t -p -e -i 0.1 -w80 -N ' SCREENCAST' >/dev/null 230 | else 231 | script -Enever -O $TEMPD/script.log -c \ 232 | "stty rows $H cols $W && $RECORDER_C '$TMUX_ATTACH_RO' " 233 | stdin_discard # Absorb to terminal DSR escape sequences 234 | fi 235 | ) & 236 | echo "$!" > $TEMPD/subshell.pid 237 | sleep $sync 238 | test -z "$(find_asciinema_pid)" && sleep $sync 239 | test -n "$(find_asciinema_pid)" || { 240 | ps --no-headers -ao pid,comm,args 241 | die "failed to identify asciinema process for screencast session: $SCREENCAST_SESSION" 242 | } 243 | stdin_discard # Absorb to terminal DSR escape sequences 244 | true 245 | } 246 | 247 | # Stop recording 248 | stop_screencast() 249 | { 250 | set -Eeuo pipefail # -x 251 | # hard abort asciinema, so last frame is preserved 252 | ( set -x 253 | ps --no-headers -ao pid,comm,args 254 | kill -SIGUSR1 $(find_asciinema_pid) # PID=$(tmux list-panes -t $SCREENCAST_SESSION -F '#{pane_pid}') 255 | ) > $TEMPD/kill.log 2>&1 256 | tmux kill-session -t $SCREENCAST_SESSION 257 | ( wait -fn $(cat $TEMPD/subshell.pid) || true ) >/dev/null 2>&1 258 | sleep $sync 259 | $SCREENCAST_HIDE && echo # leave PV line 260 | if $SCREENCAST_FIXTTY ; then 261 | stdin_discard # Absorb to terminal DSR escape sequences 262 | stty sane || true # may fail in Github CI env 263 | # Reset terminal state from mouse/alt-screen/etc 264 | reset # does: sleep 1 265 | # resize 266 | else 267 | sleep $sync 268 | fi 269 | [[ $- == *x* ]] && 270 | cat $TEMPD/kill.log 271 | printf ' %-8s %s\n' STOP $SCREENCAST_ABSPATH 272 | } 273 | 274 | # == repo commands == 275 | # Usage: make_repo [-quitstage] [repo] [brancha] [branchb] 276 | make_repo() 277 | ( 278 | [[ "${1:-}" =~ ^- ]] && { DONE="${1:1}"; shift; } || DONE=___ 279 | R="${1:-repo0}" 280 | A="${2:-deva}" 281 | B="${3:-devb}" 282 | 283 | rm -rf $R/ 284 | mkdir $R 285 | ( # set -x 286 | cd $R 287 | git init -b trunk 288 | echo -e "# $R\n\nHello Git World" > README 289 | git add README && git commit -m "README: hello git world" 290 | G=`git log -1 --pretty=%h` 291 | [[ $DONE =~ root ]] && exit 292 | 293 | git switch -C $A 294 | echo -e "Git was here" > git-here.txt 295 | git add git-here.txt && git commit -m "git-here.txt: Git was here" 296 | echo -e "\n## Copying Restricted\n\nCopying prohibited." >> README 297 | git add README && git commit -m "README: copying restricted" 298 | L=`git log -1 --pretty=%h` # L=`jj log --no-graph -T change_id -r @-` 299 | echo -e "Two times" >> git-here.txt 300 | git add git-here.txt && git commit -m "git-here.txt: two times" 301 | [[ $DONE =~ $A ]] && exit 302 | 303 | jj git init --colocate 304 | jj new $G 305 | sed -r "s/Git/JJ/" -i README 306 | jj commit -m "README: jj repo" 307 | echo -e "\n## Public Domain\n\nDedicated to the Public Domain under the Unlicense: https://unlicense.org/UNLICENSE" >> README 308 | jj commit -m "README: public domain license" 309 | echo -e "JJ was here" > jj-here.txt 310 | jj file track jj-here.txt && jj commit -m "jj-here.txt: JJ was here" 311 | jj bookmark set $B -r @- 312 | [[ $DONE =~ $B ]] && exit 313 | 314 | jj new trunk 315 | echo -e "---\ntitle: Repo README\n---\n\n" > x && sed '0rx' -i README && rm x 316 | jj commit -m "README: yaml front-matter" 317 | 318 | [[ $DONE =~ 3tips ]] && jj abandon -r $L # allow conflict-free merge of 3tips 319 | sed '/title:/i Date: today' -i README 320 | jj commit -m "README: add date to front-matter" 321 | jj bookmark set trunk --allow-backwards -r @- 322 | [[ $DONE =~ 3tips ]] && exit 323 | 324 | jj new $A $B -m "Merging '$A' and '$B'" 325 | M1=`jj log --no-graph -T change_id -r @` 326 | [[ $DONE =~ merged ]] && exit 327 | 328 | jj backout -r $L -d @ && jj edit @+ && jj rebase -r @ --insert-after $A- 329 | jj rebase -b trunk -d @ 330 | [[ $DONE =~ backout ]] && exit 331 | 332 | jj new trunk $M1 -m "Merge into trunk" 333 | [[ $DONE =~ squashall ]] && ( 334 | EDITOR=/bin/true jj squash --from 'root()+::@-' --to @ -m "" 335 | jj bookmark delete trunk gitdev jjdev 336 | ) 337 | 338 | true 339 | ) 340 | 341 | ls -ald $R/* 342 | ) 343 | 344 | # JJ_CONFIG suitable for screencasts with tight layout 345 | make_jj_config() 346 | ( 347 | LOG_TMPL="${1-builtin_log_oneline}" 348 | cat > $TEMPD/jjfzfcast.toml <<__EOF 349 | [template-aliases] 350 | "commit_timestamp(commit)" = "commit.author().timestamp()" 351 | 'format_timestamp(timestamp)' = 'timestamp.local().format("%Y-%m-%d")' 352 | 'format_short_signature(signature)' = ' coalesce(signature.email().local(), email_placeholder) ' 353 | [colors] 354 | "diff token" = { underline = false } 355 | [jj-fzf] 356 | # log_template = "$LOG_TMPL" 357 | __EOF 358 | echo $TEMPD/jjfzfcast.toml 359 | ) 360 | 361 | # Clone JJ repo into reproducible state (from ~/.cache/jj.git) 362 | clone_jj_repo() 363 | { 364 | local DIR="$1" 365 | # cd ~/.cache/ && git clone --bare --single-branch --shallow-since 2024-12-31 -b v0.29.0 git@github.com:jj-vcs/jj.git 366 | test -r /$HOME/.cache/jj.git/ || 367 | die 'missing ~/.cache/jj.git' 368 | rm -rf "$DIR" 369 | ( stdio_to_dev_null # unless set -x 370 | git clone --shallow-since 2025-01-01 file://$HOME/.cache/jj.git "$DIR" 371 | cd "$DIR" 372 | rm -f .git/packed-refs .git/refs/tags/v0.3* .git/refs/tags/v0.28.2 .git/refs/tags/v0.29.0 373 | mkdir -p .git/refs/remotes/origin/ 374 | echo 041c4fecb77434dd6720e7d7f1ce48d9575ac5f7 > .git/refs/remotes/origin/main 375 | jj git init --colocate 376 | jj new b9ebe2f0 377 | jj abandon --ignore-immutable ' lxuluxyq:: | sqywrslw::' 378 | jj rebase --destination 3aac8d21 --source 8b949f7e 379 | ) 380 | } 381 | -------------------------------------------------------------------------------- /lib/bookmarks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/setup.sh # preflight.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | PRINTOUT= 11 | echo 'MODE=V' > $JJFZF_TEMPD/bookmarks.env 12 | INPUT_REV=@ 13 | while test $# -ne 0 ; do 14 | case "$1" in \ 15 | --help-bindings) PRINTOUT="$1" ;; 16 | -B|-m) echo 'MODE=B' > $JJFZF_TEMPD/bookmarks.env ; FZF_ARGS+=( --disabled ) ;; 17 | -T) echo 'MODE=T' > $JJFZF_TEMPD/bookmarks.env ; FZF_ARGS+=( --disabled ) ;; 18 | -D) echo 'MODE=D' > $JJFZF_TEMPD/bookmarks.env ;; 19 | -O) echo 'MODE=O' > $JJFZF_TEMPD/bookmarks.env ;; 20 | -V) echo 'MODE=V' > $JJFZF_TEMPD/bookmarks.env ;; 21 | -x) set -x ;; 22 | -*) true ;; 23 | *) INPUT_REV="$1" ;; 24 | *) break ;; 25 | esac 26 | shift 27 | done 28 | 29 | # == Config == 30 | TITLE='Bookmarks & Tags' 31 | jjfzf_log_detailed # for preview, assigns $JJFZF_LOG_DETAILED_CONFIG 32 | export JJFZFT="jj --no-pager --ignore-working-copy log --no-graph -T" 33 | JJFZF_COMMITID=$($JJFZFT commit_id -r "$INPUT_REV") || 34 | die "Unknown revision: $INPUT_REV" 35 | export JJFZF_COMMITID 36 | jjfzf_status # snapshot dirty working tree 37 | B=() H=() 38 | 39 | # == Aliases == 40 | # Bookmarks are managed locally, @git, @origin and possibly other remotes. 41 | # There are varying states, depending on wether a bookmark is tracked, 42 | # new/delete is unpushed, or @remote disagrees with a moved @git after 43 | # fetch (conflicted). 44 | # For the UI, we try to map the states of a bookmark name onto 1 dimension, 45 | # by defining a hierarchy of states for local bookmark names where more 46 | # important / dominant states shadow others. 47 | # In addition we list remote bookmarks that have no local name. 48 | # 49 | # Possible states: 50 | # [Deleted] (but still tracked @origin) 51 | # [Conflicted] (local, tracked, undecided @git != @origin) 52 | # [Tracked] (local and @origin) 53 | # [Untracked] (exists locally and possibly-different @origin) 54 | # [Local] (not @origin) 55 | # [Remote] (only @origin, not local, untracked) 56 | cat > $JJFZF_TEMPD/bm.toml <<\__EOF 57 | [template-aliases] 58 | # Hierarchical state categories (1D) for a bookmark name 59 | 'bookmark_state1d(untracked)'=''' 60 | if(!present, "Deleted", 61 | if(conflict, "Conflicted", 62 | if(tracked && remote && remote != "git", "Tracked", 63 | if(!tracked && !remote, "Local", 64 | if(!tracked && remote != "git", untracked, 65 | "OTHER_REMOTE" 66 | ) 67 | ) 68 | ) 69 | ) 70 | )''' 71 | # Format local bookmark with 1D state 72 | bookmark_local1d=''' 73 | bookmark_state1d("Untracked") ++ '¸' 74 | ++ name ++ '¸' ++ if(remote,"@"++remote) ++ '¸' 75 | ++ pad_end(32, label("bookmark", name), " ") ++ " " 76 | ++ pad_end(13, 77 | label(if(conflict || !present, "conflict"), 78 | "[" ++ bookmark_state1d("Untracked") ++ "]") ) 79 | ++ if(present && !conflict, 80 | format_commit_summary_with_refs(self.normal_target(), "") ) 81 | ++ "\n" 82 | ''' 83 | # Format @remote bookmark with 1D state 84 | bookmark_remote1d=''' 85 | if(present && !tracked && remote && remote != "git", 86 | bookmark_state1d("Remote") ++ '¸' 87 | ++ name ++ if(remote,"@"++remote) ++ '¸' ++ if(remote,"@"++remote) ++ '¸' 88 | ++ pad_end(32, label("bookmark", name ++ '@' ++ remote), " ") ++ " " 89 | ++ pad_end(13, "[" ++ bookmark_state1d("Remote") ++ "]", " ") 90 | ++ if(present && !conflict, 91 | format_commit_summary_with_refs(self.normal_target(), "") ) 92 | ) ++ "\n" 93 | ''' 94 | # Format tags 95 | tag_local1d = ''' 96 | "Tag¸" 97 | ++ name ++ '¸@git¸' 98 | ++ pad_end(32, label("tag", name), " ") ++ " " 99 | ++ pad_end(13, "[" ++ "Tag" ++ "]", " ") 100 | ++ format_commit_summary_with_refs(self.normal_target(), "") ++ "\n" 101 | ''' 102 | __EOF 103 | 104 | # == jjfzf_b_l == 105 | # jj bookmark list 106 | jjfzf_b_l() 107 | ( 108 | ERR=0 109 | [[ " $* " =~ --color ]] && COLOR='' || COLOR="$JJFZF_COLOR" 110 | jj --no-pager --ignore-working-copy --config-file=$JJFZF_TEMPD/bm.toml \ 111 | bookmark list $COLOR "$@" 2>$JJFZF_TEMPD/err || ERR=$? 112 | grep -Fv 'Hint:' $JJFZF_TEMPD/err >&2 || : 113 | exit $ERR 114 | ) 115 | export -f jjfzf_b_l 116 | 117 | # == Nearest Bookmark == 118 | rev_bookmarks() 119 | { 120 | T='self.local_bookmarks()++"\n"' 121 | test "$1" == -t && { shift ; T="$T ++ ' ' ++ self.tags()" ; } 122 | $JJFZFT "$T" "$@" 2>/dev/null | 123 | tr ' ' '\n' | sed -r '/@/d; s/[*?].*//;' || : 124 | } 125 | # Identify input bookmark name or find first local bookmark in $INPUT_REV 126 | NEAREST=( $(jjfzf_b_l --color=never -T 'name++"\n"' -- "$INPUT_REV" | sed -r '1q') 127 | $(jj tag list -T 'name++"\n"' -- "$INPUT_REV" | sed -r '1q') 128 | $(rev_bookmarks -t -r "$INPUT_REV") 129 | $(rev_bookmarks -r "$INPUT_REV"-) 130 | $(rev_bookmarks -r "$INPUT_REV"+) 131 | $(rev_bookmarks -r .."$INPUT_REV") ) 132 | 133 | # == jjfzf_bookmark_list0 == 134 | jjfzf_bookmark_list0() 135 | ( 136 | # Keep things simple and only consider remote bookmarks @origin. 137 | set -Eeuo pipefail 138 | # Truely local bookmark names 139 | LOCAL_BOOKMARKS=( $(jjfzf_b_l -a -T 'if(!remote, name) ++ "\n"') ) 140 | jjfzf_b_l -a -T 'if(!remote || remote == "origin", bookmark_local1d )' > $JJFZF_TEMPD/bm_local1d 141 | for B in "${LOCAL_BOOKMARKS[@]}" ; do 142 | for S in "Deleted" "Conflicted" "Tracked" "Untracked" "Local" ; do # "UNKNOWN" 143 | grep -m1 "^$S¸$B¸" $JJFZF_TEMPD/bm_local1d && break 144 | done 145 | done > $JJFZF_TEMPD/bm_refs.lst 146 | # Bookmarks untracked @origin 147 | ORIGIN_BOOKMARKS=( $(jjfzf_b_l --remote origin -T 'if(!tracked && remote == "origin", name) ++ "\n"') ) 148 | jjfzf_b_l --remote origin -T 'if(!tracked && remote == "origin", bookmark_remote1d )' > $JJFZF_TEMPD/bm_origin1d 149 | for B in "${ORIGIN_BOOKMARKS[@]}" ; do 150 | jjfzf_contained "$B" "${LOCAL_BOOKMARKS[@]}" && continue 151 | S=Remote 152 | grep -m1 "^$S¸$B@[^¸]*¸" $JJFZF_TEMPD/bm_origin1d && break 153 | done >> $JJFZF_TEMPD/bm_refs.lst 154 | # Add spacer and save spacer position 155 | echo '¸¸¸' >> $JJFZF_TEMPD/bm_refs.lst 156 | wc -l < $JJFZF_TEMPD/bm_refs.lst > $JJFZF_TEMPD/bm_start_pos 157 | # list tags 158 | jj --no-pager --ignore-working-copy --config-file=$JJFZF_TEMPD/bm.toml \ 159 | tag list $JJFZF_COLOR -T tag_local1d \ 160 | >> $JJFZF_TEMPD/bm_refs.lst 161 | # 0-termination 162 | sed '/¸.*¸/s/^/\x00/ ; 1s/^\x00//' < $JJFZF_TEMPD/bm_refs.lst > $JJFZF_TEMPD/bm_refs0.lst 163 | ) 164 | export -f jjfzf_bookmark_list0 165 | jjfzf_bookmark_list0 # setup $JJFZF_TEMPD/bm_start_pos 166 | 167 | # == Start Position == 168 | # Position at nearest bookmark 169 | JJFZF_REFS_START_POS="$(cat "$JJFZF_TEMPD/bm_start_pos")" 170 | [[ ${#NEAREST[@]} -ge 1 ]] && 171 | NEAREST_POS=$(sed -r 's/\x1b\[[0-9;]*[mK]//g' $JJFZF_TEMPD/bm_refs.lst | 172 | grep -m 1 -n "\b${NEAREST[0]}\b" | cut -d: -f1) && 173 | test -n "$NEAREST_POS" && 174 | JJFZF_REFS_START_POS="$NEAREST_POS" 175 | B+=( --bind "load:+pos($JJFZF_REFS_START_POS)" ) 176 | 177 | # == jjfzf_refs_transform == 178 | export JJFZF_BOOKMARK_AT=$(jjfzf_jjlog "$JJFZF_LOG_DETAILED_CONFIG" $JJFZF_COLOR --no-graph -T builtin_log_compact -r "$JJFZF_COMMITID" ) 179 | # Adjust prompt, etc according to mode 180 | jjfzf_refs_transform() 181 | ( 182 | source $JJFZF_TEMPD/bookmarks.env # MODE 183 | [[ "${2-}" == "Tag" ]] && ISTAG=true || ISTAG=false 184 | AT=$'\n \n ' # place holder for builtin_log_compact 185 | case "$MODE" in 186 | T) 187 | T='Create Tag'; I='Create new tag'; P='New Tag Name > '; AT=$' at:\n'"$JJFZF_BOOKMARK_AT"; G="" 188 | ;; 189 | B) 190 | T='Set Bookmark'; I='Move or create bookmark'; P='Bookmark Name > '; AT=$' at:\n'"$JJFZF_BOOKMARK_AT" 191 | $ISTAG && G='' || G="${1%% *}" 192 | ;; 193 | D) 194 | T='Delete Ref'; I='Delete bookmark or tag'; P='Delete Ref > '; G="${1%% *}"; 195 | ;; 196 | O) 197 | T='Track @ Origin'; I='Track origin bookmark'; P='Bookmark to track > ' 198 | $ISTAG && G='' || G="${1%% *}" 199 | ;; 200 | V|*) 201 | T='View Ref'; I='View details'; P='View Ref > '; G="${1%% *}"; 202 | ;; 203 | esac 204 | echo -n "+refresh-preview" 205 | echo -n "+change-prompt($P)" 206 | echo -n "+change-ghost($G)" 207 | echo -n "+change-input-label( Enter: $I )" 208 | # echo -n "+change-footer-label( $T )" 209 | echo -n "+change-footer:${I}$AT" 210 | ) 211 | export -f jjfzf_refs_transform 212 | REFRESH='transform(jjfzf_refs_transform {2} {1})' 213 | FZF_ARGS+=( --bind "start,focus,change:+$REFRESH" ) 214 | 215 | # == Bindings == 216 | H+=( 'Alt-B: Create new or move existing bookmark' ) 217 | B+=( --bind "alt-b,insert:clear-query+disable-search+search()+execute-silent( sed 's/^MODE=.*/MODE=B/' -i $JJFZF_TEMPD/bookmarks.env )+$REFRESH" ) 218 | H+=( 'Alt-T: Create new tag' ) 219 | B+=( --bind "alt-t:clear-query+disable-search+search()+execute-silent( sed 's/^MODE=.*/MODE=T/' -i $JJFZF_TEMPD/bookmarks.env )+$REFRESH" ) 220 | H+=( 'Alt-D: Delete bookmark or tag' ) 221 | B+=( --bind "alt-d,ctrl-delete:enable-search+search()+execute-silent( sed 's/^MODE=.*/MODE=D/' -i $JJFZF_TEMPD/bookmarks.env )+$REFRESH" ) 222 | H+=( 'Alt-O: Toggle tracking of bookmark @ origin' ) 223 | B+=( --bind "alt-o:enable-search+search()+execute-silent( sed 's/^MODE=.*/MODE=O/' -i $JJFZF_TEMPD/bookmarks.env )+$REFRESH" ) 224 | H+=( 'Alt-V: View details' ) 225 | B+=( --bind "alt-v:execute-silent( echo {q} > $JJFZF_TEMPD/refs_query )+enable-search+execute-silent( sed 's/^MODE=.*/MODE=V/' -i $JJFZF_TEMPD/bookmarks.env )+$REFRESH+transform-query( cat $JJFZF_TEMPD/refs_query )" ) 226 | 227 | # == Header Help == 228 | HEADER_HELP=$(printf "%s\n" "${H[@]}" | jjfzf_bold_keys) 229 | B+=( --header "$HEADER_HELP" ) 230 | 231 | # == jjfzf_refs_enter == 232 | # Handle create / delete / etc 233 | jjfzf_refs_enter() 234 | ( 235 | set -Eeuo pipefail 236 | source $JJFZF_TEMPD/bookmarks.env # MODE 237 | QUERY="$2" REF="${1%% *}" 238 | STATE1D="${3-}" 239 | [[ "$STATE1D" == "Tag" ]] && ISTAG=true || ISTAG=false 240 | NEWNAME="$QUERY" 241 | case "$MODE" in 242 | T) 243 | test -z "$NEWNAME" -o -z "$JJFZF_COMMITID" || { 244 | jjfzf_run +n git tag "$NEWNAME" "$JJFZF_COMMITID" 245 | jjfzf_run +n jj --no-pager $JJFZF_KEEPCOMMITS status # import tag 246 | } 247 | ;; 248 | B) 249 | test -z "$NEWNAME" -a $ISTAG == false && NEWNAME="$REF" 250 | test -z "$NEWNAME" -o -z "$JJFZF_COMMITID" || { 251 | jjfzf_run +n jj --no-pager bookmark set --allow-backwards -r "$JJFZF_COMMITID" -- "$NEWNAME" 252 | } 253 | ;; 254 | D) 255 | if [[ "$STATE1D" == "Tag" ]] ; then 256 | GIT_DIR=$(jj --no-pager --ignore-working-copy git root) || 257 | die "need Git to delete tag: $REF" 258 | export GIT_DIR 259 | jjfzf_run +n git tag -d "$REF" 260 | jjfzf_run +n jj --no-pager $JJFZF_KEEPCOMMITS status # import deletion 261 | else 262 | jjfzf_run +n jj --no-pager $JJFZF_KEEPCOMMITS bookmark delete "exact:$REF" 263 | fi 264 | ;; 265 | O) 266 | if [[ "$STATE1D" == "Remote" ]] ; then 267 | jjfzf_run +n jj --no-pager bookmark track -- "$REF" 268 | elif [[ "$STATE1D" == "Tracked" ]] ; then 269 | jjfzf_run +n jj --no-pager bookmark untrack -- "$REF"@origin 270 | elif [[ "$STATE1D" == "Untracked" ]] ; then 271 | jjfzf_run +n jj --no-pager bookmark track -- "$REF"@origin 272 | elif [[ "$STATE1D" == "Local" ]] ; then 273 | PUSH_ARGS=(--allow-new --remote origin --bookmark "$REF") 274 | # needs push to be possible @origin 275 | jjfzf_run +n jj git push $JJFZF_COLOR "${PUSH_ARGS[@]}" --dry-run > $JJFZF_TEMPD/bpush.log 2>&1 \ 276 | && STATUS=0 || STATUS=$? 277 | cat $JJFZF_TEMPD/bpush.log 278 | if test $STATUS == 0 && grep -qEi 'nothing *changed|won.?t push|rejected *commit' $JJFZF_TEMPD/bpush.log ; then 279 | STATUS=-1 280 | fi 281 | # jj-pre-push --dry-run to run hooks 282 | if test $STATUS == 0 -a -r .pre-commit-config.yaml && 283 | jjfzf_config get 'aliases.push' | grep -q '\bjj-pre-push\b' ; then 284 | jjfzf_run +n jj push "${PUSH_ARGS[@]}" --dry-run \ 285 | || STATUS=$? 286 | fi 287 | # jj git push 288 | if test $STATUS != 0 ; then 289 | read -p "Press Enter..." 290 | else 291 | read -p 'Proceed with bookmark push and submit changes? (y/N) ' YN 292 | [[ "${YN:0:1}" =~ [yY] ]] && 293 | jjfzf_run +n jj git push $JJFZF_COLOR --allow-new --bookmark "$REF" 294 | fi 295 | fi 296 | ;; 297 | V|*) 298 | test -z "$REF" || 299 | ( 300 | jj --no-pager --ignore-working-copy tag list $JJFZF_COLOR -- "exact:$REF" 301 | jj --no-pager --ignore-working-copy bookmark list $JJFZF_COLOR -- "exact:$REF" 302 | echo 303 | REVS="${REF+coalesce( tags(exact:'$REF') , bookmarks(exact:'$REF'), $REF )}" 304 | jjfzf_jjlog "$JJFZF_LOG_DETAILED_CONFIG" $JJFZF_COLOR --no-graph -r "$REVS" -p \ 305 | -T 'concat( builtin_log_oneline , builtin_log_detailed , diff.stat() , "\n" )' 306 | ) |& $JJFZF_PAGER 307 | ;; 308 | esac 309 | ) 310 | export -f jjfzf_refs_enter 311 | B+=( --bind "enter:execute( jjfzf_refs_enter {2} {q} {1} )+transform: source $JJFZF_TEMPD/bookmarks.env && [[ \$MODE != V ]] && echo 'close+close'" ) 312 | 313 | # == Preview == 314 | # Show commit(s) with diff and full info 315 | jjfzf_ref_info() 316 | ( 317 | set -Eeuo pipefail 318 | jjfzf_jjlog "$JJFZF_LOG_DETAILED_CONFIG" $JJFZF_COLOR --no-graph -r "$1" -p \ 319 | -T 'concat( builtin_log_oneline , builtin_log_detailed , diff.stat() , "\n" )' 320 | ) 321 | export -f jjfzf_ref_info 322 | B+=( --preview ' L={4} && test -n "$L" && jjfzf_ref_info "${L%% *}" ' ) 323 | B+=( --preview-label " Ref Info " ) 324 | 325 | # == PRINTOUT == 326 | [[ "$PRINTOUT" == --help-bindings ]] && { 327 | for h in "${H[@]}" ; do 328 | echo "$h" | 329 | sed -r 's/^([^ ]+): *([^ ]+) *(.*)/\n### _\1_: **\2**\n\2 \3/' 330 | done 331 | echo 332 | exit 0 333 | } 334 | 335 | # == fzf == 336 | FZF_ARGS+=( 337 | --color=border:red,label:red 338 | --border-label "-[ ${TITLE^^} — JJ-FZF ]-" 339 | ) 340 | jjfzf_status 341 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 342 | export JJFZF_LOAD_LIST=jjfzf_bookmark_list0 343 | fzf -m "${FZF_ARGS[@]}" "${B[@]}" \ 344 | --read0 '-d¸' --accept-nth=2 --with-nth '{4..}' \ 345 | < $JJFZF_TEMPD/bm_refs0.lst 346 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | ## JJ-FZF 0.34.0 - 2025-10-02 2 | 3 | ### Added: 4 | 5 | * In the last month, jj-fzf underwent a complete rewrite. The new version has 6 | out of the box support for running jj commands with multiple revisions and 7 | extends utilization of new jj and fzf features. 8 | 9 | * All key binding commands now operate on a change_id or a list thereof. 10 | 11 | * In case of divergent commits, an fzf list entry now expands to a commit_id, 12 | which also means pretty much all commands now handle divergent commits. 13 | 14 | * Inject will now copy the author, timestamp and message into the new commit. 15 | 16 | * The oplog now combines the operation show, diff and historic log views. 17 | 18 | * The default set of revisions for the jj-fzf log list is now 19 | `jj-fzf.log_revset` with a fallback of `revsets.log` (the standard jj log 20 | revset). To use a different revset with jj-fzf, type the revset into the 21 | query field for live revset updates. To persist the revset in the repo 22 | config under `jj-fzf.log_revset`, hit Alt-Enter. 23 | 24 | * The default commit display template for the jj-fzf log list is now 25 | `jj-fzf.log_template` with a fallback of `templates.log` (the standard jj 26 | log template). In order to configure jj-fzf for one line display, use: 27 | `jj config set --user jj-fzf.log_template builtin_log_oneline` 28 | 29 | * Alt-B now presents a dialog to create, move, delete or track bookmarks and 30 | delete tags. The former bookmark deletion under Alt-D has been merged into 31 | Alt-B. 32 | 33 | * Alt-Q will now squash changes from selected revisions into the revision 34 | under the pointer, or into the parent if no revisions are selected. 35 | 36 | * Alt-S now starts `jj restore --interactive` and restores files from a single 37 | selected revision into the revision under the pointer, or into the parent 38 | if no revisions are selected. 39 | 40 | * Ctrl-D will pre-generate a commit message for merge commits only. For normal 41 | commit messages, use `templates.draft_commit_description` instead. If you 42 | depend on the messages of previous jj-fzf versions, consider the hint printed 43 | out by: `lib/draft.sh --hint` 44 | See also: https://jj-vcs.github.io/jj/latest/config/#default-description 45 | 46 | * Ctrl-F now toggles between the fzf finder and live revset editing. 47 | There is no key binding replacement for the old 'file-editor', just run 48 | `jj edit` or `jj new` on an old commit and open the file of interest. 49 | 50 | * Ctrl-L will now either show the history up to a single selected revision, 51 | or for the selected (multiple) revisions only. 52 | 53 | * Ctrl-V is the new key binding for the evolution log browser. 54 | 55 | * An LLM can be used to generate commit messages with the Ctrl-S key binding. 56 | The generated message is provided to `jj describe` as a config value in 57 | `template-aliases.default_commit_description`. 58 | See the manual page for LLM configurations via environment variables. 59 | 60 | * Sub-dialogs like rebase, reparent or even bookmarks should now retain the 61 | commit (bookmark) pointer position. 62 | 63 | * An optimal column-major text layout algorithm now presents the key bindings. 64 | 65 | * The CI now runs and validates a selected set of screencasts. 66 | 67 | * New -c +c -r +r -s options allow using jj-fzf as a picker for 1 or many 68 | commits, 1 or many revisions or a revset expression. 69 | 70 | ### Changed: 71 | 72 | * Bookmarks are now display with a simplified state that indicates: 73 | Deleted / Conflicted / Tracked / Untracked / Local Remote 74 | 75 | * On startup `jj-fzf` now offers revset editing in the query field. 76 | Use Ctrl-F for the fzf filter. 77 | 78 | * When running `jj describe` a $EDITOR wrapper is used that prevents jj 79 | from accepting an auto-generated default description as message. 80 | 81 | * Running a command from jj-fzf switches back from the alternative screen 82 | and will reload the entire `jj log` output before returning. This may 83 | take longer than the async log loading in previous versions, but it 84 | allows fzf to track and keep the current pointer position. 85 | 86 | ### Fixed: 87 | 88 | * The man page now list key bindings for jj-fzf and all sub-commands. 89 | 90 | * A new configuration section in the man page describes config keys that 91 | jj-fzf makes use of, as well as how to configure LLM usage. 92 | 93 | * The `push` command now avoids querying if nothing changed. 94 | 95 | ### Breaking: 96 | 97 | * The minimum supported fzf version is now 0.65.2. 98 | 99 | * This release requires jj-0.34.0 100 | 101 | * Commands missing from the rewrite: 102 | - Alt-V: vivifydivergent - use `jj metaedit --update-change-id` 103 | - Ctrl-A: author-reset - use `jj metaedit --update-author` 104 | - Ctrl-I: diff - should be handled by Ctrl-L now 105 | - Ctrl-V: gitk - not provided anymore 106 | - Ctrl-W: wb-diff - toggle ±b ±w for diff 107 | 108 | * A number of changes listed above could be considered breaking old 109 | workflows. Please provide feedback in Github discussions or IRC 110 | if you encounter regressions or miss important features. 111 | 112 | Thanks to everyone who gave feedback regarding the rewrite and 113 | helped to make this release happen! 114 | 115 | 116 | ## JJ-FZF 0.33.0 - 2025-09-11 117 | 118 | ### Added: 119 | * New preflight.sh script dedicated to dependency handling 120 | * Added version.sh to support Github "Source code" archives 121 | * Added self extracting jj-fzf.sfx script to release artifacts 122 | * Added manual page jj-fzf.1.gz to release artifacts 123 | * Added contrib/jj-foreach.sh to run shell command for each commit in a revset 124 | * Added option to contrib/jj-foreach.sh to not affecting descendants 125 | * Alt-J: inject selected revision as historic commit before @ 126 | * Documented F5 and F11 keybindings 127 | 128 | ### Breaking: 129 | * This release requires jj-0.33.0 130 | * This release is the last one to support fzf 0.44.1, future release 131 | will depend on more recent fzf versions 132 | * Upon start, fzf will now wait for `jj log` to finish before display; if this 133 | turns out too slow for some repos, please file an issue and request --async 134 | * Instead of enforcing gsed use, preflight.sh now defines an `sed()` function 135 | that proxies `gsed` if needed. Please file an issue if sed problems remain 136 | 137 | ### Changed: 138 | * Pushing to a remote will now also push deleted bookmarks 139 | * Preserve history when deleting tags or bookmarks 140 | (enforces git.abandon-unreachable-commits=false during deletion) 141 | * Added cursor down to swap-commits to follow swapped commit 142 | * Moved preview and helper into library files (speeds up previews) 143 | * Undo/redo operations in jj-fzf now use jj's built-in commands 144 | * Use `jj-fzf oplog` to display the undo stack with ⋯ undo step markers 145 | * Renamed oplog and oplog-browser commands 146 | 147 | ### Fixed: 148 | * Fixed evolog preview and evolog paging (was broken since jj-0.30) 149 | * Fixed outdated uses of `jj --config-toml` 150 | * Fixed broken Github jj-fzf links 151 | * Fixed lacking DESTDIR for make (un)install 152 | 153 | ### Removed: 154 | * Removed unnecessary `all:` prefix in jj revset expressions 155 | * Removed unsed command / key binding for undo marker reset 156 | 157 | 158 | ## JJ-FZF 0.32.0 - 2025-08-14 159 | 160 | ### Added: 161 | * Ctrl-W: Added way to toggle between various diff formats 162 | * Alt+M: New multi-select mode, use TAB to select multiple commits 163 | * Added multi-mode support for abandon, backout, duplicate, squash, rebase 164 | * Added `make distcheck`, always check in CI 165 | * Added check for jj-fzf --help 166 | * Added installcheck rule 167 | * Added separate manual page 168 | * Added file summary to oplog history 169 | * Added scripts to automate releases 170 | * Added contirb/suspend-with-shell.el to run jj-fzf from emacs, see: 171 | https://testbit.eu/2025/jj-fzf-in-emacs 172 | 173 | ### Breaking: 174 | * Depend on jj-0.32.0 175 | * Changed Alt-N to run new-after with --no-edit 176 | * Preserve PWD in subshells if possible (present) 177 | * Remove unused 'merging' command 178 | 179 | ### Changed: 180 | * To install, run `make all install` 181 | * To run all checks, run `make all check install installcheck` 182 | * Builds require GNU Make 183 | * Moved version checks for all tool dependencies into Makefile 184 | * Use /usr/bin/env to find bash 185 | * Undeprecate Alt-S: restore-file from selected revision 186 | * Build man page, use a man page browser for `jj-fzf --help` 187 | * Fetch version information from Git 188 | * Automatically run CI for PRs and tags 189 | * Introduced pandoc dependency for man builds 190 | 191 | ### Fixed: 192 | * Fixed installations not working in non-jj repos 193 | * Fixed --version not working outside a jj repo 194 | 195 | 196 | ## JJ-FZF 0.25.0 - 2025-01-23 197 | 198 | ### Added: 199 | * Fzflog: use jjlog unless jj-fzf.fzflog-depth adds bookmark ancestry 200 | * Use author.email().local(), required by jj-0.25 201 | * Absorb: unconditionally support absorb 202 | * Evolog: add Alt-J to inject a historic commit 203 | * Evolog: add Enter to browse detailed evolution with patches 204 | * Add Ctrl-T evolog dialog with detailed preview 205 | * Add content-diff to jj describe 206 | * Add ui.default-description to commit messages 207 | * Display 'private' as a flag in preview 208 | * Add jj-am.sh to apply several patches in email format 209 | * Add jj-undirty.el, an elisp hook to auto-snapshot after saving emacs buffers 210 | 211 | ### Changed: 212 | * Always cd to repo root, so $PWD doesn't vanish 213 | * Adjust Makefile to work with macOS, #6 214 | * Merging: prefer (master|main|trunk) as UPSTREAM 215 | * Make sure to use gsed 216 | * Check-gsed: show line numbers 217 | * Echo_commit_msg: strip leading newline from ui.default-description 218 | * Flags: display hidden, divergent, conflict 219 | * Cut off the preview after a few thausand lines 220 | * Split-files: try using `jj diff` instead of `git diff-tree` 221 | * Use JJ_EDITOR to really override th JJ editor settings 222 | * Honor the JJ_EDITOR precedence 223 | * Show content diff when editing commit message 224 | * Adjust Bookmark, Commit, Change ID descriptions 225 | * Display 'immutable' as a flag in preview 226 | * Fzflog: silence deprecation warnings on stderr 227 | * Include fzflog error messages in fzf input if any 228 | * Unset FZF_DEFAULT_COMMAND in subshells 229 | 230 | ### Fixed: 231 | * Fix RESTORE-FILE title 232 | * Properly parse options --help, --key-bindings, --color=always 233 | * Echo_commit_msg: skip signoff if no files changed 234 | 235 | ### Deprecation: 236 | * Deprecate Alt-S for restore-file 237 | * Deprecate Ctrl-V for gitk 238 | 239 | ### Breaking: 240 | * Depend on jj-0.25.0 241 | * Op-log: use Alt-J to inject an old working copy as historic commit 242 | * Alt-Z: subshells will always execute in the repository root dir 243 | 244 | ### Contributors 245 | 246 | Thanks to everyone who made this release happen! 247 | 248 | * Tim Janik (@tim-janik) 249 | * Douglas Stephen (@dljsjr) 250 | 251 | 252 | ## JJ-FZF 0.24.0 - 2024-12-12 253 | 254 | ### Added: 255 | * Added Alt-O: Absorb content diff into mutable ancestors 256 | * Added `jj op show -p` as default op log preview (indicates absorbed changes) 257 | * Added marker based multi-step undo which improved robustness 258 | * Op-log: Added restore (Alt-R), undo memory reset (Alt-K) and op-diff (Ctrl-D) 259 | * Added RFC-1459 based simple message IRC bot for CI notifications 260 | * Added checks for shellcheck-errors to CI 261 | * Creating a Merge commit can now automatically rebase (Alt-R) other work 262 | * Added duplicate (Alt-D) support to rebase (including descendants) 263 | * Added auto-completion support to bookmarks set/move (Alt-B) 264 | * Reparenting: added Alt-P to simplify-parents after `jj rebase` 265 | * Implemented faster op log preview handling 266 | * New config `jj-fzf.fzflog-depth` to increase `fzflog` depth 267 | * Ctrl-I: add diff browser between selected revision and working copy 268 | * F5: trigger a reload (shows concurrent jj repo changes) 269 | * Support rebase with --ignore-immutable via Alt-I 270 | * Implement adaptive key binding display (Alt-H) 271 | * Ctrl-H: show extended jj-fzf help via pager 272 | * Broadened divergent commit support: squash-into-parent, describe, log 273 | * Started adding unit tests and automated unit testing in CI 274 | * Introduced Makefile with rules to check, install, uninstall 275 | 276 | ### Breaking: 277 | * Depend on jj-0.24.0 and require fzf-0.43.0 278 | * Removed Alt-U for `jj duplicate`, use rebase instead: Alt-R Alt-D 279 | * Assert that bash supports interactive mode with readline editing 280 | * Check-deps: check dependencies before installing 281 | * Rebase: rename rebasing to `jj-fzf rebase` 282 | * Rebase: apply simplify-parents to the rebased revision only 283 | * Rename 'edit' (from 'edit-workspace') 284 | * Rename revset-assign → revset-filter 285 | * Op-log: Ctrl-S: Preview "@" at a specific operation via `jj show @` 286 | (formerly Ctrl-D) 287 | 288 | ### Changed: 289 | * Avoid JJ_CONFIG overrides in all places 290 | * Support ui.editor, ui.diff-editor and other settings 291 | * Squash-into-parent: use `jj new -A` to preserve change_id 292 | * Jump to first when reparenting and after rebase 293 | * Ctrl-P: jj git fetch default remote, not all 294 | * Support deletion of conflicted bookmarks 295 | * Line Blame: skip signoff and empty lines 296 | 297 | ### Fixed: 298 | * Avoid slowdowns during startup 299 | * Fixed some cases of undesired snapshotting 300 | * Lots of fixes and improvements to allow automated testing 301 | * Minor renames to make shellcheck happy 302 | * Log: Ctrl-L: fix missing patch output 303 | * Ensure `jj log` view change_id width matches jj log default width 304 | 305 | 306 | ## JJ-FZF 0.23.0 - 2024-11-11 307 | 308 | Development version - may contain bugs or compatibility issues. 309 | 310 | ### Breaking: 311 | * Depend on jj-0.23.0 312 | * Remove experimental line-history command 313 | 314 | ### Added: 315 | * Support 'gsed' as GNU sed binary name 316 | * Support line blame via: jj-fzf + 317 | * Support '--version' to print version 318 | * Define revset `jjlog` to match `jj log` 319 | * Define revset `fzflog` as `jjlog` + tags + bookmarks 320 | * Display `jj log -r fzflog` revset by default 321 | * Store log revset in --repo `jj-fzf.revsets.log` 322 | * Ctrl-R: reload log with new revset from query string 323 | 324 | ### Changed: 325 | * Require 'gawk' as GNU awk binary 326 | * Ctrl-Z: use user's $SHELL to execute a subshell 327 | * Shorten preview diffs with --ignore-all-space 328 | * Show error with delay after failing jj commands 329 | * Restore-file: operate on root relative file names 330 | * Split-files: operate on root relative file names 331 | * Fallback to @ if commands are called without a revision 332 | * Allow user's jj config to take effect in log display 333 | * Unset JJ_CONFIG in Ctrl+Z subshell 334 | * Rebase: Alt-P: toggle simplify-parents (off by default) 335 | * Reduce uses of JJ_CONFIG (overrides user configs) 336 | 337 | ### Fixed: 338 | * Split-files: use Git diff-tree for a robust file list 339 | * Ensure that internal sub-shell is bash to call functions, #1 340 | * Clear out tags in screencast test repo 341 | * Various smaller bug fixes 342 | * Add missing --ignore-working-copy in some places 343 | * Fix git_head() expression for jj-0.23.0 344 | 345 | ### Removed: 346 | * Remove unused color definitions 347 | * Skip explicit jj git import/export statements 348 | * Skip remove-parent in screencast, use simplify-parents 349 | 350 | ### Contributors 351 | 352 | Thanks to everyone who made this release happen! 353 | 354 | * Török Edwin (@edwintorok) 355 | * Tim Janik (@tim-janik) 356 | 357 | 358 | ## JJ-FZF 0.22.0 - 2024-11-05 359 | 360 | First project release, depending on jj-0.22.0, including the following commands: 361 | - *Alt-A:* abandon 362 | - *Alt-B:* bookmark 363 | - *Alt-C:* commit 364 | - *Alt-D:* delete-refs 365 | - *Alt-E:* diffedit 366 | - *Alt-F:* split-files 367 | - *Alt-I:* split-interactive 368 | - *Alt-K:* backout 369 | - *Alt-L:* line-history 370 | - *Alt-M:* merging 371 | - *Alt-N:* new-before 372 | - *Alt-P:* reparenting 373 | - *Alt-Q:* squash-into-parent 374 | - *Alt-R:* rebasing 375 | - *Alt-S:* restore-file 376 | - *Alt-T:* tag 377 | - *Alt-U:* duplicate 378 | - *Alt-V:* vivifydivergent 379 | - *Alt-W:* squash-@-into 380 | - *Alt-X:* swap-commits 381 | - *Alt-Z:* undo 382 | - *Ctrl-↑:* preview-up 383 | - *Ctrl-↓:* preview-down 384 | - *Ctrl-A:* author-reset 385 | - *Ctrl-D:* describe 386 | - *Ctrl-E:* edit-workspace 387 | - *Ctrl-F:* file-editor 388 | - *Ctrl-H:* help 389 | - *Ctrl-L:* log 390 | - *Ctrl-N:* new 391 | - *Ctrl-O:* op-log 392 | - *Ctrl-P:* push-remote 393 | - *Ctrl-T:* toggle-evolog 394 | - *Ctrl-U:* clear-filter 395 | - *Ctrl-V:* gitk 396 | 397 | See also `jj-fzf --help` or the wiki page 398 | [jj-fzf-help](https://github.com/tim-janik/jj-fzf/wiki/jj-fzf-help) for detailed descriptions. 399 | -------------------------------------------------------------------------------- /lib/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | 5 | # Avoid interference with use of `cd` 6 | unset CDPATH 7 | 8 | # $JJFZF_ABSPATHLIB points to jj-fzf/lib 9 | [[ "${BASH_SOURCE[0]}" = "${BASH_SOURCE[0]#/}" ]] && 10 | JJFZF_ABSPATHLIB="$PWD/${BASH_SOURCE[0]}" || JJFZF_ABSPATHLIB="${BASH_SOURCE[0]}" 11 | export JJFZF_ABSPATHLIB="${JJFZF_ABSPATHLIB%/*}" 12 | 13 | # Check for dependencies, define sed(), etc 14 | source "$JJFZF_ABSPATHLIB"/../preflight.sh 15 | 16 | # Common definitions also used by preview.sh 17 | source "$JJFZF_ABSPATHLIB"/common.sh 18 | 19 | # Bash function exports needs sub-shell to be bash too 20 | export JJFZF_ORIGPWD="$PWD" # save original PWD for interactive subshells 21 | export JJFZF_ORIGSHELL="${JJFZF_ORIGSHELL-$SHELL}" # save original $SHELL 22 | export SHELL=bash 23 | 24 | # Pager config 25 | command -v less >/dev/null && JJFZF_PAGER="less -Rc" || JJFZF_PAGER="more" 26 | export JJFZF_PAGER 27 | 28 | # JJ compatible --collor=-... arg 29 | test -z "${NO_COLOR-}" && JJFZF_COLOR=--color=always || JJFZF_COLOR=--color=never 30 | export JJFZF_COLOR 31 | # abandon-unreachable=true can be dangerous: https://github.com/jj-vcs/jj/discussions/7248#discussioncomment-14135120 32 | export JJFZF_KEEPCOMMITS=--config=git.abandon-unreachable-commits=false 33 | 34 | # == JJFZF_TEMPD == 35 | # Ensure temporary directory 36 | jjfzf_tempd() 37 | { 38 | test -n "${JJFZF_TEMPD-}" || { 39 | JJFZF_TEMPD=$(mktemp --tmpdir -d jjfzf.XXXXXX) && 40 | trap "rm -rf '$JJFZF_TEMPD'" 0 || 41 | { echo "$0: mktemp failed" >&2 ; exit 1 ; } 42 | export JJFZF_TEMPD 43 | } 44 | } 45 | 46 | # == jjfzf_bold_keys == 47 | jjfzf_bold_keys() 48 | ( 49 | # Key list (fzf) 50 | KL="Ctrl-[\\/_A-Z6^]|Ctrl-(Space|Delete)|Ctrl-\]|Ctrl-Alt-[A-Z]|Alt-[a-zA-Z]|F[1-9]|F1[012]" 51 | KL="$KL|(Alt-)?(Enter|Return|Space|Backspace|Bspace|Bs)" 52 | KL="$KL|Tab|Shift-Tab|Esc|(Shift-)?Delete|Del|Home|End|Insert|Page-Up|Pg[Uu]p|Page-Down|Pg[Dd]n" 53 | KL="$KL|(Alt-|Shift-|Alt-Shift-)?(Up|Down|Left|Right)" 54 | KL="$KL|Left-Click|Right-Click|Double-Click|Shift-Left-Click|Shift-Right-Click" 55 | KL="$KL|(Preview-)?Scroll-(Up|Down)|Shift-Scroll-(Up|Down)" # |[a-zA-Z] 56 | sed -r "s,\b($KL):,\x1b[1m\1:\x1b[0m,g" # Add bold around \bKEY: 57 | ) 58 | export -f jjfzf_bold_keys 59 | 60 | # == jjfzf_wrap_args == 61 | # Fold arguments at wrap width 62 | jjfzf_wrap_args() 63 | ( 64 | WRAP_WIDTH="$1" && shift # first argument = max line width 65 | LINE="" 66 | for ARG in "$@"; do 67 | # if adding ARG exceeds WRAP_WIDTH, start a new one 68 | if (( ${#LINE} + ${#ARG} + (${#LINE} > 0 ? 1 : 0) > WRAP_WIDTH )); then 69 | printf '%s\n' "$LINE" 70 | LINE="$ARG" 71 | else 72 | LINE="$LINE${LINE:+ }$ARG" 73 | fi 74 | done 75 | test -n "$LINE" && 76 | printf '%s\n' "$LINE" 77 | ) 78 | export -f jjfzf_wrap_args 79 | 80 | # == jjfzf_config == 81 | # Handle jj config without errors 82 | jjfzf_config() 83 | ( 84 | set -Eeuo pipefail 85 | JJ="jj --no-pager --ignore-working-copy" 86 | case "$1" in 87 | # [fallback] 88 | get) $JJ config get "$2" 2>/dev/null || { 89 | test -z "${3-}" || echo "$3" 90 | } ;; 91 | # 92 | set) $JJ config set --repo "$2" -- "$3" ;; 93 | # 94 | toggle) 95 | T="$($JJ config get "$2" 2>/dev/null || :)" 96 | test "$T" == 0 -o "$T" == false && T=true || T=false 97 | $JJ config set --repo "$2" "$T" 98 | ;; 99 | esac 100 | exit 0 101 | ) 102 | export -f jjfzf_config 103 | 104 | # == jjfzf_config_quote == 105 | # Add quotes and escapes to a stream to be usable as toml config value 106 | jjfzf_config_quote() # [prefix] [postfix] 107 | ( 108 | echo -n "''' \"${1-}" 109 | sed -r 's/([\\"])/\\\1/g;'"s/'/\\\\x27/g" 110 | echo -n "${2-}\" '''" 111 | ) 112 | export -f jjfzf_config_quote 113 | 114 | # == jjfzf_status == 115 | # Snapshot and show jj status if it changed 116 | jjfzf_status() 117 | ( 118 | CID=$(jj --no-pager --ignore-working-copy log --no-graph -r @ -T commit_id) 119 | if ( set -x && jj --no-pager status $JJFZF_COLOR ) > $JJFZF_TEMPD/status 2>&1 ; then 120 | test $(jj --no-pager --ignore-working-copy log --no-graph -r @ -T commit_id) == "$CID" || 121 | cat $JJFZF_TEMPD/status >&2 122 | ERR=0 123 | else 124 | ERR=$? 125 | cat $JJFZF_TEMPD/status >&2 126 | fi 127 | rm -f $JJFZF_TEMPD/status 128 | exit $ERR 129 | ) 130 | export -f jjfzf_status 131 | 132 | # == jjfzf_revset == 133 | # Determine jj log revset, supports reading $JJFZF_REVSET_OVERRIDE 134 | jjfzf_revset() 135 | ( 136 | set -Eeuo pipefail 137 | JJ="jj --no-pager --ignore-working-copy" 138 | test -n "${JJFZF_REVSET_OVERRIDE-}" && REVSET=$(cat "$JJFZF_REVSET_OVERRIDE" 2>/dev/null) || REVSET= 139 | test -n "$REVSET" || REVSET=$($JJ config get jj-fzf.log_revset 2>/dev/null) || : 140 | test -n "$REVSET" || REVSET=$($JJ config get revsets.log 2>/dev/null) || : 141 | test -n "$REVSET" || REVSET=:: 142 | echo "$REVSET" 143 | ) 144 | export -f jjfzf_revset 145 | export JJFZF_REVSET_OVERRIDE= # has to be changed *after* `source setup.sh` 146 | 147 | # == jjfzf_log_detailed == 148 | # Extend builtin_log_detailed 149 | # TODO: It'd be nice if JJ had a builtin_log_detailed + Parents + PRIVATE marker 150 | jjfzf_log_detailed() 151 | { 152 | local STAR='""++' 153 | test -n "$JJFZF_PRIVATE" && # see also JJFZF_PRIVATE_CONFIG 154 | STAR=" if(self.contained_in(\"$JJFZF_PRIVATE\") \&\& !immutable, label(\"committer\", \"🌟\")++\" \") ++ " 155 | local PARENTS=' "Parents :" ++ commit.parents().map(|c| " " ++ c.change_id()) ++ "\n" ' 156 | JJFZF_LOG_DETAILED_CONFIG="$( 157 | jjfzf_config get "template-aliases.'builtin_log_detailed(commit)'" | 158 | sed -r -e 's/(\bcommit\.change_id\(\)\s*\++\s*"\\n"),/\1,'"$PARENTS"',/' \ 159 | -e 's/(\bif\(commit\.description\()/'"$STAR"'\1/' 160 | )" 161 | export JJFZF_LOG_DETAILED_CONFIG=--config="template-aliases.'builtin_log_detailed(commit)'=''' $JJFZF_LOG_DETAILED_CONFIG '''" 162 | } 163 | 164 | # == jjfzf_jjlog == 165 | # Just run a non-snapshotting jj log 166 | jjfzf_jjlog() 167 | ( 168 | ARGS=(--ignore-working-copy --no-pager) 169 | # avoid underlines hiding +- diff chars 170 | ARGS+=( '--config=colors."diff token"={underline=false}' ) 171 | test -n "$JJFZF_PRIVATE_CONFIG" && ARGS+=( "$JJFZF_PRIVATE_CONFIG" ) 172 | jj "${ARGS[@]}" log "$@" 173 | ) 174 | export -f jjfzf_jjlog 175 | 176 | # == JJFZF_REVISION_TMPL == 177 | # Unique identifier for revisions, ideally use the change_id which has better usability and is stable 178 | # across ancestor rebase operations. Uunless a commit is hidden or divergent, in which case we must 179 | # use the commit_id for unique identification. 180 | export JJFZF_REVISION_TMPL='if(self.divergent()||self.hidden(),commit_id,change_id)' 181 | 182 | # == jjfzf_log0 == 183 | # Write current log with 0-separation 184 | jjfzf_log0() 185 | ( 186 | set -Eeuo pipefail 187 | JJ="jj --no-pager --ignore-working-copy" 188 | REVSET="$(jjfzf_revset)" 189 | TMPL=$($JJ config get jj-fzf.log_template 2>/dev/null) || 190 | TMPL=$($JJ config get templates.log 2>/dev/null) || 191 | TMPL=builtin_log_oneline 192 | LOGMODE=$(jjfzf_config get jj-fzf.log-mode) 193 | jjfzf_jjlog $JJFZF_COLOR $LOGMODE -r "$REVSET" \ 194 | -T " '¸'++stringify($JJFZF_REVISION_TMPL)++'¸¸' ++ $TMPL " 2>&1 | 195 | sed '/¸¸/s/^/\x00/ ; 1s/^\x00//' 196 | ) 197 | export -f jjfzf_log0 198 | 199 | # == jjfzf_load == 200 | # Wrapper to write log or oplog or evolog to $JJFZF_TEMPD/jjfzf_list, also supports --stdout 201 | jjfzf_load() 202 | ( 203 | set -Eeuo pipefail 204 | if test "${1-}" != "--stdout" ; then 205 | $JJFZF_LOAD_LIST > $JJFZF_TEMPD/jjfzf_list 2>&1 206 | else 207 | $JJFZF_LOAD_LIST 2>&1 | 208 | tee $JJFZF_TEMPD/jjfzf_list 209 | fi 210 | ) 211 | export -f jjfzf_load 212 | 213 | # == jjfzf_load_and_status == 214 | # Ensure JJ snapshot before jjfzf_load 215 | jjfzf_load_and_status() 216 | { 217 | jjfzf_status || : 218 | jjfzf_load 219 | } 220 | export -f jjfzf_load_and_status 221 | 222 | # == jjfzf_run == 223 | # Run JJ command, show command and error message, update $JJFZF_TEMPD/jjfzf_list 224 | jjfzf_run() 225 | ( 226 | set -Eeuo pipefail 227 | IGNORE=false NOLOAD=false _X=-x 228 | while test $# -ne 0 ; do 229 | case "$1" in \ 230 | +x) _X=+x ;; 231 | +e) IGNORE=true ;; 232 | +n) NOLOAD=true ;; 233 | +*) true ;; # skip 234 | *) break ;; 235 | esac 236 | shift 237 | done 238 | ERR=0 239 | if test -n "${1-}" ; then 240 | if ( set $_X; "$@" ) ; then 241 | : 242 | else 243 | ERR=$? 244 | $IGNORE && 245 | ERR=0 || { 246 | echo "jj-fzf: command exit_status=$ERR" >&2 247 | read -t 1 || : # pause 248 | } 249 | fi 250 | fi 251 | $NOLOAD || jjfzf_load_and_status 252 | exit $ERR 253 | ) 254 | export -f jjfzf_run 255 | 256 | # == jjfzf_oneline == 257 | jjfzf_oneline_graph() 258 | ( 259 | jjfzf_jjlog $JJFZF_COLOR -T builtin_log_oneline "$@" 260 | ) 261 | export -f jjfzf_oneline_graph 262 | 263 | # == jjfzf_oneline == 264 | jjfzf_oneline() 265 | ( 266 | jjfzf_jjlog $JJFZF_COLOR --no-graph -T builtin_log_oneline "$@" 267 | ) 268 | export -f jjfzf_oneline 269 | 270 | # == jjfzf_ccrevs == 271 | # Concat arguments into a single OR-ed revset 272 | jjfzf_ccrevs() 273 | ( 274 | set -Eeuo pipefail 275 | REVS=( "$@" ) 276 | IFS='|' 277 | echo "${REVS[*]}" 278 | ) 279 | export -f jjfzf_ccrevs 280 | 281 | # == jjfzf_list_commit_ids == 282 | # Produce newline-separated commit_id list from revset 283 | jjfzf_list_commit_ids() 284 | ( 285 | CIDTMPL='commit_id ++ "\n"' 286 | # forward chronological needs --reversed 287 | jj --no-pager --ignore-working-copy log --color=never --no-graph -T "$CIDTMPL" -r "$(jjfzf_ccrevs "$@")" 288 | ) 289 | export -f jjfzf_list_commit_ids 290 | 291 | # == jjfzf_list_change_ids == 292 | # Produce newline-separated change_id list (or commit_id if divergent) from revset 293 | jjfzf_list_change_ids() 294 | ( 295 | CIDTMPL="$JJFZF_REVISION_TMPL"' ++ "\n"' 296 | jj --no-pager --ignore-working-copy log --color=never --no-graph -T "$CIDTMPL" -r "$(jjfzf_ccrevs "$@")" 297 | ) 298 | export -f jjfzf_list_change_ids 299 | 300 | # == jjfzf_chronological_change_ids == 301 | # Produce newline-separated change_id list (or commit_id if divergent) in forward chronological order 302 | jjfzf_chronological_change_ids() 303 | ( 304 | CIDTMPL="$JJFZF_REVISION_TMPL"' ++ "\n"' 305 | # forward chronological needs --reversed 306 | jj --no-pager --ignore-working-copy log --color=never --no-graph -T "$CIDTMPL" --reversed -r "$(jjfzf_ccrevs "$@")" 307 | ) 308 | export -f jjfzf_chronological_change_ids 309 | 310 | # == jjfzf_contained == 311 | # Return if "$1" is contained in "$@" ? 312 | jjfzf_contained() 313 | { 314 | local first="$1" && shift 315 | for e in "$@"; do 316 | [[ "$e" == "$first" ]] && return 0 317 | done 318 | return 1 319 | } 320 | export -f jjfzf_contained 321 | 322 | # == jjfzf_list_unique_elements == 323 | # List unique array elements 324 | jjfzf_list_unique_elements() 325 | ( 326 | local -n arr_a="$1" # nameref to first array 327 | local -n arr_b="$2" # nameref to second array 328 | declare -A a_set # associative array 329 | for element in "${arr_a[@]}"; do 330 | a_set["$element"]=1 331 | done 332 | # Find elements in B but not in A 333 | for element in "${arr_b[@]}"; do 334 | [[ -z ${a_set["$element"]-} ]] && 335 | printf "%s\n" "$element" 336 | done 337 | true 338 | ) 339 | export -f jjfzf_list_unique_elements 340 | 341 | # == jjfzf_match_elements == 342 | # List elements from $1 matching $2 343 | jjfzf_match_elements() 344 | ( 345 | local -n arr_a="$1" # nameref to first array 346 | for element in "${arr_a[@]}"; do 347 | [[ "$element" =~ $2 ]] && 348 | printf "%s\n" "$element" 349 | done 350 | true 351 | ) 352 | export -f jjfzf_match_elements 353 | 354 | # == jjfzf_list_parents == 355 | # Print commit IDs in revset $1 356 | jjfzf_list_commits() 357 | ( 358 | # jj --no-pager --ignore-working-copy log --no-graph -r "$1" -T 'self.parents().map(|c|c.commit_id()).join("\n") ' 359 | jj --no-pager --ignore-working-copy log --no-graph -r "$1" -T 'commit_id++"\n"' 360 | ) 361 | export -f jjfzf_list_commits 362 | 363 | # == jjfzf_inject == 364 | # Inject revisions as historic commits before $1 365 | jjfzf_inject() 366 | ( 367 | set -Eeuo pipefail 368 | TREE=false 369 | while [ $# -gt 0 ]; do 370 | case "$1" in 371 | --tree) shift ; TREE=true ;; 372 | --diff) shift ; TREE=false ;; 373 | *) break ;; 374 | esac 375 | done 376 | REV="$1" && shift 377 | for ((i=$#; i>0; i--)); do 378 | C="${!i}" 379 | AUTHOR="$($JJFZFT 'self.author().name()' -r "$C")" 380 | EMAIL="$($JJFZFT 'self.author().email()' -r "$C")" 381 | TIMESTAMP="$($JJFZFT 'self.author().timestamp()' -r "$C")" 382 | DESCRIPTION="$($JJFZFT 'self.description()' -r "$C")" 383 | ARGS=( 384 | --config "user.name=\"$AUTHOR\"" 385 | --config "user.email=\"$EMAIL\"" 386 | --message="$DESCRIPTION" 387 | ) 388 | export JJ_TIMESTAMP="$(date --rfc-3339=ns -d "$TIMESTAMP")" 389 | if $TREE && [[ "$REV" == @ ]] ; then 390 | jjfzf_run +n jj --no-pager new --no-edit --insert-before @ "${ARGS[@]}" 391 | jjfzf_run +n jj --no-pager restore --restore-descendants --from "$C" --to @- 392 | else 393 | # TODO: JJ new --no-edit doesn't give us the new revision; so we have to count children 394 | # of the previous parent commit_id to find it (avoiding possibly divergent change IDs) 395 | COMMITS=( $(jjfzf_list_commits "$REV") ) # REV as commit_id 396 | [[ ${#COMMITS[@]} -eq 1 ]] && REV="${COMMITS[0]}" || 397 | { echo "jjfzf_inject: need exactly one matching commit: $REV" >&2 ; exit 1 ; } 398 | P=( $(jjfzf_list_commits "$REV-") ) && P1="${P[0]}" # first parent 399 | Cb=( $(jjfzf_list_commits "$P+") ) # children of first parent 400 | test "$(jjfzf_match_elements Cb $REV)" == "$REV" || 401 | { echo "jjfzf_inject: failed to find revision in ancestry: $REV" >&2 ; exit 1 ; } 402 | jjfzf_run +n jj --no-pager new --no-edit --insert-before "$REV" "${ARGS[@]}" 403 | Cn=( $(jjfzf_list_commits "$P+") ) # children of parent after new 404 | N=( $(jjfzf_list_unique_elements Cb Cn) ) # new commit + children 405 | [[ ${#N[@]} -eq 1 ]] || 406 | { echo "jjfzf_inject: failed to find revision newly created before: $REV" >&2 ; exit 1 ; } 407 | if $TREE ; then # --tree 408 | jjfzf_run +n jj --no-pager restore --restore-descendants --from "$C" --to "${N[0]}" 409 | else # --diff 410 | jjfzf_run +n jj --no-pager duplicate -r "$C" -d "$P" # duplicate (+rebase) commit onto new parent 411 | Cd=( $(jjfzf_list_commits "$P+") ) # children of parent after duplicate 412 | D=( $(jjfzf_list_unique_elements Cn Cd) ) # duplicated commit + children 413 | [[ ${#D[@]} -eq 1 ]] || 414 | { echo "jjfzf_inject: failed to find revision duplicated onto: $P" >&2 ; exit 1 ; } 415 | jjfzf_run +n jj --no-pager restore --restore-descendants --from "${D[0]}" --to "${N[0]}" 416 | jjfzf_run +n jj --no-pager abandon "${D[0]}" # discard duplicate after restore 417 | fi 418 | fi 419 | done # TODO: JJ ideally would support metadata copies for jj restore --restore-descendants 420 | ) 421 | export -f jjfzf_inject 422 | 423 | # == jjfzf_exec_usershell == 424 | # Exec $USERSHELL 425 | jjfzf_exec_usershell() 426 | { 427 | set -Eeuo pipefail 428 | USERSHELL="$JJFZF_ORIGSHELL" 429 | T=$(tty 2>/dev/null || tty <&1 2>/dev/null || tty <&2 2>/dev/null) || : 430 | if test -n "$T" ; then 431 | echo -e "\n#\n# Type \"exit\" to leave subshell\n#" 432 | for func in $(compgen -A function); do 433 | unset -f "$func" 434 | done # remove all exported functions 435 | unset CDPATH 436 | test -d "$JJFZF_ORIGPWD" && cd "$JJFZF_ORIGPWD" || : 437 | for var in $(compgen -v); do 438 | [[ "$var" =~ ^JJFZF ]] && 439 | unset "$var" 440 | done # remove all exported variables 441 | # Use 'exec' to avoid an unwanted intermediate parent process, see: pstree -ps $$ 442 | exec /usr/bin/env "$USERSHELL" <$T 1>$T 2>$T 443 | fi 444 | } 445 | export -f jjfzf_exec_usershell 446 | 447 | # == fzf == 448 | FZF_ARGS=( 449 | --ansi 450 | --track 451 | --no-tac 452 | --no-sort 453 | --extended 454 | --exact 455 | # --no-mouse 456 | --style default 457 | --border 458 | --input-border=line 459 | --header-border=line 460 | --preview-border=left 461 | --info default 462 | --layout reverse-list 463 | --scrollbar '▍' # '▌' 464 | --scroll-off 0 465 | --highlight-line 466 | --preview-label-pos 2 467 | --input-label-pos 2 468 | --list-label-pos 2 469 | --footer-label-pos 2 470 | --header-first 471 | --header-label-pos 2 472 | --list-label ' JJ-LOG ' 473 | --bind 'page-down:half-page-down' 474 | --bind 'page-up:half-page-up' 475 | --bind "scroll-down:offset-up" 476 | --bind "scroll-up:offset-down" 477 | --bind "alt-up:preview-up" 478 | --bind "alt-down:preview-down" 479 | --bind "alt-<:first" 480 | --bind "alt->:last" 481 | --bind 'ctrl-u:deselect-all+clear-query' 482 | --bind "ctrl-alt-w:toggle-wrap+toggle-preview-wrap" 483 | --bind "ctrl-x:jump" 484 | --bind "ctrl-z:execute( jjfzf_exec_usershell )+refresh-preview" 485 | --bind='f11:change-preview-window(bottom,50%,border-horizontal|hidden|)' 486 | --preview-window 'right,border-left' 487 | --info-command='echo -e " \x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO "' 488 | ) 489 | # Ctrl-U : Clear query or selection 490 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /jj-fzf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 3 | set -Eeuo pipefail #-x 4 | die() { echo "${BASH_SOURCE[0]##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } 5 | ABSPATHSCRIPT=$(readlink -f "${BASH_SOURCE[0]}") # Resolve symlinks to find installdir 6 | 7 | # == Setup & Options == 8 | source "${ABSPATHSCRIPT%/*}"/lib/setup.sh # preflight.sh common.sh 9 | jjfzf_tempd # assigns $JJFZF_TEMPD 10 | ENTER= 11 | PRINTOUT= 12 | WITH_PREVIEW=true 13 | while test $# -ne 0 ; do 14 | case "$1" in \ 15 | --version) echo "${ABSPATHSCRIPT##*/} `$JJFZF_ABSPATHLIB/../version.sh`"; exit ;; 16 | --help-bindings) PRINTOUT="$1" ;; 17 | -h|--help) echo "Usage: ${ABSPATHSCRIPT##*/} [--man] [--version] [OPTIONS...]"; exit ;; 18 | -c|+c|-r|+r|-s) ENTER="$1" ;; 19 | --man) exec man $JJFZF_ABSPATHLIB/../doc/jj-fzf.1 ;; 20 | --no-preview) WITH_PREVIEW=false ;; 21 | -x) set -x ;; 22 | *) break ;; 23 | esac 24 | shift 25 | done 26 | echo "DIFFMODE='$(jjfzf_config get jj-fzf.diff-mode)'" >> $JJFZF_TEMPD/preview.env 27 | echo "IGNORE_SPACE='$(jjfzf_config get jj-fzf.whitespace-mode)'" >> $JJFZF_TEMPD/preview.env 28 | 29 | # == Config == 30 | export JJFZF_REVSET_OVERRIDE="$JJFZF_TEMPD/log_revset" 31 | # Always ensure root relative paths 32 | JJROOT="$(jj --ignore-working-copy --no-pager workspace root 2>/dev/null)" && 33 | cd "$JJROOT" || 34 | die "$PWD: not a JJ repository" 35 | JJ="jj --no-pager --ignore-working-copy" 36 | export JJFZFT="$JJ log --no-graph -T" 37 | RELOAD="reload-sync(cat $JJFZF_TEMPD/jjfzf_list)" # see jjfzf_load 38 | 39 | # == jjfzf_full_commit == 40 | # Show commit(s) with diff and full info 41 | jjfzf_full_commit() 42 | ( 43 | set -Eeuo pipefail 44 | test -z "${1-}" && # may be revset 45 | exit 0 46 | IGNORE_SPACE= DIFFMODE= 47 | test -r "$JJFZF_TEMPD/preview.env" && . $JJFZF_TEMPD/preview.env # IGNORE_SPACE DIFFMODE 48 | test -z "$DIFFMODE" || DIFFMODE=--color-words 49 | test -z "$IGNORE_SPACE$DIFFMODE" && DIFF_COMMENT='""' || DIFF_COMMENT="\"diff $IGNORE_SPACE $DIFFMODE\\n\"" 50 | jjfzf_jjlog "$JJFZF_LOG_DETAILED_CONFIG" $JJFZF_COLOR $IGNORE_SPACE $DIFFMODE --no-graph -r "$1" -p \ 51 | -T '"\n" ++ concat( builtin_log_oneline , builtin_log_detailed , diff.stat() , "\n", '"$DIFF_COMMENT"' )' 52 | ) 53 | export -f jjfzf_full_commit 54 | jjfzf_log_detailed # for preview, assigns $JJFZF_LOG_DETAILED_CONFIG 55 | 56 | # == jjfzf_full_history == 57 | # Browse revset history in pager 58 | jjfzf_full_history() 59 | ( 60 | set -Eeuo pipefail 61 | REVSET="${1-@}" 62 | jjfzf_full_commit "$REVSET" | 63 | sed '1{/^$/d}' | 64 | $JJFZF_PAGER 65 | ) 66 | export -f jjfzf_full_history 67 | 68 | # == Key Bindings == 69 | BINDINGS=() 70 | declare -A POST 71 | declare -A KEY 72 | declare -A RUN 73 | declare -A DOC 74 | 75 | # Abandon 76 | KEY[abandon]="Alt-A" 77 | RUN[abandon]="jjfzf_run jj --no-pager abandon {+2}" 78 | DOC[abandon]='Use `jj abandon` to abandon the currently selected revisions.' 79 | 80 | # Commit 81 | KEY[commit]="Alt-C" POST[commit]="+first" 82 | RUN[commit]="JJFZF_EDITOR=\"${JJ_EDITOR-}\" JJ_EDITOR=\"$JJFZF_ABSPATHLIB/editor.sh\" jjfzf_run +e jj commit -i" 83 | DOC[commit]='Use `jj commit` to describe the currently selected revision and create a new child revision as working-copy.' 84 | 85 | # Delete bookmarks & tags 86 | KEY[bookmark]="Alt-B" 87 | DOC[bookmark]='Create, move, delete jj bookmarks and git tags.' 88 | RUN[bookmark]="jjfzf_run +x +e '$JJFZF_ABSPATHLIB'/bookmarks.sh -m {2}" 89 | 90 | # Diffedit interactively 91 | KEY[diffedit]="Alt-E" 92 | RUN[diffedit]="jjfzf_run +e jj --no-pager diffedit -r {2}" 93 | DOC[diffedit]='Use `jj diffedit` to interactively select content diff hunks to be kept in the currently selected revision, the rest is discarded.' 94 | 95 | # Split files --without-description 96 | jjfzf_filesplit() 97 | ( 98 | set -Eeuo pipefail 99 | COMMIT="$1" 100 | # read files affected by $COMMIT 101 | mapfile -t MAPFILE < <(jj --no-pager diff --name-only -r "$COMMIT") 102 | [[ ${#MAPFILE[@]} -gt 1 ]] || 103 | return 104 | # create n-1 new commits from n files 105 | while [[ ${#MAPFILE[@]} -gt 1 ]] ; do 106 | unset 'MAPFILE[-1]' # unset 'MAPFILE[${#MAPFILE[@]}-1]' 107 | export JJ_EDITOR='true' # Override ui.editor to implement --without-description 108 | jjfzf_run +n jj split -m= -r "$COMMIT" -- "${MAPFILE[@]}" 109 | done 110 | jjfzf_run 111 | ) 112 | export -f jjfzf_filesplit 113 | KEY[filesplit]="Alt-F" 114 | RUN[filesplit]="jjfzf_filesplit {2}" 115 | DOC[filesplit]='Use `jj split` in a loop to split each file modified by the currently selected revision into its own commit.' 116 | 117 | # Showkeys 118 | # KEY[showkeys]="Alt-H" # TODO: add for --help 119 | BINDINGS+=( --header-label "$(echo ' Alt-H: Toggle Key Display ⫶ Ctrl-H: help ' | jjfzf_bold_keys)" ) 120 | BINDINGS+=( --bind "alt-h:execute-silent( jjfzf_config toggle jj-fzf.show-keys )" ) # see also transform-header 121 | DOC[showkeys]='Display or hide the list of avilable key bindings, persist the setting in `jj-fzf.show-keys` of the `jj` user config.' 122 | 123 | # Help 124 | # KEY[help]="Ctrl-H" # TODO: add for --help 125 | BINDINGS+=( --bind "ctrl-h:execute( '$ABSPATHSCRIPT' --man )" ) 126 | DOC[help]='Show jj-fzf manual page.' 127 | 128 | # Split interactively 129 | KEY[split]="Alt-I" 130 | RUN[split]="jjfzf_split_i {2}" 131 | DOC[split]='Use `jj split` to interactively select content diff hunks to be split into a new commit with an empty description.' 132 | jjfzf_split_i() 133 | ( 134 | set -Eeuo pipefail 135 | # Split without message editing, instead keep only the first (original) description 136 | touch "$JJFZF_TEMPD/skip1" 137 | export POPFILE_LIST= KEEPFILE_LIST="$JJFZF_TEMPD/skip1" 138 | export JJ_EDITOR="$JJFZF_ABSPATHLIB/popfile.sh" # Override ui.editor and implement --without-description 139 | jjfzf_run +e jj split -m= --interactive -r "$1" 140 | ) 141 | export -f jjfzf_split_i 142 | 143 | # Inject commit 144 | KEY[inject]="Alt-J" 145 | RUN[inject]="jjfzf_inject --diff @ {2} ; jjfzf_load_and_status" 146 | DOC[inject]='Inject the currently selected revision as historic diff before @ while preserving its content.' 147 | 148 | # New-A 149 | KEY[new-a]="Alt-N" 150 | RUN[new-a]="jjfzf_run jj --no-pager new --no-edit --insert-after {2}" 151 | DOC[new-a]='Use `jj new --no-edit --insert-after` to create and insert a new revision after the currently selected revision. Add `Ctrl` to use `--insert-before` instead.' 152 | BINDINGS+=( --bind "ctrl-alt-n:execute( jjfzf_run jj --no-pager new --no-edit -B {2} )+$RELOAD+down" ) 153 | 154 | # Absorb a content diff into mutable ancestors 155 | KEY[absorb]="Alt-O" 156 | RUN[absorb]="jjfzf_run jj --no-pager absorb --from {2}" 157 | DOC[absorb]='Use `jj absorb` to split the content diff of the current revision and squash pieces into related mutable ancestors.' 158 | 159 | # Reparent Commits 160 | KEY[reparent]="Alt-P" 161 | DOC[reparent]='Change parent(s) of the current revision by adding a new or removing an existing parent, and optionally simplify the parent list.' 162 | RUN[reparent]="jjfzf_run +x +e '$JJFZF_ABSPATHLIB'/reparent.sh {+2}" 163 | 164 | # Squash 165 | KEY[squash]="Alt-Q" 166 | RUN[squash]="jjfzf_squash {2} {+2}" 167 | DOC[squash]='Use `jj squash` to move the changes from all selected revisions into the revision under the cursor.' 168 | jjfzf_squash() 169 | ( 170 | set -Eeuo pipefail 171 | TARGET="${1-}"; shift 172 | ALL="$(jjfzf_ccrevs "$@")" 173 | if test -n "$TARGET" ; then 174 | test "$TARGET" == "$ALL" && TARGET="$TARGET-" 175 | if test -n "$($JJFZFT 'true' -r "~$TARGET & ($ALL)")"; then 176 | if [[ true == $($JJFZFT 'true' -r "@ & ($ALL)") ]]; then 177 | # The working copy @ is to be squashed. Squashing without --keep-emptied would start a new branch at @- which 178 | # is undesired if @+ exists. But using --keep-emptied does not squash the message. As a workaround, create a 179 | # new @+, so we never squash directly from @. This new working copy will receive any children from the 180 | # original squashed working copy. 181 | jjfzf_run +n jj --no-pager new --insert-after @ 182 | fi 183 | jjfzf_run jj --no-pager squash --from "$ALL" --into "$TARGET" 184 | return # already called jjfzf_load_and_status 185 | fi 186 | fi 187 | # else 188 | jjfzf_load_and_status 189 | ) 190 | export -f jjfzf_squash 191 | 192 | # Restore 193 | KEY[restore]="Alt-S" 194 | RUN[restore]="jjfzf_restore {2} {+2}" 195 | DOC[restore]='Use `jj restore` to interactively select changes to be restored from a selected revisions into the revision under the cursor. Add `Ctrl` to run with `--restore-descendants`.' 196 | BINDINGS+=( --bind "ctrl-alt-s:execute( jjfzf_restore --restore-descendants {2} {+2} )+$RELOAD" ) 197 | jjfzf_restore() 198 | ( 199 | set -Eeuo pipefail 200 | ARGS=() 201 | while [[ "${1-}" == --* ]] ; do 202 | ARGS+=("$1") && shift 203 | done 204 | TARGET="${1-}"; shift 205 | ALL="$(jjfzf_ccrevs "$@")" 206 | if test -n "$TARGET" ; then 207 | test "$TARGET" == "$ALL" && TARGET="$TARGET-" 208 | if test -n "$($JJFZFT 'true' -r "~$TARGET & ($ALL)")"; then 209 | jjfzf_run jj restore --interactive --from "$ALL" --into "$TARGET" "${ARGS[@]}" 210 | return # already called jjfzf_load_and_status 211 | fi 212 | fi 213 | # else 214 | jjfzf_load_and_status 215 | ) 216 | export -f jjfzf_restore 217 | 218 | # Rebase Commits 219 | KEY[rebase]="Alt-R" 220 | DOC[rebase]='Use `jj rebase` or `jj duplicate` to move or copy a set of revisions (possibly with descendants), onto, before or after another revision. Also supports `jj simplify-parents` afterwards.' 221 | RUN[rebase]="jjfzf_run +x +e '$JJFZF_ABSPATHLIB'/rebase.sh {+2}" 222 | 223 | # Revert Commits 224 | KEY[revert]="Alt-V" POST[revert]="+first" 225 | RUN[revert]="jjfzf_run jj --no-pager revert -d @ -r \$(jjfzf_ccrevs {+2})" 226 | DOC[revert]='Use `jj revert` to create new commits that undo the changes made by the currently selected revisions and apply the changes on top of the working-copy.' 227 | 228 | # Swap 229 | KEY[swap]="Alt-X" POST[swap]="+down" 230 | RUN[swap]="jjfzf_run jj --no-pager rebase --insert-before {2}- -r {2}" 231 | DOC[swap]='Use `jj rebase --insert-before` to quickly swap the currenly selected revision with the revision immediately before it. Add `Ctrl` to use `--insert-after` instead.' 232 | BINDINGS+=( --bind "ctrl-alt-x:execute( jjfzf_run jj --no-pager rebase --insert-after {2}+ -r {2} )+$RELOAD+up" ) 233 | 234 | # Redo 235 | KEY[redo]="Alt-Y" 236 | RUN[redo]="jjfzf_run jj --no-pager redo" 237 | DOC[redo]='Use `jj redo` to redo the last undo operation performed by `jj undo`.' 238 | 239 | # Undo 240 | KEY[undo]="Alt-Z" 241 | RUN[undo]="jjfzf_run jj --no-pager undo" 242 | DOC[undo]='Use `jj undo` to undo the last operation performed by `jj` that was not previously undone.' 243 | 244 | # Whitespace toggle 245 | KEY[nospace]="Ctrl-B" 246 | DOC[nospace]='Cycle between preview diff display with and without whitespace.' 247 | BINDINGS+=( --bind "ctrl-b:execute-silent( jjfzf_whitespace_cycle )+refresh-preview" ) 248 | jjfzf_whitespace_cycle() 249 | ( 250 | CURRENT_WHITESPACE=$(jjfzf_config get jj-fzf.whitespace-mode) 251 | WHITESPACE_MODES=("--ignore-space-change" "--ignore-all-space" "") 252 | index=0 253 | for i in "${!WHITESPACE_MODES[@]}"; do 254 | [[ "${WHITESPACE_MODES[$i]}" == "$CURRENT_WHITESPACE" ]] && { index=$i && break; } 255 | done 256 | next_index=$(( (index + 1) % ${#WHITESPACE_MODES[@]} )) 257 | jjfzf_config set jj-fzf.whitespace-mode "${WHITESPACE_MODES[$next_index]}" 258 | sed "s/^IGNORE_SPACE=.*/IGNORE_SPACE='${WHITESPACE_MODES[$next_index]}'/" -i $JJFZF_TEMPD/preview.env 259 | ) 260 | export -f jjfzf_whitespace_cycle 261 | 262 | # Word-diff toggle 263 | KEY[wdiff]="Ctrl-W" 264 | DOC[wdiff]='Toggle preview diff display between line diff and word diff.' 265 | BINDINGS+=( --bind "ctrl-w:execute-silent( jjfzf_word_diff_toggle )+refresh-preview" ) 266 | jjfzf_word_diff_toggle() 267 | ( 268 | CURRENT_DIFFMODE=$(jjfzf_config get jj-fzf.diff-mode) 269 | DIFF_MODES=("--color-words" "") 270 | index=0 271 | for i in "${!DIFF_MODES[@]}"; do 272 | [[ "${DIFF_MODES[$i]}" == "$CURRENT_DIFFMODE" ]] && { index=$i && break; } 273 | done 274 | next_index=$(( (index + 1) % ${#DIFF_MODES[@]} )) 275 | jjfzf_config set jj-fzf.diff-mode "${DIFF_MODES[$next_index]}" 276 | sed "s/^DIFFMODE=.*/DIFFMODE='${DIFF_MODES[$next_index]}'/" -i $JJFZF_TEMPD/preview.env 277 | ) 278 | export -f jjfzf_word_diff_toggle 279 | 280 | # Toggle log --types 281 | KEY[types]="Ctrl-T" 282 | DOC[types]='Toggle display of a two letter file type diff in the log view.' 283 | RUN[types]="jjfzf_toggle_types ; jjfzf_load_and_status" 284 | jjfzf_toggle_types() 285 | ( 286 | CURRENT_LOGMODE=$(jjfzf_config get jj-fzf.log-mode) 287 | LOGMODES=("--types" "") 288 | index=0 289 | for i in "${!LOGMODES[@]}"; do 290 | [[ "${LOGMODES[$i]}" == "$CURRENT_LOGMODE" ]] && { index=$i && break; } 291 | done 292 | next_index=$(( (index + 1) % ${#LOGMODES[@]} )) 293 | jjfzf_config set jj-fzf.log-mode "${LOGMODES[$next_index]}" 294 | ) 295 | export -f jjfzf_toggle_types 296 | 297 | # Describe 298 | KEY[describe]="Ctrl-D" 299 | RUN[describe]="jjfzf_describe_commit {2}" 300 | DOC[describe]='Use `jj describe` to describe the currently selected revision.' 301 | jjfzf_describe_commit() 302 | ( 303 | REV="${1-@}" 304 | CONFIG=() 305 | if test true == $(jj --no-pager --ignore-working-copy log --no-graph \ 306 | -r "$REV" -T 'self.parents().len() > 1') ; then 307 | echo '[template-aliases]' > $JJFZF_TEMPD/descdraft.toml 308 | echo -n "default_commit_description=" >> $JJFZF_TEMPD/descdraft.toml 309 | $JJFZF_ABSPATHLIB/draft.sh "$REV" | 310 | jjfzf_config_quote '' '' >> $JJFZF_TEMPD/descdraft.toml 311 | CONFIG+=( --config-file $JJFZF_TEMPD/descdraft.toml ) 312 | fi 313 | export JJFZF_EDITOR="${JJ_EDITOR-}" # preserve user setting 314 | export JJ_EDITOR="$JJFZF_ABSPATHLIB/editor.sh" # wrapper that rejects non-edits 315 | jjfzf_run +e jj describe --edit "${CONFIG[@]}" "$REV" 316 | ) 317 | export -f jjfzf_describe_commit 318 | 319 | # Describe with LLM assistance 320 | RUN[synth]="jjfzf_describe_llm {2}" 321 | DOC[synth]='Use `jj describe` to edit a synthetic LLM generated description of the currently selected revision.' 322 | KEY[synth]="Ctrl-S" 323 | jjfzf_describe_llm() 324 | ( 325 | set -Eeuo pipefail 326 | REV="${1-@}" 327 | CONFIG=() 328 | COMMIT=$($JJFZFT 'if(description, "", commit_id)' -r "$REV") 329 | if test -n "$COMMIT" ; then 330 | echo '[template-aliases]' > $JJFZF_TEMPD/llmdraft.toml 331 | echo -n "default_commit_description=" >> $JJFZF_TEMPD/llmdraft.toml 332 | $JJFZF_ABSPATHLIB/gen-message.py --dup-output "$COMMIT" | 333 | jjfzf_config_quote '' '' >> $JJFZF_TEMPD/llmdraft.toml # $'\n' 334 | CONFIG+=( --config-file $JJFZF_TEMPD/llmdraft.toml ) 335 | fi 336 | export JJFZF_EDITOR="${JJ_EDITOR-}" # preserve $JJ_EDITOR user settings 337 | export JJ_EDITOR="$JJFZF_ABSPATHLIB/editor.sh" # use hash-comparing wrapper 338 | jjfzf_run +e jj describe --edit "${CONFIG[@]}" "$REV" 339 | ) 340 | export -f jjfzf_describe_llm 341 | 342 | # Edit 343 | KEY[edit]="Ctrl-E" 344 | RUN[edit]="jjfzf_run jj --no-pager \$($JJFZFT \"if(immutable,'new','edit')\" -r {2}) {2}" 345 | DOC[edit]='Use `jj {edit|new}` to set the currently selected revision (or divergent commit) as the working-copy revision. Will create a new empty commit if the selected revision is immutable.' 346 | 347 | # Enter - commit info 348 | case "$ENTER" in 349 | -c) BINDINGS+=( --bind 'enter:become( jjfzf_list_commit_ids {2} )' ) ;; 350 | +c) BINDINGS+=( --bind 'enter:become( jjfzf_list_commit_ids {+2} )' ) ;; 351 | -r) BINDINGS+=( --bind 'enter:become( jjfzf_list_change_ids {2} )' ) ;; 352 | +r) BINDINGS+=( --bind 'enter:become( jjfzf_list_change_ids {+2} )' ) ;; 353 | -s) BINDINGS+=( --bind 'enter:become( jjfzf_revset )' ) ;; 354 | *) BINDINGS+=( --bind 'enter:execute( jjfzf_full_history {2} )' ) ;; 355 | esac 356 | 357 | # Log history 358 | KEY[log]="Ctrl-L" 359 | BINDINGS+=( --bind 'ctrl-l:execute( jjfzf_selected_history {+2} )' ) 360 | DOC[log]='Use `jj log` to browse the history including patches, for the selected revisions, or the ancestry of a single revision.' 361 | jjfzf_selected_history() 362 | ( 363 | set -Eeuo pipefail 364 | if test -n "${2-}" ; then 365 | ALL="$(jjfzf_ccrevs "$@")" 366 | else 367 | ALL="..${1-@}" 368 | fi 369 | jjfzf_full_history "$ALL" 370 | ) 371 | export -f jjfzf_selected_history 372 | 373 | # New 374 | KEY[new]="Ctrl-N" POST[new]="+first" 375 | RUN[new]='jjfzf_run jj --no-pager new $(jjfzf_chronological_change_ids {+2})' 376 | DOC[new]='Use `jj new` to create a new revision on top of the currently selected revision(s).' 377 | 378 | # Oplog Commits 379 | KEY[oplog]="Ctrl-O" 380 | RUN[oplog]="jjfzf_run +x +e '$JJFZF_ABSPATHLIB'/oplog.sh" 381 | DOC[oplog]='Use `jj operation log` to browse the recent operations log. Use hotkeys to view operation diffs and history. Undo operations or restore its working copy into a new commit.' 382 | 383 | # Push & fetch from remotes 384 | KEY[push]="Ctrl-P" POST[new]="+first" 385 | DOC[push]='Use `jj git fetch` and `jj git push --tracked --deleted` to update the local and remote repositories. Pushing needs confirmation after a dry-run. Tries to push all refs if no revisions are currently selected. Uses `jj push` if that is configured as an alias for `jj-pre-push`.' 386 | RUN[push]="jjfzf_push {2} {+2}" 387 | jjfzf_push() 388 | ( 389 | set -Eeuo pipefail 390 | REVS=( --tracked --deleted ) 391 | if test ${#@} -gt 2 -o "$1" != "$2" ; then # fzf selection beyond pointer 392 | shift # eat pointer 393 | ALL="$(jjfzf_ccrevs "$@")" && 394 | REVS=( -r "$ALL" ) 395 | fi 396 | STATUS=0 397 | # fetch to ensure up-to-date bookmarks 398 | jjfzf_run +n jj git fetch $JJFZF_COLOR $JJFZF_KEEPCOMMITS \ 399 | || STATUS=$? 400 | # push --dry-run to see if anything needs pushing 401 | if test $STATUS == 0 ; then 402 | jjfzf_run +n jj git push "${REVS[@]}" $JJFZF_COLOR --dry-run > $JJFZF_TEMPD/push.log 2>&1 \ 403 | || STATUS=$? 404 | cat $JJFZF_TEMPD/push.log 405 | if test $STATUS == 0 && grep -qEi 'nothing *changed|won.?t push|rejected *commit' $JJFZF_TEMPD/push.log ; then 406 | STATUS=-1 407 | fi 408 | fi 409 | # jj-pre-push --dry-run to run hooks 410 | if test $STATUS == 0 -a -r .pre-commit-config.yaml && 411 | jjfzf_config get 'aliases.push' | grep -q '\bjj-pre-push\b' ; then 412 | jjfzf_run +n jj push "${REVS[@]}" --dry-run \ 413 | || STATUS=$? 414 | fi 415 | # push when confirmed 416 | if test $STATUS != 0 ; then 417 | read -p "Press Enter..." 418 | else 419 | read -p 'Proceed with actual push and submit changes? (y/N) ' YN 420 | [[ "${YN:0:1}" =~ [yY] ]] || exit 0 421 | jjfzf_run +n jj git push "${REVS[@]}" 422 | fi 423 | jjfzf_load_and_status 424 | ) 425 | export -f jjfzf_push 426 | 427 | # Evolog Commits 428 | KEY[evolog]="Ctrl-V" 429 | DOC[evolog]='Use `jj evolog` to browse the evolution of the selected revision. Inject historic commits into the ancestry without changing descendants.' 430 | RUN[evolog]="jjfzf_run +x +e '$JJFZF_ABSPATHLIB'/evolog.sh {2}" 431 | 432 | # Subshell 433 | KEY[shell]="Ctrl-Z" 434 | DOC[shell]='Start interactive subshell.' 435 | BINDINGS+=( --bind "ctrl-z:execute( jjfzf_exec_usershell )+execute( jjfzf_load_and_status )+$RELOAD+refresh-preview" ) 436 | # This needs to run execute() twice, because of the exec in jjfzf_exec_usershell 437 | 438 | # Reload 439 | KEY[reload]="F5" 440 | DOC[reload]='Reload the revset.' 441 | BINDINGS+=( --bind "f5:execute( jjfzf_load_and_status )+$RELOAD+refresh-preview" ) 442 | 443 | # Interactive Revset 444 | BINDINGS+=( --bind 'change:reload: jjfzf_revset_log {q} ' ) 445 | jjfzf_revset_log() 446 | ( 447 | Q="${1-}" 448 | if test "${Q:0:1}" != " " && 449 | jj --no-pager --ignore-working-copy log --no-graph -T '' -r "@&($Q)" >/dev/null 2>&1 ; then 450 | echo "$Q" > $JJFZF_REVSET_OVERRIDE 451 | else 452 | rm -f $JJFZF_REVSET_OVERRIDE 453 | fi 454 | jjfzf_load --stdout 455 | ) 456 | export -f jjfzf_revset_log 457 | 458 | # Ctrl-F toggle FZF vs Revset 459 | export JJFZF_PROMPT1="Revset > " JJFZF_ILABEL1="$(echo ' Ctrl-F: fzf-filter ⫶ Alt-Enter: save-revset ' | jjfzf_bold_keys)" 460 | export JJFZF_PROMPT2="fzf > " JJFZF_ILABEL2="$(echo ' Ctrl-F: Revset ' | jjfzf_bold_keys)" 461 | BINDINGS+=( --disabled --prompt "$JJFZF_PROMPT1" ) 462 | BINDINGS+=( --bind 'ctrl-f:transform: jjfzf_fzf_transform' ) 463 | jjfzf_fzf_transform() 464 | ( 465 | # https://github.com/junegunn/fzf/blob/master/ADVANCED.md#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding 466 | if [[ ! $FZF_PROMPT =~ Revset ]] ; then 467 | T="rebind(change)+rebind(alt-enter)+change-prompt($JJFZF_PROMPT1)+disable-search" 468 | T="$T+change-input-label($JJFZF_ILABEL1)" 469 | T="$T+change-ghost()" 470 | T="$T+transform-query:echo {q} > $JJFZF_TEMPD/q_fzf ; cat $JJFZF_TEMPD/q_revset" 471 | else 472 | T="unbind(change)+unbind(alt-enter)+change-prompt($JJFZF_PROMPT2)+enable-search" 473 | T="$T+change-input-label($JJFZF_ILABEL2)" 474 | T="$T+transform-query:echo {q} > $JJFZF_TEMPD/q_revset ; cat $JJFZF_TEMPD/q_fzf" 475 | fi 476 | echo "$T" # fzf transform instructions 477 | ) 478 | export -f jjfzf_fzf_transform 479 | BINDINGS+=( --bind 'ctrl-f,focus:+transform-ghost: [[ ! $FZF_PROMPT =~ Revset ]] && R={2} && echo -n "${R:0:12}" ' ) 480 | 481 | # Persist Revset 482 | BINDINGS+=( --input-label="$JJFZF_ILABEL1" ) 483 | BINDINGS+=( --bind 'alt-enter:execute-silent( jjfzf_set_revset {q} )+reload( jjfzf_load --stdout )+clear-query' ) 484 | BINDINGS+=( --info-command='echo -e " \x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO ⫶ all: $(jjfzf_revset)"' ) 485 | #KEY[revset]="Alt-Enter" # TODO: for --help only 486 | #DOC[revset]='Persist current revset in repo configuration.' 487 | jjfzf_set_revset() 488 | ( 489 | if test -z "${1-}" ; then # reset 490 | if test "$(jj --no-pager --ignore-working-copy config get jj-fzf.log_revset 2>/dev/null)" == "" ; then 491 | RECENT7=' @:: | ancestors(immutable_heads()..,7) | tags() | bookmarks() | remote_bookmarks() | present(trunk()) ' 492 | # toggle default revset 493 | jj --no-pager --ignore-working-copy config set --repo jj-fzf.log_revset "$RECENT7" 494 | else 495 | jj --no-pager --ignore-working-copy config unset --repo jj-fzf.log_revset 496 | fi 497 | elif test -r $JJFZF_REVSET_OVERRIDE ; then # assign new if validated 498 | jj --no-pager --ignore-working-copy config set --repo jj-fzf.log_revset "$(cat $JJFZF_REVSET_OVERRIDE)" 499 | fi 500 | true 501 | ) 502 | export -f jjfzf_set_revset 503 | 504 | # == RUN commands == 505 | if [ -v "RUN[${1-}]" ]; then 506 | KEY="$1" && CMD="${RUN[$KEY]}" ; shift 507 | if [[ "$CMD" =~ \{ ]] ; then 508 | CIDS=( $(jjfzf_list_change_ids "${@-@}") ) 509 | test "${#CIDS[@]}" -lt 1 && 510 | die "missing revisions for command: $KEY" 511 | # Placeholder replacments 512 | while [[ "$CMD" =~ (.*)\{2\}(.*) ]] ; do 513 | CMD="${BASH_REMATCH[1]}${CIDS[0]}${BASH_REMATCH[2]}" 514 | done 515 | while [[ "$CMD" =~ (.*)\{[+]2\}(.*) ]] ; do 516 | CMD="${BASH_REMATCH[1]}${CIDS[*]}${BASH_REMATCH[2]}" 517 | done 518 | [[ "$CMD" =~ \{ ]] && 519 | die "failed to run command: $KEY" 520 | fi 521 | export JJFZF_LOAD_LIST=true 522 | bash -c " $CMD " 523 | exit 524 | fi 525 | 526 | # == Process Bindings == 527 | declare -a CMDS # Sort commands by key binding string (KEY[$cmd]) 528 | while IFS= read -r line; do 529 | CMDS+=("${line#* }") 530 | done < <(for cmd in "${!KEY[@]}"; do echo "${KEY[$cmd]} $cmd"; done | sort) 531 | for cmd in "${CMDS[@]}"; do 532 | key="${KEY[$cmd],,}" 533 | post="$RELOAD${POST[$cmd]-}" 534 | KEYDOCS+=( "${KEY[$cmd]}: $cmd" ) 535 | test -n "${RUN[$cmd]-}" && 536 | BINDINGS+=( --bind "$key:execute( ${RUN[$cmd]} )+$post" ) 537 | done 538 | 539 | # == jjfzf_showkeys == 540 | declare -p KEYDOCS > $JJFZF_TEMPD/jjfzf_KEYDOCS 541 | jjfzf_showkeys() 542 | ( 543 | set +u 544 | HEADER_LINES="${LINES-$FZF_LINES}" 545 | test -n "$FZF_PREVIEW_LEFT" && test "$FZF_PREVIEW_LEFT" -gt $((FZF_COLUMNS / 10)) && 546 | HEADER_COLUMNS="$FZF_PREVIEW_LEFT" || HEADER_COLUMNS="$FZF_COLUMNS" 547 | if test "$(jjfzf_config get jj-fzf.show-keys)" == false -o "$HEADER_LINES" -lt 20 -o "$HEADER_COLUMNS" -lt 20 ; then 548 | : # echo "Ctrl-H:help Alt-H:showkeys" 549 | else 550 | source $JJFZF_TEMPD/jjfzf_KEYDOCS 551 | FMT_WIDTH="$((HEADER_COLUMNS - 8))" # Subtract fzf border 552 | printf '%s\n' "${KEYDOCS[@]}" | 553 | awk -v WIDTH="$FMT_WIDTH" -f $JJFZF_ABSPATHLIB/ocolumns.awk | 554 | jjfzf_bold_keys 555 | fi 556 | ) 557 | export -f jjfzf_showkeys 558 | BINDINGS+=( --bind "start,resize,alt-h,f11:+transform-header: jjfzf_showkeys 2>&1 " ) 559 | 560 | # == PRINTOUT == 561 | [[ "$PRINTOUT" == --help-bindings ]] && { 562 | for cmd in "${CMDS[@]}"; do 563 | echo 564 | echo "### _${KEY[$cmd]}_:" "**$cmd**" 565 | echo "${DOC[$cmd]}" 566 | done 567 | exit 0 568 | } 569 | 570 | # == fzf == 571 | V=$($JJFZF_ABSPATHLIB/../version.sh | sed 's/-0-g.*//; s/-[1-9][0-9]*-g.*/+/') 572 | FZF_ARGS+=( 573 | --marker=' ' 574 | --border-label "-[ JJ-FZF ($V) ]-" --color=border:cyan,label:cyan 575 | --preview-label " Revision Info " 576 | ) 577 | $WITH_PREVIEW && 578 | FZF_ARGS+=( --preview "jjfzf_full_commit {2} | sed '1{/^$/d}; 3001q' " ) 579 | jjfzf_status 580 | export JJFZF_LOAD_LIST=jjfzf_log0 581 | unset FZF_DEFAULT_OPTS FZF_DEFAULT_COMMAND 582 | jjfzf_load --stdout | 583 | fzf -m "${FZF_ARGS[@]}" "${BINDINGS[@]}" \ 584 | --read0 '-d¸' --accept-nth=2 --with-nth '{1} {4..}' \ 585 | --header-first 586 | # Notes: 587 | # - Initial listing is read async to quickly popup the fzf log view 588 | # - Commands are executed, then the new log is cached in $JJFZF_TEMPD/jjfzf_list and synchronously read by fzf 589 | # This allows fzf to track the current cursor position 590 | # --bind 'start,resize:transform-header(env | fgrep FZF)' \ 591 | --------------------------------------------------------------------------------