Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package jj-fzf for openSUSE:Factory checked in at 2025-08-21 17:00:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/jj-fzf (Old) and /work/SRC/openSUSE:Factory/.jj-fzf.new.29662 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "jj-fzf" Thu Aug 21 17:00:18 2025 rev:4 rq:1300735 version:0.32.0 Changes: -------- --- /work/SRC/openSUSE:Factory/jj-fzf/jj-fzf.changes 2025-03-05 15:19:05.716247913 +0100 +++ /work/SRC/openSUSE:Factory/.jj-fzf.new.29662/jj-fzf.changes 2025-08-21 17:00:30.896944222 +0200 @@ -1,0 +2,38 @@ +Thu Aug 21 06:41:55 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- Update to version 0.32.0: + * Added: + - Ctrl-W: Added way to toggle between various diff formats + - Alt+M: New multi-select mode, use TAB to select multiple + commits + - Added multi-mode support for abandon, backout, duplicate, + squash, rebase + - Added make distcheck, always check in CI + - Added check for jj-fzf --help + - Added installcheck rule + - Added separate manual page + - Added file summary to oplog history + - Added scripts to automate releases + - Added contirb/suspend-with-shell.el to run jj-fzf from emacs, + see: https://testbit.eu/2025/jj-fzf-in-emacs + * Breaking: + - Depend on jj-0.32.0 + - Changed Alt-N to run new-after with --no-edit + - Preserve PWD in subshells if possible (present) + - Remove unused 'merging' command + * Changed: + - To install, run make all install + - To run all checks, run make all check install installcheck + - Builds require GNU Make + - Moved version checks for all tool dependencies into Makefile + - Use /usr/bin/env to find bash + - Undeprecate Alt-S: restore-file from selected revision + - Build man page, use a man page browser for jj-fzf --help + - Fetch version information from Git + - Automatically run CI for PRs and tags + - Introduced pandoc dependency for man builds + * Fixed: + - Fixed installations not working in non-jj repos + - Fixed --version not working outside a jj repo + +------------------------------------------------------------------- Old: ---- jj-fzf-0.25.0.obscpio New: ---- jj-fzf-0.32.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ jj-fzf.spec ++++++ --- /var/tmp/diff_new_pack.NTrkKY/_old 2025-08-21 17:00:31.416965999 +0200 +++ /var/tmp/diff_new_pack.NTrkKY/_new 2025-08-21 17:00:31.420966167 +0200 @@ -1,7 +1,7 @@ # # spec file for package jj-fzf # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2025 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: jj-fzf -Version: 0.25.0 +Version: 0.32.0 Release: 0 Summary: Text UI for Jujutsu based on fzf License: MPL-2.0 @@ -57,8 +57,17 @@ %autosetup -p1 %build +DATE_FMT="+%%Y-%%m-%%dT%%H:%%M:%%SZ" +BUILD_DATE=$(date -u -d "@${SOURCE_DATE_EPOCH}" "${DATE_FMT}" 2>/dev/null || date -u -r "${SOURCE_DATE_EPOCH}" "${DATE_FMT}" 2>/dev/null || date -u "${DATE_FMT}") + sed -i 's#env bash#bash#' %{name} +# fix version output in jj-fzf, +# so the version.sh script (which is not working) is +# no longer required +sed -i '/--version/ s|".*"|%{name} %{version}|' %{name} +sed -i "/--version/ s|%{name} %{version}|%{name} %{version} ${BUILD_DATE}|" %{name} + %install install -D -d -m 0755 %{buildroot}%{_bindir} install -m 0755 %{name} %{buildroot}%{_bindir}/%{name} @@ -68,6 +77,7 @@ jj git init # version output without leading "v" %{buildroot}%{_bindir}/%{name} --version | grep %{version} +%{buildroot}%{_bindir}/%{name} --version %files %license LICENSE ++++++ _service ++++++ --- /var/tmp/diff_new_pack.NTrkKY/_old 2025-08-21 17:00:31.448967339 +0200 +++ /var/tmp/diff_new_pack.NTrkKY/_new 2025-08-21 17:00:31.452967506 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/tim-janik/jj-fzf</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.25.0</param> + <param name="revision">v0.32.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.NTrkKY/_old 2025-08-21 17:00:31.468968176 +0200 +++ /var/tmp/diff_new_pack.NTrkKY/_new 2025-08-21 17:00:31.472968344 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/tim-janik/jj-fzf</param> - <param name="changesrevision">5fae4591a73b1ff1fb38633f97b902a78b8dd81d</param></service></servicedata> + <param name="changesrevision">01e93e5640b41a753cc1bca69dc1b225ce645115</param></service></servicedata> (No newline at EOF) ++++++ jj-fzf-0.25.0.obscpio -> jj-fzf-0.32.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/.github/workflows/ircbot.py new/jj-fzf-0.32.0/.github/workflows/ircbot.py --- old/jj-fzf-0.25.0/.github/workflows/ircbot.py 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/.github/workflows/ircbot.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,211 +0,0 @@ -#!/usr/bin/env python3 -# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 -import sys, os, re, socket, select, time - -# https://datatracker.ietf.org/doc/html/rfc1459 - -server = "irc.libera.chat" -port = 6667 -channel = "#anklang2" -nickname = "YYBOT" -ircsock = None -timeout = 150 -wait_timeout = 15000 - -def colors (how): - E = '\u001b[' - C = '\u0003' - if how == 0: # NONE - d = { 'YELLOW': '', 'ORANGE': '', 'RED': '', 'GREEN': '', 'CYAN': '', 'BLUE': '', 'MAGENTA': '', 'RESET': '' } - elif how == 1: # ANSI - d = { 'YELLOW': E+'93m', 'ORANGE': E+'33m', 'RED': E+'31m', 'GREEN': E+'32m', 'CYAN': E+'36m', 'BLUE': E+'34m', 'MAGENTA': E+'35m', 'RESET': E+'m' } - elif how == 2: # mIRC - d = { 'YELLOW': C+'08,99', 'ORANGE': C+'07,99', 'RED': C+'04,99', 'GREEN': C+'03,99', 'CYAN': C+'10,99', 'BLUE': C+'12,99', 'MAGENTA': C+'06,99', 'RESET': C+'' } - from collections import namedtuple - colors = namedtuple ("Colors", d.keys()) (*d.values()) - return colors - -def status_color (txt, c): - ER = r'false|\bno\b|\bnot|\bfail|fatal|error|\bwarn|\bbug|\bbad|\bred|broken' - OK = r'true|\byes|\bok\b|success|\bpass|good|\bgreen' - if re.search (ER, txt, flags = re.IGNORECASE): - return c.RED - if re.search (OK, txt, flags = re.IGNORECASE): - return c.GREEN - return c.YELLOW - -def format_msg (args, how = 2): - msg = ' '.join (args.message) - c = colors (how) - if args.S: - msg = '[' + status_color (args.S, c) + args.S.upper() + c.RESET + '] ' + msg - if args.D: - msg = c.CYAN + args.D + c.RESET + ' ' + msg - if args.U: - msg = c.ORANGE + args.U + c.RESET + ' ' + msg - if args.R: - msg = '[' + c.BLUE + args.R + c.RESET + '] ' + msg - return msg - -def sendline (text): - global args - if not args.quiet: - print (text, flush = True) - msg = text + "\r\n" - ircsock.send (msg.encode ('utf8')) - -def connect (server, port): - global ircsock - ircsock = socket.socket (socket.AF_INET, socket.SOCK_STREAM) - ircsock.connect ((server, port)) - ircsock.setblocking (True) # False - -def canread (milliseconds): - rs, ws, es = select.select ([ ircsock ], [], [], milliseconds * 0.001) - return ircsock in rs - -readall_buffer = b'' # unterminated start of next line -def readall (milliseconds = timeout): - global readall_buffer - gotlines = False - while canread (milliseconds): - milliseconds = 0 - buf = ircsock.recv (128 * 1024) - if len (buf) == 0: - # raise (Exception ('SOCKET BROKEN:', 'readable but has 0 data')) - break # socket closed - gotlines = True - readall_buffer += buf - if readall_buffer.find (b'\n') >= 0: - lines, readall_buffer = readall_buffer.rsplit (b'\n', 1) - lines = lines.decode ('utf8', 'replace') - for l in lines.split ('\n'): - if l: - gotline (l.rstrip()) - return gotlines - -def gotline (msg): - global args - if not args.quiet: - print (msg, flush = True) - cmdargs = re.split (' +', msg) - if cmdargs: - prefix = '' - if cmdargs[0] and cmdargs[0][0] == ':': - prefix = cmdargs[0] - cmdargs = cmdargs[1:] - if not cmdargs: - return - gotcmd (prefix, cmdargs[0], cmdargs[1:]) - -expecting_commands = [] -check_cmds = [] -def gotcmd (prefix, cmd, args): - global expecting_commands, check_cmds - if check_cmds: - try: check_cmds.remove (cmd) - except: pass - if cmd in expecting_commands: - expecting_commands = [] - if cmd == 'PING': - return sendline ('PONG ' + ' '.join (args)) - -def expect (what = []): - global expecting_commands - expecting_commands = what if isinstance (what, (list, tuple)) else [ what ] - while readall (wait_timeout) and expecting_commands: pass - if expecting_commands: - raise (Exception ('MISSING REPLY: ' + ' | '.join (expecting_commands))) - -usage_help = ''' -Simple IRC bot for short messages. -A password for authentication can be set via $IRCBOT_PASS. -''' - -def parse_args (sysargs): - import argparse - global server, port, nickname - parser = argparse.ArgumentParser (description = usage_help) - parser.add_argument ('message', metavar = 'messages', type = str, nargs = '*', - help = 'Message to post on IRC') - parser.add_argument ('-j', metavar = 'CHANNEL', default = '', - help = 'Channel to join on IRC') - parser.add_argument ('-J', metavar = 'CHANNEL', default = '', - help = 'Message channel without joining') - parser.add_argument ('-n', metavar = 'NICK', default = nickname, - help = 'Nickname to use on IRC [' + nickname + ']') - parser.add_argument ('-s', metavar = 'SERVER', default = server, - help = 'Server for IRC connection [' + server + ']') - parser.add_argument ('-p', metavar = 'PORT', default = port, type = int, - help = 'Port to connect to [' + str (port) + ']') - parser.add_argument ('-l', action = "store_true", - help = 'List channels') - parser.add_argument ('-R', metavar = 'REPOSITORY', default = '', - help = 'Initiating repository name') - parser.add_argument ('-U', metavar = 'NAME', default = '', - help = 'Initiating user name') - parser.add_argument ('-D', metavar = 'DEPARTMENT', default = '', - help = 'Initiating department') - parser.add_argument ('-S', metavar = 'STATUS', default = '', - help = 'Initiating status code') - parser.add_argument ('--ping', action = "store_true", - help = 'Require PING/PONG after connecting') - parser.add_argument ('--quiet', '-q', action = "store_true", - help = 'Avoid unnecessary output') - args = parser.parse_args (sysargs) - #print ('ARGS:', repr (args), flush = True) - return args - -args = parse_args (sys.argv[1:]) -if args.message and not args.quiet: - print (format_msg (args, 1)) -connect (args.s, args.p) -readall (500) - -ircbot_pass = os.getenv ("IRCBOT_PASS") -if ircbot_pass: - sendline ("PASS " + ircbot_pass) -sendline ("USER " + args.n + " localhost " + server + " :" + args.n) -readall() -sendline ("NICK " + args.n) -expect ('251') # LUSER reply - -if args.ping: - sendline ("PING :pleasegetbacktome") - expect ('PONG') - -if args.j: - #sendline ("PING :ircbotpyping") - #expect ('PONG') - sendline ("JOIN " + args.j) - expect ('JOIN') - -msg = format_msg (args) -for line in re.split ('\n ?', msg): - channel = args.j or args.J or args.n - if line: - sendline ("PRIVMSG " + channel + " :" + line) - readall() - -if args.l: - sendline ("LIST") - check_cmds = [ '322' ] - expect ('323') - if check_cmds: - # empty list, retry after 60seconds - time.sleep (30) - check_cmds = [ 'PING' ] - readall() - if check_cmds: - sendline ("PING :pleasegetbacktome") - expect ('PONG') - time.sleep (30) - readall() - sendline ("LIST") - expect ('323') - -readall (500) - -sendline ("QUIT :Bye Bye") -expect (['QUIT', 'ERROR']) -ircsock.close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/.github/workflows/testing.yml new/jj-fzf-0.32.0/.github/workflows/testing.yml --- old/jj-fzf-0.25.0/.github/workflows/testing.yml 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/.github/workflows/testing.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,50 +0,0 @@ -# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 - -# Linting: xclip -sel c <.github/workflows/testing.yml # https://rhysd.github.io/actionlint/ - -on: - pull_request: - push: - branches: [ '**' ] - # tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ] - -jobs: - - MakeCheck: - runs-on: ubuntu-24.04 - steps: - - { uses: actions/checkout@v4.1.1, with: { fetch-depth: 0, submodules: recursive, github-server-url: 'https://github.com' } } - - run: git fetch -f --tags && git describe --long # Fix actions/checkout#290 - - run: | - curl -s -L https://github.com/junegunn/fzf/releases/download/v0.56.3/fzf-0.56.3-linux_amd64.tar.gz | - tar zxvf - -C ~/.cargo/bin/ fzf - fzf --version - - run: | - curl -s -L https://github.com/martinvonz/jj/releases/download/v0.25.0/jj-v0.25.0-x86_64-unknown-linux-musl.tar.gz | - tar zxvf - -C ~/.cargo/bin/ ./jj - jj --version - - run: | - jj git init --colocate - - run: | - make check - - Ping-IRC: - if: always() - needs: [MakeCheck] - runs-on: ubuntu-24.04 - steps: - - { uses: actions/checkout@v4.1.1, with: { fetch-depth: 0, github-server-url: 'https://github.com' } } - - run: git fetch -f --tags && git describe --long # Fix actions/checkout#290 - - name: Check Jobs - run: | - echo '${{ needs.MakeCheck.result }}' - [[ ${{ needs.MakeCheck.result }} =~ success|skipped ]] - - name: Ping IRC - if: ${{ always() && !env.ACT }} - run: | - R='${{ github.repository }}' && R=${R#*/} - B='${{ github.ref }}' && B=${B#refs/heads/} - S='${{ job.status }}' && URL='${{ github.event.head_commit.url }}' - A='${{ github.actor }}' && B="$(git branch --show-current)" - MSG=$(git log -1 --format='%s') - .github/workflows/ircbot.py -q -j "#Anklang" -R "$R" -U "$A" -D "$B" -S "$S" "$MSG" "$URL" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/.gitignore new/jj-fzf-0.32.0/.gitignore --- old/jj-fzf-0.25.0/.gitignore 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/.gitignore 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -wiki/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/GNUmakefile new/jj-fzf-0.32.0/GNUmakefile --- old/jj-fzf-0.25.0/GNUmakefile 1970-01-01 01:00:00.000000000 +0100 +++ new/jj-fzf-0.32.0/GNUmakefile 2025-08-14 03:36:35.000000000 +0200 @@ -0,0 +1 @@ +include Makefile.mk diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/Makefile new/jj-fzf-0.32.0/Makefile --- old/jj-fzf-0.25.0/Makefile 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/Makefile 1970-01-01 01:00:00.000000000 +0100 @@ -1,32 +0,0 @@ -# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 - -SHELL := /bin/bash -o pipefail -prefix ?= /usr/local -bindir ?= ${prefix}/bin -INSTALL := install -c -RM := rm -f -Q := $(if $(findstring 1, $(V)),, @) - -all: check - -check-deps: jj-fzf - $Q ./jj-fzf --version - $Q ./jj-fzf --help >/dev/null # check-deps -install: check-deps - $(INSTALL) jj-fzf "$(bindir)" -uninstall: - $(RM) "$(bindir)/jj-fzf" - -shellcheck-warning: jj-fzf - $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } - shellcheck -W 3 -S warning -e SC2178,SC2207,SC2128 jj-fzf -shellcheck-error: - $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } - shellcheck -W 3 -S error jj-fzf -tests-basics.sh: - $Q tests/basics.sh -check-gsed: jj-fzf - $Q ! grep --color=auto -nE '[^\\]\bsed ' jj-fzf /dev/null \ - || { echo "ERROR: use gsed" >&2 ; false; } - $Q echo ' OK gsed uses' -check: check-deps shellcheck-error check-gsed tests-basics.sh diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/Makefile.mk new/jj-fzf-0.32.0/Makefile.mk --- old/jj-fzf-0.25.0/Makefile.mk 1970-01-01 01:00:00.000000000 +0100 +++ new/jj-fzf-0.32.0/Makefile.mk 2025-08-14 03:36:35.000000000 +0200 @@ -0,0 +1,131 @@ +# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 + +all: +SHELL := /usr/bin/env bash -o pipefail +version_full != ./version.sh +version_bits := $(subst _, , $(subst -, , $(subst ., , $(version_full)))) +PREFIX ?= /usr/local +BINDIR ?= ${PREFIX}/bin +SHAREDIR ?= $(PREFIX)/share +MANDIR ?= $(SHAREDIR)/man +PKGVERSION := $(word 1, $(version_bits)).$(word 2, $(version_bits)) +LIBEXEC ?= libexec/jj-fzf-$(PKGVERSION) +PRJDIR ?= $(PREFIX)/$(LIBEXEC) +CLEANFILES := *.tmp +CLEANDIRS := +Q := $(if $(findstring 1, $(V)),, @) +QGEN = @echo ' GEN ' $@ +QSKIP := $(if $(findstring s,$(MAKEFLAGS)),: ) +QECHO = @QECHO() { Q1="$$1"; shift; QR="$$*"; QOUT=$$(printf ' %-8s ' "$$Q1" ; echo "$$QR") && $(QSKIP) echo "$$QOUT"; }; QECHO + +# == Compare Versions == +# Shell command that is true if $1 <= $2 in version comparisons +VERSION_LE := python3 -c 'import sys, re; v1 = list (map (int, sys.argv[1].split ("."))); v2 = list (map (int, re.search (r"\b[0-9]+\.[0-9]+\.[0-9]+", sys.argv[2]).group().split ("."))); sys.exit (v1 > v2)' + +# == Check presence of dependencies == +check-deps: jj-fzf + $(QGEN) + $Q python3 -c "import sys; sys.exit ((3,9) >= sys.version_info);" || { echo "$@: ERROR: python3 >= 3.9 is required" >&2; false; } + $Q V="5.1.16" && T="`bash --version`" && $(VERSION_LE) "$$V" "$$T" || { echo "$@: ERROR: \`bash\` >= $$V is required, found: $${T%%$$'\n'*}" >&2; false; } + $Q [[ "`bash -c 'set -o'`" =~ emacs ]] || { echo "$@: ERROR: the \`bash\` executable lacks interactive readline support" >&2; false; } + $Q command -v gsed 2>/dev/null 1>&2 || gsed() { \sed "$$@"; } \ + && gsed --version 2>/dev/null | grep -Fq 'GNU sed' || { echo "$@: failed to detect GNU sed as \`gsed\` or \`sed\`" >&2; false; } + $Q [[ "`awk 'BEGIN{print(123)}'`" =~ 123 ]] || { echo "$@: ERROR: a usable \`awk\` executable is required" >&2; false; } + $Q V="0.32.0" && T="`jj --version --ignore-working-copy`" && $(VERSION_LE) "$$V" "$$T" || { echo "$@: ERROR: jj >= $$V is required, found: $${T%%$$'\n'*}" >&2; false; } + $Q V="0.44.1" && T="`fzf --version`" && $(VERSION_LE) "$$V" "$$T" || { echo "$@: ERROR: fzf >= $$V is required, found: $${T%%$$'\n'*}" >&2; false; } + $Q [[ "`command -v column`" =~ column ]] || { echo "$@: ERROR: failed to find the \`column\` executable in \$$PATH" >&2; false; } + $Q ./jj-fzf --version >/dev/null || { echo "$@: ERROR: failed to start ./jj-fzf as \`bash\` script" >&2; false; } +.PHONY: check-deps +all check: check-deps + +# == doc/jj-fzf.1 == +doc/jj-fzf.1: doc/jj-fzf.1.md jj-fzf Makefile.mk + $(QGEN) + $Q TEMPD="`mktemp -d`" && cd "$$TEMPD" && jj git init 2>/dev/null \ + && $(abspath ./jj-fzf) --help-bindings > $(abspath doc/keys.tmp) \ + && cd / && rm -r -f "$$TEMPD" # jj-fzf needs a .jj repo to run + $Q sed -r $$'/```jj-fzf --help-bindings```/ { r doc/keys.tmp\n d ; }' $< > doc/jj-fzf.1.tmp.md + $Q pandoc $(man/markdown-flavour) -s -p \ + -M date="$(word 2, $(version_full))" \ + -M footer="jj-fzf-$(word 1, $(version_full))" \ + -t man doc/jj-fzf.1.tmp.md -o $@.tmp + $Q rm -f doc/keys.tmp doc/jj-fzf.1.tmp.md && mv $@.tmp $@ +man/markdown-flavour := -f markdown+hard_line_breaks+autolink_bare_uris+emoji+lists_without_preceding_blankline-smart +CLEANFILES += doc/jj-fzf.1 doc/*.tmp* +all: doc/jj-fzf.1 + +# == tests == +tests-basics.sh: + $Q tests/basics.sh +.PHONY: tests-basics.sh + +# == shellcheck == +shellcheck-warning: jj-fzf + $(QGEN) + $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } + shellcheck -W 3 -S warning -e SC2178,SC2207,SC2128 jj-fzf +shellcheck-error: + $(QGEN) + $Q shellcheck --version | grep -q 'script analysis' || { echo "$@: missing GNU shellcheck"; false; } + shellcheck -W 3 -S error jj-fzf +check-gsed: jj-fzf + $(QGEN) + $Q ! grep --color=auto -nE '[^\\]\bsed ' jj-fzf /dev/null \ + || { echo "ERROR: use gsed" >&2 ; false; } + $Q echo ' OK gsed uses' +check-help: + $(QGEN) + $Q ./jj-fzf --help | grep -qF jj-fzf || { echo "$@: ERROR: failed to render \`./jj-fzf --help\`" >&2; false; } +check: check-deps check-gsed check-help shellcheck-error tests-basics.sh + +# == install & uninstall == +install: all + $(QGEN) + mkdir -p $(DESTDIR)$(PRJDIR)/doc $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 + install -c version.sh jj-fzf $(DESTDIR)$(PRJDIR) + @ # Note, .gitattributes:export-subst + git archive + tar are used to hardcode version in $(PRJDIR)/version.sh + test ! -e .gitattributes || git archive HEAD version.sh | tar xC $(DESTDIR)$(PRJDIR) + install -c doc/jj-fzf.1 $(DESTDIR)$(PRJDIR)/doc + ln -sf ../../../$(LIBEXEC)/doc/jj-fzf.1 $(DESTDIR)$(MANDIR)/man1/ + ln -sf ../$(LIBEXEC)/jj-fzf $(DESTDIR)$(BINDIR)/jj-fzf +installcheck: + $(QGEN) + $Q $(DESTDIR)$(BINDIR)/jj-fzf --version >/dev/null || { echo "$@: ERROR: failed to start $(BINDIR)/jj-fzf" >&2; false; } + $Q man $(DESTDIR)$(PRJDIR)/doc/jj-fzf.1 | grep -qF jj-fzf || { echo "$@: ERROR: failed to render $(DESTDIR)$(PRJDIR)/doc/jj-fzf.1" >&2; false; } +uninstall: + $(QGEN) + rm -r -f $(DESTDIR)$(PRJDIR) $(DESTDIR)$(BINDIR)/jj-fzf $(DESTDIR)$(MANDIR)/man1/jj-fzf.1 + +# == distcheck == +distcheck: + @$(eval distversion != git describe --match='v[0-9]*.[0-9]*.[0-9]*' | sed 's/^v//') + @$(eval distname := jj-fzf-$(distversion)) + $(QECHO) MAKE $(distname).tar.zst + $Q test -n "$(distversion)" || { echo -e "#\n# $@: ERROR: no dist version, is git working?\n#" >&2; false; } + $Q git describe --dirty | grep -qve -dirty || echo -e "#\n# $@: WARNING: working tree is dirty\n#" + $Q rm -r -f artifacts/ && mkdir -p artifacts/ + $Q # Generate ChangeLog with ^^-prefixed records. Tab-indent commit bodies, kill whitespaces and multi-newlines + $Q git log --abbrev=13 --date=short --first-parent HEAD \ + --pretty='^^%ad %an # %h%n%n%B%n' > artifacts/ChangeLog \ + && sed 's/^/ /; s/^ ^^// ; s/[[:space:]]\+$$// ' -i artifacts/ChangeLog \ + && sed '/^\s*$$/{ N; /^\s*\n\s*$$/D }' -i artifacts/ChangeLog + $Q # Generate and compress artifacts/jj-fzf-*.tar.zst + $Q git archive --prefix=$(distname)/ --add-file artifacts/ChangeLog -o artifacts/$(distname).tar HEAD + $Q rm -f artifacts/$(distname).tar.zst && zstd --ultra -22 --rm artifacts/$(distname).tar && ls -lh artifacts/$(distname).tar.zst + $Q T=`mktemp -d` && cd $$T && tar xf $(abspath artifacts/$(distname).tar.zst) \ + && cd jj-fzf-$(distversion) \ + && nice make all -j`nproc` \ + && make PREFIX=$$T/inst install \ + && make PREFIX=$$T/inst installcheck -j`nproc` \ + && (set -x && $$T/inst/bin/jj-fzf --version) \ + && make PREFIX=$$T/inst uninstall \ + && (set -x && $$PWD/jj-fzf --version) \ + && cd / && rm -r "$$T" + $Q echo "Archive ready: artifacts/$(distname).tar.zst" | sed '1h; 1s/./=/g; 1p; 1x; $$p; $$x' + +# == clean == +clean: + rm -f $(CLEANFILES) + rm -f -r $(CLEANDIRS) +.PHONY: clean + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/NEWS.md new/jj-fzf-0.32.0/NEWS.md --- old/jj-fzf-0.25.0/NEWS.md 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/NEWS.md 2025-08-14 03:36:35.000000000 +0200 @@ -1,3 +1,41 @@ +## JJ-FZF 0.32.0 - 2025-08-14 + +### Added: +* Ctrl-W: Added way to toggle between various diff formats +* Alt+M: New multi-select mode, use TAB to select multiple commits +* Added multi-mode support for abandon, backout, duplicate, squash, rebase +* Added `make distcheck`, always check in CI +* Added check for jj-fzf --help +* Added installcheck rule +* Added separate manual page +* Added file summary to oplog history +* Added scripts to automate releases +* Added contirb/suspend-with-shell.el to run jj-fzf from emacs, see: + https://testbit.eu/2025/jj-fzf-in-emacs + +### Breaking: +* Depend on jj-0.32.0 +* Changed Alt-N to run new-after with --no-edit +* Preserve PWD in subshells if possible (present) +* Remove unused 'merging' command + +### Changed: +* To install, run `make all install` +* To run all checks, run `make all check install installcheck` +* Builds require GNU Make +* Moved version checks for all tool dependencies into Makefile +* Use /usr/bin/env to find bash +* Undeprecate Alt-S: restore-file from selected revision +* Build man page, use a man page browser for `jj-fzf --help` +* Fetch version information from Git +* Automatically run CI for PRs and tags +* Introduced pandoc dependency for man builds + +### Fixed: +* Fixed installations not working in non-jj repos +* Fixed --version not working outside a jj repo + + ## JJ-FZF 0.25.0 - 2025-01-23 ### Added: @@ -53,6 +91,7 @@ * Tim Janik (@tim-janik) * Douglas Stephen (@dljsjr) + ## JJ-FZF 0.24.0 - 2024-12-12 ### Added: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/README.md new/jj-fzf-0.32.0/README.md --- old/jj-fzf-0.25.0/README.md 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/README.md 2025-08-14 03:36:35.000000000 +0200 @@ -88,6 +88,16 @@ * **jj-undirty.el:** A simple Emacs lisp script that automatically runs `jj status` every time a buffer is saved to snapshot file modifications. `Usage: (load (expand-file-name "~/jj-fzf/contrib/jj-undirty.el"))` + This will install an after-save-hook that calls `jj-undirty` to snapshot the changes in a saved buffer in a jj repository. + +* **suspend-with-shell.el:** A simple Emacs lisp script that allows to suspend Emacs with a custom command. + ``` + Usage: + ;; Suspend emacs with a custom command, without using `ioctl(TIOCSTI)` + (load (expand-file-name "~/jj-fzf/contrib/suspend-with-shell.el")) + ;; Suspend emacs and start jj-fzf on Ctrl+T + (global-set-key (kbd "C-t") (lambda () (interactive) (suspend-with-shell "jj-fzf"))) + ``` <!-- LICENSE --> ## License diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/contrib/suspend-with-shell.el new/jj-fzf-0.32.0/contrib/suspend-with-shell.el --- old/jj-fzf-0.25.0/contrib/suspend-with-shell.el 1970-01-01 01:00:00.000000000 +0100 +++ new/jj-fzf-0.32.0/contrib/suspend-with-shell.el 2025-08-14 03:36:35.000000000 +0200 @@ -0,0 +1,25 @@ +;; This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 + +;; == suspend-with-shell == +;; Wrap `(suspend-emacs)` so that a subshell is executed with $SHELL pointing +;; to a script that will run `COMMAND`. This way, emacs can be suspended +;; to run another terminal process on the same tty, without using +;; `ioctl(TIOCSTI)` - which `(suspend-emacs)` relies on but is not available +;; in recent kernel versions. +(defun suspend-with-shell (COMMAND) + "Call (suspend-emacs) with $SHELL assigned to a script that will run COMMAND" + (interactive) + (let ((oldshell (getenv "SHELL")) + (tfile (make-temp-file "emacssubshell")) + (script (concat "#!/usr/bin/env bash\nset -Eeu #-x\n" COMMAND "\n")) + (cannot-suspend 't)) ; force suspend-emacs to use sys_subshell + (with-temp-file tfile + (insert script)) + (set-file-modes tfile #o700 'nofollow) + ;; see sys_subshell() in https://github.com/emacs-mirror/emacs/blob/master/src/keyboard.c + (setenv "SHELL" tfile) + (suspend-emacs) ; this calls system($SHELL) + (setenv "SHELL" oldshell) + (delete-file tfile) + ) + ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/doc/jj-fzf.1.md new/jj-fzf-0.32.0/doc/jj-fzf.1.md --- old/jj-fzf-0.25.0/doc/jj-fzf.1.md 1970-01-01 01:00:00.000000000 +0100 +++ new/jj-fzf-0.32.0/doc/jj-fzf.1.md 2025-08-14 03:36:35.000000000 +0200 @@ -0,0 +1,114 @@ +% JJ-FZF(1) | jj-fzf Manual Page + +# NAME + jj-fzf - Terminal interface for the `jj` version control system based on fzf + +# SYNOPSIS + **jj-fzf** [*COMMAND*] [*ARGUMENTS*...] + +# DESCRIPTION + + **jj-fzf** is a text-based user interface for the `jj` version control system, + built on top of the fuzzy finder `fzf`. **jj-fzf** centers around the `jj log` + graph view, providing previews of `jj diff` or `jj evolog` for each revision. + Several key bindings are available for actions such as squashing, swapping, + rebasing, splitting, branching, committing, or abandoning revisions. A + separate view for the operations log, `jj op log`, allows fast previews of + diffs and commit histories of past operations and enabling undo of previous + actions. The available hotkeys are displayed on-screen for easy + discoverability. The commands and key bindings can also be displayed with + `jj-fzf --help` and are documented in the **jj-fzf** wiki. + +## JJ LOG VIEW + + The `jj log` view in **jj-fzf** displays a list of revisions with commit + information on each line. Each line contains the following elements: + + `@` + : Marks the working copy + + `○` + : Indicates a mutable commit, a commit that has not yet been pushed + + `◆` + : Indicates an immutable commit, that has been pushed or tagged + + `Change ID` + : The (mostly unique) identifier to track this change across commits + + `Username` + : The abbreviated username of the author + + `Date` + : The day when the commit was authored + + `Commit ID` + : The unique hash for this commit and its meta data + + `Refs` + : Any tags or bookmarks associated with the revisions + + `Message` + : A brief description of the changes made in the revisions + + Note, in `jj`, the set of immutable commits can be configured via + the `revset-aliases."immutable_heads()"` config setting. + +## PREVIEW WINDOW + + The preview window on the right displays detailed information for the + currently selected revisions. The meaning of the preview items are as follows: + + **First Line** + : The `jj log -T builtin_log_oneline` output for the selected commit + + **Change ID** + : The `jj` revision identifier for this revisions + + **Commit ID** + : The unique identifier for the Git commit + + **Refs** + : Tags and bookmarks (similar to branch names) for this revisions + + **Immutable** + : A boolean indication for immutable revisions + + **Parents** + : A list of parent revisions (more than one for merge commits) + + **Author** + : The author of the revision, including name and email, timestamp + + **Committer** + : The committer, including name and email, timestamp + + **Message** + : Detailed message describing the changes made in the revision + + **File List** + : A list of files modified by this revision + + **Diff** + : A `jj diff` view of changes introduced by the revision + +# COMMAND EXECUTION + + For all repository-modifying commands, **jj-fzf** prints the actual `jj` commands + executed to stderr. The output aids users in learning how to use `jj` directly + to achieve the desired effects. It can also be useful when debugging and helps + users determine which actions they might wish to undo. Most commands can also + be run via the command line, using: `jj-fzf <command> <revision>` + +# KEY BINDINGS + + Most **jj-fzf** commands operate on the currently selected revision and + are made available via the following keyboard shortcuts: + + ```jj-fzf --help-bindings``` + +# SEE ALSO + + For screencasts, workflow suggestions or feature requests, visit the + **jj-fzf** project page at: https://github.com/tim-janik/jj-fzf + For revsets, see: https://martinvonz.github.io/jj/latest/revsets diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/jj-fzf new/jj-fzf-0.32.0/jj-fzf --- old/jj-fzf-0.25.0/jj-fzf 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/jj-fzf 2025-08-14 03:36:35.000000000 +0200 @@ -1,10 +1,28 @@ #!/usr/bin/env bash # This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 set -Eeuo pipefail #-x -SCRIPTNAME=`basename $0` && function die { [ -n "$*" ] && echo "$SCRIPTNAME: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } -SELF="$0" +ABSPATHSCRIPT=`readlink -f "$0"` && function die { [ -n "$*" ] && echo "${ABSPATHSCRIPT##*/}: **ERROR**: ${*:-aborting}" >&2; exit 127 ; } -# == only jj-root cannot vanish during checkouts == +# == Execution Trampoline == +# Hook to serve as a single-executable trampoline for `jj show --tool $SELF` +test -z "${JJFZF_EXEC_WITH0_AS_:-}" || { $JJFZF_EXEC_WITH0_AS_ "$@" || : ; exec true ; } # force exit_status=0 +SELF="$0" # Allow to re-execute this script + +# == Early options, outside a jj repo == +COLORALWAYS= +while test $# -ne 0 ; do + case "$1" in \ + --version) echo "${ABSPATHSCRIPT##*/} `${ABSPATHSCRIPT%/*}/version.sh`"; exit ;; + -h|--help) exec man ${ABSPATHSCRIPT%/*}/doc/jj-fzf.1 ;; + --color=always) COLORALWAYS=t ;; + *) break ;; + esac + shift +done +command -v gsed 2>/dev/null 1>&2 || { gsed() { \sed "$@"; } && export -f gsed; } + +# == Subdirs may vanish during checkouts == +JJPWD="$PWD" JJROOT=$(jj --ignore-working-copy root) && cd "$JJROOT" || # always ensure root relative paths die "$PWD: not a JJ repository" @@ -32,6 +50,20 @@ coalesce(description, label(if(empty, "empty"), description_placeholder) ++ "\n")), "\n", )' # extended version of builtin_log_detailed; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml +# Show commit diff according to jj-fzf.diff-mode +jj_show_diff() +{ + local COLOR && [[ " $* " =~ --color=always ]] && COLOR=--color=always || COLOR=--color=never + # Use git-diff (for --git and --word-diff) which has better heuristics for informative hunk headers than jj (0.31.0) + case "$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true)" in + diff-b) export JJFZF_EXEC_WITH0_AS_='git -P diff --no-index --diff-algorithm=histogram -b '"$COLOR" + jj show --no-pager --ignore-working-copy "$@" --tool "$SELF" ;; + word-b) export JJFZF_EXEC_WITH0_AS_='git -P diff --no-index --diff-algorithm=histogram -b --word-diff '"$COLOR" + jj show --no-pager --ignore-working-copy "$@" --tool "$SELF" ;; + *) jj show --no-pager --ignore-working-copy "$@" ;; + esac +} +# handle 'preview' early on to avoid slow previews due to parsing the entire script export REVPAT='^[^a-z()0-9]*([k-xyz]{7,})([?]*)\ ' # line start, ignore --graph, parse revision letters, catch '??'-postfix if test "${1:-}" == preview # preview command, nested invocation then @@ -42,13 +74,12 @@ then # https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id jj --no-pager --ignore-working-copy show -T builtin_log_oneline -r "${BASH_REMATCH[1]}" 2>&1 || : - REVISION=$(echo " $2 " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || exit 0 # find likely commit id - echo - echo + echo && echo fi - { jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$REVISION" - jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} show --color=always -T ' "\n" ' -r "$REVISION" --ignore-space-change - } | head -n 4000 + REVISION=$(echo " $2 " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || exit 0 # find likely commit id + { jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$REVISION" + jj_show_diff --color=always -T '"\n"' -r "$REVISION" + } 2>&1 | head -n 4000 else # no valid revision true fi @@ -60,7 +91,7 @@ preview_oplog) [[ " ${2:-} " =~ $OPRPAT ]] && { jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always op log --no-graph -n 1 -T builtin_op_log_comfortable - jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log -r .. # -T builtin_log_oneline + jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always log -s -r .. # -T builtin_log_oneline } ; exit ;; preview_opshow) [[ " ${2:-} " =~ $OPRPAT ]] && { @@ -83,26 +114,15 @@ } ; exit ;; esac -# == Check Deps == -VERSION=0.25.0 -gawk --version | grep -Fq 'GNU Awk' || die "failed to find 'gawk' in \$PATH (GNU Awk)" -version0d() { gawk 'BEGIN{FPAT="[0-9]+"} {printf("%04d.%04d.%04d.%04d.%04d\n",$1,$2,$3,$4,$5);exit}' <<<" $* "; } -versionge() { test "$(version0d "$2")a" '<' "$(version0d "$1")b"; } -versionge "$(bash --version)" 5.1.16 || die "failed to find 'bash' version 5.1.16 in \$PATH" -[[ `set -o` =~ emacs ]] || die "the 'bash' executable lacks interactive readline support" -versionge "$(jj --version --ignore-working-copy)" 0.25 || die "failed to find 'jj' version 0.25.0 in \$PATH" -versionge "$(fzf --version)" 0.43 || die "failed to find 'fzf' version 0.43.0 in \$PATH" # 0.43.0 supports offset-up -sed --version 2>/dev/null | grep -Fq 'GNU sed' && gsed() { \sed "$@"; } && export -f gsed || - gsed --version | grep -Fq 'GNU sed' || die "failed to find 'gsed' in \$PATH (GNU sed)" -command -v column >/dev/null || column() { cat; } - -# == Early Options == -SHOWHELP= SHOWKEYBINDINGS= COLORALWAYS= ONESHOT=false +# == General Options == +MULTISELECT= HELPKEYBINDINGS= SHOWKEYBINDINGS= COLORALWAYS= ONESHOT=false POSTCMDEXIT= while test $# -ne 0 ; do case "$1" in \ - -h|--help) SHOWHELP=t ;; + --help-bindings) HELPKEYBINDINGS=t ;; + -m) MULTISELECT=-m ;; + +m) MULTISELECT= ;; + --postcmd-exit) POSTCMDEXIT=1 ;; --key-bindings) SHOWKEYBINDINGS=t ;; - --version) echo "$SCRIPTNAME $VERSION"; exit ;; --oneshot) ONESHOT=true ;; # auto-exit after first command --color=always) COLORALWAYS=t ;; *) break ;; @@ -114,10 +134,11 @@ export FZF_DEFAULT_OPTS= # prevent user defaults from messing up the layout declare -A DOC # JJ repository -JJFZFSHOW="jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} show --tool true" +JJFZFSHOW="jj --no-pager --ignore-working-copy show --tool true" JJFZFONELINE="jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline" JJFZFPAGER="less -Rc" -JJSUBSHELL='T=$(tty 2>/dev/null||tty <&1 2>/dev/null||tty <&2 2>/dev/null)&&test -n "$T"&&echo -e "\n#\n# Type \"exit\" to leave subshell\n#" && unset FZF_DEFAULT_COMMAND && exec /usr/bin/env '"$SHELL"' <$T 1>$T 2>$T' +JJSUBSHELL='T=$(tty 2>/dev/null||tty <&1 2>/dev/null||tty <&2 2>/dev/null) && test -n "$T" && echo -e "\n#\n# Type \"exit\" to leave subshell\n#" && + unset FZF_DEFAULT_COMMAND && { test ! -d "'"$JJPWD"'" || cd "'"$JJPWD"'"; } && exec /usr/bin/env '"$SHELL"' <$T 1>$T 2>$T' INFO_BINDING=" fzf </dev/null >/dev/tty 2>&1 --prompt ' ' --disabled --layout=reverse --height 1 --margin 4 --padding 4 --border=block --no-info --no-scrollbar --no-clear --bind=enter:print-query " FUNCTIONS=() FZFSETTINGS=( @@ -259,6 +280,21 @@ xrev_as_commit "$@" ) FUNCTIONS+=( 'xrev_or_commit' ) +# Fill <ARRAY> with <PREFIX> and xrev_or_commit from $@ +parse_xrevs_or_commits() +{ + local -n _parsed_REVS=$1 # nameref to the target array + local _prefix_REV=$2 + shift 2 + test $# -ne 0 && _parsed_REVS=() || _parsed_REVS=($_prefix_REV @) + while test $# -ne 0; do + local _rev + _parsed_rev=$(xrev_or_commit "$1") || die "no such revision: $1" + _parsed_REVS+=($_prefix_REV "$_parsed_rev") + shift + done +} + # Look up full commit hash via JJ commit_id rev_commitid() ( xrev_as_commit "$@" ) # Print first bookmark or the revision itself @@ -279,10 +315,10 @@ jj --no-pager --ignore-working-copy log --no-graph -r "all: $1-" -T 'change_id++"\n"' ) -# List children of a revision -rev_children() +# List commits of a revset +list_revset() ( - jj --no-pager --ignore-working-copy log --no-graph -r "all: $1+" -T 'change_id++"\n"' + jj --no-pager --ignore-working-copy log --no-graph -r "all: $* " -T 'commit_id++"\n"' ) # join_args <joiner> [args...] @@ -451,7 +487,7 @@ local TEMPFILE="$TEMPD/$FILE" cat >"$TEMPFILE" <<<"$_ueovMSG" test -z "$COMMIT" || { - jj diff --ignore-working-copy --no-pager --color=never -r "$COMMIT" | + jj_show_diff --color=never -T '"\n"' -r "$COMMIT" | head -n 4000 > "$TEMPFILE.diff" test -s "$TEMPFILE.diff" && { echo @@ -509,8 +545,10 @@ # == Functions == declare -A KEYBINDINGS +declare -A MULTIKEYBINDINGS FIRSTS="" NEXTS="" +SILENTREFRESH="" # fzflog revset aliases revsets_toml() @@ -534,7 +572,7 @@ if $REVSETNAME ; then echo "$REVSETS_LOG" else - jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} \ + jj --no-pager --ignore-working-copy \ --config-toml "$(revsets_toml)" \ log --color=always -T "$JJ_FZF_ONELINE" -r "$REVSETS_LOG" 2>/dev/null fi @@ -555,16 +593,15 @@ KEYBINDINGS["Ctrl-R"]="revset-filter" # overridden below # Abandon Revision -DOC['abandon']='Use `jj abandon` to remove the currently selected revision (or divergent commit) from the history.' +DOC['abandon']='Use `jj abandon` to remove the currently selected revisions (or divergent commit) from the history.' abandon() ( - R="$(xrev_or_commit "${1:-@}")" || - die "no such revision" + local -a REVS && parse_xrevs_or_commits REVS '-r' "$@" ( set -x - jj abandon -r "$R" ) || + jj abandon "${REVS[@]}" ) || sleep 1 ) -KEYBINDINGS["Alt-A"]="abandon" +MULTIKEYBINDINGS["Alt-A"]="abandon" # Bookmark Creation DOC['bookmark']='Use `jj bookmark {create|set -B}` to (re-)assign a bookmark name to the currently selected revision (or divergent commit).' @@ -776,28 +813,15 @@ KEYBINDINGS["Ctrl-I"]="diff" # Backout Commit -DOC['backout']='Use `jj backout` to create a new commit that undoes the changes made by the currently selected revision and apply the changes on top of the working-copy.' +DOC['backout']='Use `jj backout` to create new commits that undo the changes made by the currently selected revisions and apply the changes on top of the working-copy.' backout() ( - R="$(xrev "${1:-@}")" - # use working copy as destination, unless it is empty - test "$(rev_edpstate @)" == empty-silent-p1 && - D=@- || - D=@ - # record base commit children before/after, then backout - A=( $(rev_children "$D") ) + local -a REVS && parse_xrevs_or_commits REVS '-r' "$@" ( set -x - jj backout -r "$R" -d "$D" - ) || die - B=( $(rev_children "$D") ) - C=() && diff_arrays A B C - [ ${#C[@]} -eq 1 ] || - die "failed to find newly created backout revision" - ( set -x - jj edit "${C[0]}" + jj backout -d @ "${REVS[@]}" ) || die ) -KEYBINDINGS["Alt-K"]="backout" FIRSTS="$FIRSTS backout" +MULTIKEYBINDINGS["Alt-K"]="backout" FIRSTS="$FIRSTS backout" # Line Blame: jj-fzf +<line> <gitfile> if [[ $# == 2 ]] && [[ "${1:0:1}" == + ]] ; then @@ -825,142 +849,37 @@ exit 0 fi -# Merge into tracked bookmark -DOC['merging']='Start a dialog to select parents for a new merge commit, using `jj new REVISIONS...`. Possibly rebase the working copy after merge commit creation.' -merging() +# Start multi-select mode +DOC["multi-select"]='Select multiple revisions to operate on.' +multi-select() ( - P="$(xrev "${1:-@}")" - temp_dir - # Find tracked upstream revision - for ups in $(jj --no-pager --ignore-working-copy log --no-graph -r 'trunk()' -T 'bookmarks') ; do - [[ $ups =~ ^(master|main|trunk)(@.*)$ ]] && { UPSTREAM="${BASH_REMATCH[1]}" && break ; } - [[ $ups =~ ^([^@\ :]+).* ]] && UPSTREAM="${BASH_REMATCH[1]}" - done && echo $UPSTREAM - WCA="$(jj log --ignore-working-copy --no-pager --no-graph -r "::@- & $P" -T change_id)" # is $P working copy ancestor? - test -z "$WCA" && WCA=0 || WCA=1 - echo $WCA > $TEMPD/wcrebase.toggle - echo 0 > $TEMPD/upstream.toggle - export JJFZFONELINE REVPAT TEMPD UPSTREAM P - # Parse jj log lines into merging.revs - merging_revs() - ( - declare -A counts_ - echo -n > $TEMPD/merging.revs - test "$(cat $TEMPD/upstream.toggle)" -eq 1 -a -n "$UPSTREAM" && - R=$(jj --no-pager --ignore-working-copy show --tool true -r "$UPSTREAM" -T 'change_id') && - test -n "$R" && { - echo "$UPSTREAM" >> $TEMPD/merging.revs - counts_["$R"]=1 # use change_id for deduplication - } - INPUTLINES=("$@") && REVERSED=() && reverse_array INPUTLINES REVERSED - for ARG in ". $P " "${REVERSED[@]}" ; do - [[ "$ARG" =~ $REVPAT ]] || continue - R=$(jj --no-pager --ignore-working-copy show --tool true -r "${BASH_REMATCH[1]}" -T 'change_id') - test -n "$R" && test -z "${counts_[$R]:-}" || continue - echo "$R" >> $TEMPD/merging.revs - counts_["$R"]=1 - done - ) - # Preview merge command for merging.revs - merging_preview() - ( - mapfile -t REVS < $TEMPD/merging.revs - test "$(< $TEMPD/wcrebase.toggle)" -eq 1 && NOEDIT=--no-edit || NOEDIT= - echo && echo jj "new $NOEDIT" "${REVS[@]}" - test "$(< $TEMPD/wcrebase.toggle)" -eq 1 && echo jj rebase -b @ -d "MERGE-OF-${REVS[0]:0:7}…" - echo - test "$(< $TEMPD/upstream.toggle)" -eq 1 && echo "Upstream: $UPSTREAM" - echo 'Parents:' - while read R ; do - $JJFZFONELINE -r "$R" - done < $TEMPD/merging.revs - ) - # Provide functions for FZF - export -f merging_revs merging_preview reverse_array - # FZF popup to select parent list - H=$'\n' - H="$H"$'Alt-R: Toggle rebasing the working copy after merge creation\n' - H="$H"$'Alt-U: Toggle merging into Upstream bookmark\n' - export FZF_DEFAULT_COMMAND="$SELF fzflog" - "${FZFPOPUP[@]}" \ - --preview "merging_revs {+} && merging_preview" \ - --border-label '-[ MERGING ]-' --color=border:bright-blue,label:bright-blue \ - --prompt "Merge +> " \ - --header "$H" --header-first \ - --bind "alt-r:execute-silent( gsed 's/0/2/;s/1/0/;s/2/1/' -i $TEMPD/wcrebase.toggle )+refresh-preview" \ - --bind "alt-u:execute-silent( gsed 's/0/2/;s/1/0/;s/2/1/' -i $TEMPD/upstream.toggle )+refresh-preview" \ - -m --color=pointer:grey \ - --no-tac --no-sort > $TEMPD/selections.txt && - mapfile -t selections < $TEMPD/selections.txt && - merging_revs "${selections[@]}" && - mapfile -t REVS < $TEMPD/merging.revs && - test "${#REVS[@]}" -ge 2 || - exit # Merge cancelled - # Create merge message - JJNEW_ARGS=( $(forward_chronologic "${REVS[@]}") ) - test "${#REVS[@]}" -ge 2 && { - MSG=$( echo_commit_msg --merge "${REVS[@]}" ) - # edit merge msg - O="$MSG" - user_editor_on_var "MERGE-MSG.txt" MSG && - test "$O" != "$MSG" || - ERROR "Merge commit cancelled by user" - JJNEW_ARGS+=(--message="$MSG") - } - test "$(< $TEMPD/wcrebase.toggle)" -eq 1 && NOEDIT=--no-edit || NOEDIT= - # Merge revisions - A=( $(rev_children "${JJNEW_ARGS[0]}") ) # record parent0 children, *before* - ( set -x - jj new $NOEDIT "${JJNEW_ARGS[@]}" - ) || ERROR - B=( $(rev_children "${JJNEW_ARGS[0]}") ) # record parent0 children, *after* - C=() && diff_arrays A B C # detect new commit - [ ${#C[@]} -eq 1 ] || die "failed to find newly created revision" - RM="${C[0]}" # new merge commit - test "$(< $TEMPD/upstream.toggle)" -ne 1 || - ( set -x - jj bookmark set -r "$RM" -B "$UPSTREAM" - ) || ERROR - test "$(< $TEMPD/wcrebase.toggle)" -ne 1 || - ( set -x - jj rebase -b @ -d "$RM" - ) || ERROR + $SELF -m --postcmd-exit || : ) -KEYBINDINGS["Alt-M"]="merging" FIRSTS="$FIRSTS merging" +KEYBINDINGS["Alt-M"]="multi-select" # New --insert-before -DOC['new-before']='Use `jj new --insert-before` to create and insert a new revision before the currently selected revision (or divergent commit). Creates a new branch for merge commits.' +DOC['new-before']='Use `jj new --no-edit --insert-before` to create and insert a new revision before the currently selected revision (or divergent commit).' new-before() ( R="$(xrev_or_commit "${1:-@}")" || die "no such revision" - if test "$($JJFZFSHOW -r "$R" -T '"p" ++ self.parents().len() ++ "\n"')" == p1 ; then - ( set -x - jj new --insert-before "$R" - ) || ERROR - else # merge commit - PARENTS=( $(jj --no-pager --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r all:"$R-") ) - MERGE_BASE=$(git merge-base --octopus "${PARENTS[@]}") - D_PARENTS=( -d $(join_args ' -d ' "${PARENTS[@]}") ) - ( set -x - jj new -r $MERGE_BASE - jj rebase -s $R "${D_PARENTS[@]}" -d @ - ) || ERROR - fi + ( set -x + jj new --no-edit --insert-before "$R" + ) || ERROR ) -KEYBINDINGS["Alt-N"]="new-before" +KEYBINDINGS["Ctrl-Alt-N"]="new-before" # New --insert-after -DOC['new-after']='EXPERIMENTAL: Use `jj new --insert-after` to create and insert a new revision after the currently selected revision (or divergent commit).' +DOC['new-after']='Use `jj new --no-edit --insert-after` to create and insert a new revision after the currently selected revision (or divergent commit).' new-after() ( R="$(xrev_or_commit "${1:-@}")" || die "no such revision" ( set -x - jj new --insert-after "$R" + jj new --no-edit --insert-after "$R" ) || ERROR ) -KEYBINDINGS["Ctrl-Alt-N"]="new-after" +KEYBINDINGS["Alt-N"]="new-after" # New DOC['new']='Use `jj new` to create a new revision on top of the currently selected revision (or divergent commit).' @@ -1021,7 +940,7 @@ H="$H"$'Alt-Y: Undo/redo the selected operation entry\n' H="$H"$'Alt-Z: Undo the next operation (not already marked `⋯`)\n' echo 'VIEW=preview_oppatch' >> $TEMPD/oplog.env - export FZF_DEFAULT_COMMAND="$SELF op_log_oneline" + export FZF_DEFAULT_COMMAND="$SELF op_log_oneline" TEMPD SELF RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")' "${FZFPOPUP[@]}" \ --border-label '-[ OP-LOG ]-' --color=border:bright-yellow,label:bright-yellow \ @@ -1037,9 +956,9 @@ --bind "alt-w:execute( $SELF restore-commit {} )+abort" \ --bind "alt-y:execute( $SELF undo-op {} )+$RELOAD" \ --bind "alt-z:execute( $SELF undo )+$RELOAD" \ - --bind "enter:execute( [[ {} =~ \$OPPAT ]] || exit && export JJFZF_ATOP=\"\${BASH_REMATCH[1]}\" && $SELF logrev @ {q} )" \ + --bind 'enter:execute( [[ {} =~ $OPPAT ]] || exit && COMMIT=$(jj log --no-pager --no-graph -T commit_id -r @ --at-operation "${BASH_REMATCH[1]}") && $SELF logrev "$COMMIT" )' \ --preview-window 'nowrap,right,border-left' \ - --preview "[[ {} =~ $OPPAT ]] || exit; export JJFZF_ATOP=\"\${BASH_REMATCH[1]}\" && . $TEMPD/oplog.env && $SELF \$VIEW {}" \ + --preview '[[ {} =~ $OPPAT ]] || exit; . $TEMPD/oplog.env && $SELF $VIEW {}' \ --no-tac --no-sort +m # TODO: remove alt-w in jj-fzf-0.26 ) @@ -1186,12 +1105,17 @@ KEYBINDINGS["Alt-O"]="absorb" # Squash Into Parent -DOC['squash-into-parent']='Use `jj squash` to move the changes from the currently selected revision (or divergent commit) into its parent.' +DOC['squash-into-parent']='Use `jj squash` to move the changes from the currently selected revision(s) (or divergent commit) into its parent.' squash-into-parent() ( - R="$(xrev_or_commit "${1:-@}")" + local -a REVS && parse_xrevs_or_commits REVS '' "$@" W="$(xrev_or_commit "@")" - if test "$W" == "$R" ; then + PARENTS="roots( none() $(printf '|%s-::' "${REVS[@]}") )" + PARENTS=( $(list_revset " $PARENTS ") ) + test ${#PARENTS[@]} -eq 1 || + ERROR "revisions lack common parent" + if [[ " ${REVS[*]} " == *" $W "* ]]; then + # The working copy @ is to be squashed. # Squashing without --keep-emptied would start a new branch at @- which is # undesired if @+ exists. But using --keep-emptied does not squash the # message. As a workaround, create a new @+, so we never squash directly @@ -1199,15 +1123,13 @@ # squashed working copy. ( set -x jj new --insert-after @ - jj squash --from "$W" --into "$W-" - ) || ERROR - else - ( set -x - jj squash -r "$R" # --use-destination-message ) || ERROR fi + ( set -x + jj squash --from "$(IFS='|' && echo "${REVS[*]}")" --into "${PARENTS[0]}" + ) || ERROR ) -KEYBINDINGS["Alt-Q"]="squash-into-parent" +MULTIKEYBINDINGS["Alt-Q"]="squash-into-parent" # Squash @ Commit DOC['squash-@-into']='Use `jj squash` to move the changes from the working copy into the currently selected revision.' @@ -1311,29 +1233,38 @@ KEYBINDINGS["Alt-P"]="reparenting" FIRSTS="$FIRSTS reparenting" # Rebase Branch/Source/Revision After/Before/Destination -DOC['rebase']='Start a dialog to configure the use of `jj rebase` to rebase a branch, source, or revision onto, before or after another revision. Also supports `jj duplicate` on the source revision before rebasing and `jj simplify-parents` afterwards.' +DOC['rebase']='Start a dialog to `jj rebase` or `jj duplicate` a set of revisions (or branch) possibly with descendants, onto, before or after another revision. Also supports `jj simplify-parents` afterwards.' rebase() ( - S="$(xrev "${1:-@}")" + local -a RBREVS && parse_xrevs_or_commits RBREVS '' "$@" temp_dir + CREVS="${RBREVS[*]}" && CREVS="${CREVS// /|}" # combined revisions echo > $TEMPD/rebase.env echo 'DP=' >> $TEMPD/rebase.env - echo 'FR=--branch' >> $TEMPD/rebase.env + echo 'CH=' >> $TEMPD/rebase.env echo 'TO=--destination' >> $TEMPD/rebase.env echo 'SP=false' >> $TEMPD/rebase.env echo 'II=' >> $TEMPD/rebase.env + if test ${#RBREVS[@]} -le 1 ; then + echo 'FR=--source' >> $TEMPD/rebase.env + else + echo 'FR=--revisions' >> $TEMPD/rebase.env + CREVS="($CREVS)" # parenthesis around multiple revisions + fi + QREVS="'$CREVS'" # quoted revisions export JJFZFONELINE PREVIEW=". $TEMPD/rebase.env" PREVIEW="$PREVIEW"' && echo' - PREVIEW="$PREVIEW"' && { test -z "$DP" || echo jj duplicate '$S' || :; }' - PREVIEW="$PREVIEW"' && echo jj rebase $II $FR ${DP:+DUPLICATE-OF-}'${S:0:13}' $TO $REV' - PREVIEW="$PREVIEW"' && { $SP && echo jj simplify-parents --revisions '$S' || :; } && echo' - PREVIEW="$PREVIEW"' && F=${FR#--} && echo ${F^^}: && $JJFZFONELINE -r '$S' && echo' - PREVIEW="$PREVIEW"' && T=${TO#--} && echo ${T^^}: && $JJFZFONELINE -r $REV && echo' - PREVIEW="$PREVIEW"' && echo COMMON: && $JJFZFONELINE -r "heads( ::'$S' & ::$REV)"' + PREVIEW="$PREVIEW"' && { test -z "$DP" || echo jj duplicate -r '"$QREVS"'$CH $TO $TARGET ; }' + PREVIEW="$PREVIEW"' && { test -n "$DP" || echo jj rebase $II $FR '"$QREVS"' $TO $TARGET ; }' + PREVIEW="$PREVIEW"' && { test $SP == true && echo && echo jj simplify-parents -r '"$QREVS"' || : ; } && echo' + PREVIEW="$PREVIEW"' && F=${FR#--} && echo ${F^^}: && $JJFZFONELINE -r '"$QREVS"' && echo' + PREVIEW="$PREVIEW"' && T=${TO#--} && echo ${T^^}: && $JJFZFONELINE -r '"$QREVS"' && echo' + PREVIEW="$PREVIEW"' && echo COMMON: && $JJFZFONELINE -r "heads( ::('"$CREVS"') & ::$TARGET)"' H='' H="$H""Alt-B: BRANCH - Rebase the whole branch relative to destination's ancestors"$'\n' - H="$H""Alt-D: DUPLICATE - duplicate the specified revision/descendants before rebase"$'\n' + H="$H""Alt-C: DUP-DESCENDANTS - duplicate the specified revisions and descendants"$'\n' + H="$H""Alt-D: DUPLICATE - duplicate the specified revisions"$'\n' H="$H"'Alt-I: IGNORE-IMMUTABLE - Use `jj rebase --ignore-immutable` command'$'\n' H="$H"'Alt-P: SIMPLIFY-PARENTS - Use `jj simplify-parents` after rebasing'$'\n' H="$H""Alt-R: REVISION - Rebase only given revision, moves descendants onto parent"$'\n' @@ -1342,66 +1273,72 @@ H="$H""Ctrl-B: BEFORE - The revision to insert before"$'\n' H="$H""Ctrl-D: DESTINATION - The revision to rebase onto"$'\n' export FZF_DEFAULT_COMMAND="$SELF fzflog" - REV=$("${FZFPOPUP[@]}" \ - --border-label '-[ REBASE ]-' --color=border:green,label:green \ - --preview "[[ {} =~ $REVPAT ]] || exit; export REV=\"\${BASH_REMATCH[1]}\"; $PREVIEW " \ - --prompt "Rebase > " \ - --header "$H" --header-first \ - --bind "alt-d:execute-silent( gsed 's/^DP=..*/DP=x/; s/^DP=$/DP=1/; s/^DP=x.*/DP=/; s/^FR=--branch/FR=--source/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "alt-b:execute-silent( gsed 's/^FR=.*/FR=--branch/; s/^DP=.*/DP=/;' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "alt-s:execute-silent( gsed 's/^FR=.*/FR=--source/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "alt-r:execute-silent( gsed 's/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "alt-p:execute-silent( gsed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "alt-i:execute-silent( gsed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "ctrl-d:execute-silent( gsed 's/^TO=.*/TO=--destination/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "ctrl-a:execute-silent( gsed 's/^TO=.*/TO=--insert-after/' -i $TEMPD/rebase.env )+refresh-preview" \ - --bind "ctrl-b:execute-silent( gsed 's/^TO=.*/TO=--insert-before/' -i $TEMPD/rebase.env )+refresh-preview" \ - --no-tac --no-sort +m ) - [[ "$REV" =~ $REVPAT ]] && - REV="${BASH_REMATCH[1]}" || + TARGET=$("${FZFPOPUP[@]}" \ + --border-label '-[ REBASE ]-' --color=border:green,label:green \ + --preview "[[ {} =~ $REVPAT ]] || exit; export TARGET=\"\${BASH_REMATCH[1]}\"; $PREVIEW " \ + --prompt "Rebase > " \ + --header "$H" --header-first \ + --bind "alt-d:execute-silent( gsed 's/^CH=.*/CH=/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-c:execute-silent( gsed 's/^CH=.*/CH=::/; s/^DP=.*/DP=1/; s/^FR=.*/FR=--source/ ' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-b:execute-silent( gsed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--branch/ ' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-s:execute-silent( gsed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--source/ ' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-r:execute-silent( gsed 's/^CH=.*/CH=/; s/^DP=.*/DP=/; s/^FR=.*/FR=--revisions/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-p:execute-silent( gsed 's/^SP=false/SP=x/; s/^SP=true/SP=false/; s/^SP=x/SP=true/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "alt-i:execute-silent( gsed 's/^II=-.*/II=x/; s/^II=$/II=--ignore-immutable/; s/^II=x.*/II=/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "ctrl-d:execute-silent( gsed 's/^TO=.*/TO=--destination/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "ctrl-a:execute-silent( gsed 's/^TO=.*/TO=--insert-after/' -i $TEMPD/rebase.env )+refresh-preview" \ + --bind "ctrl-b:execute-silent( gsed 's/^TO=.*/TO=--insert-before/' -i $TEMPD/rebase.env )+refresh-preview" \ + --no-tac --no-sort +m ) + [[ "$TARGET" =~ $REVPAT ]] && + TARGET="${BASH_REMATCH[1]}" || exit 0 - REV="$(xrev "$REV")" + TARGET="$(xrev_or_commit "$TARGET")" source $TEMPD/rebase.env rm -f TEMPD/rebase.env - # duplicate input revision - test -z "$DP" || { - test "$FR" == --source && DESCENDANTS=:: || DESCENDANTS= - A=( $(rev_children "$S-") ) C=() + # duplicate revisions + if test -n "$DP" ; then ( set -x - jj duplicate "$S"$DESCENDANTS ) || ERROR - B=( $(rev_children "$S-") ) && diff_arrays A B C # find duplicated revision - [ ${#C[@]} -eq 1 ] || ERROR "failed to find newly created revision duplicate" - S="${C[0]}" - } - # rebase revision - ( set -x - jj rebase $II $FR "$S" $TO "$REV" - ) || ERROR + jj duplicate -r "$CREVS$CH" $TO "$TARGET" + ) || ERROR + else # rebase revisions + ( set -x + jj rebase $II $FR all:"$CREVS" $TO "$TARGET" + ) || ERROR + fi # simplify-parents - ! $SP || ( - set -x - jj simplify-parents --revisions "$S" - ) || ERROR + ! $SP || + ( set -x + jj simplify-parents -r "$CREVS" + ) || ERROR ) -KEYBINDINGS["Alt-R"]="rebase" FIRSTS="$FIRSTS rebase" +MULTIKEYBINDINGS["Alt-R"]="rebase" FIRSTS="$FIRSTS rebase" # Restore File -DOC['restore-file']='DEPRECATED: Start a dialog to select a file from the currently selected revision and use `jj restore` to restore the file into the working copy.' +DOC['restore-file']='Start a dialog to select a file to `jj restore` from the currently selected revision into the working copy.' restore-file() ( R="$(xrev "${1:-@}")" - MODE_FILE=$(jj show --tool true -T '' -s -r "$R" | - "${FZFPOPUP[@]}" \ - --border-label '-[ RESTORE-FILE ]-' --color=border:blue,label:blue \ - --preview 'read M F <<<{} && test -n \"$F\" || exit; jj --no-pager --ignore-working-copy log --color=always -s --patch -T builtin_log_oneline -r "'"$R"'" -- "$F"' \ - --header "Restore File into @" \ - ) - read M F <<<"$MODE_FILE" - test -n "$M" -a -n "$F" || return - ( set -x - jj restore --from "$R" -- "$F" - ) || - sleep 1 + temp_dir + # find differing fileset + mapfile -t MAPFILE < <(jj --no-pager --ignore-working-copy diff --name-only --from @ --to "$R") + [[ ${#MAPFILE[@]} -ge 1 ]] || + return + # file picker + PREVIEW='read F <<<{} && test -n \"$F\" || exit ' + PREVIEW+='&& jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r "'"$R|@"'" && echo ' + PREVIEW+='&& jj --no-pager --ignore-working-copy diff --color=always -s --git --from @ --to "'"$R"'" -- cwd-file:"$F" ' + printf '%s\n' "${MAPFILE[@]}" | + "${FZFPOPUP[@]}" \ + --border-label '-[ RESTORE-FILE ]-' --color=border:blue,label:blue \ + --preview "$PREVIEW" \ + --header "Restore File from $R into @" \ + > $TEMPD/restore-file.sel + # restore file if any + FILENAME="$(<$TEMPD/restore-file.sel)" + test -z "$FILENAME" || ( + set -x + jj restore --from "$R" -- cwd-file:"$FILENAME" + ) || ERROR ) KEYBINDINGS["Alt-S"]="restore-file" @@ -1426,10 +1363,10 @@ # Log single change logrev() ( - R="$(xrev_or_commit "${1:-@}")" + R="$(xrev_as_commit "${1:-@}")" ( - jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$R" - jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} show --color=always -T ' "\n" ' -r "$R" + jj --no-pager --ignore-working-copy log --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -s -r "$R" + jj_show_diff --color=always -T '"\n"' -r "$R" ) | $JJFZFPAGER ) FUNCTIONS+=( 'logrev' ) @@ -1482,6 +1419,21 @@ ) KEYBINDINGS["Ctrl-V"]="gitk" +# Toggle between various diff formats +DOC['word-diff-toggle']='Toggle/cycle between various diff formats, including JJ, Git, word-diff, with and without whitespace.' +word-diff-toggle() +( + currentmode=$(jj --no-pager --ignore-working-copy config get jj-fzf.diff-mode 2>/dev/null || true) + diffmodes=("jj-diff" "diff-b" "word-b") + index=0 + for i in "${!diffmodes[@]}"; do + [[ "${diffmodes[$i]}" == "$currentmode" ]] && { index=$i && break; } + done + next_index=$(( (index + 1) % ${#diffmodes[@]} )) + jj --no-pager --ignore-working-copy config set --repo jj-fzf.diff-mode "${diffmodes[$next_index]}" +) +KEYBINDINGS["Ctrl-W"]="word-diff-toggle" SILENTREFRESH="$SILENTREFRESH word-diff-toggle" + # Edit (New) Working Copy 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.' edit() @@ -1529,7 +1481,7 @@ # Known cases where the above multi-step undo logic breaks: # * Undo of an operation like "reconcile divergent operations" just gives "Error: Cannot undo a merge operation" ) -KEYBINDINGS["Alt-Z"]="undo" +MULTIKEYBINDINGS["Alt-Z"]="undo" # Reset undo memory undo-reset() @@ -1567,80 +1519,18 @@ tr \\r \\n ) -# Help text -HELP_INTRO="# JJ-FZF ($VERSION)"' - - **jj-fzf** is a text-based user interface for the `jj` version control system, - built on top of the fuzzy finder `fzf`. **jj-fzf** centers around the `jj log` - graph view, providing previews of `jj diff` or `jj evolog` for each revision. - Several key bindings are available for actions such as squashing, swapping, - rebasing, splitting, branching, committing, or abandoning revisions. A - separate view for the operations log, `jj op log`, allows fast previews of - diffs and commit histories of past operations and enabling undo of previous - actions. The available hotkeys are displayed on-screen for easy - discoverability. The commands and key bindings can also be displayed with - `jj-fzf --help` and are documented in the **jj-fzf** wiki. - -## JJ LOG VIEW - - The `jj log` view in **jj-fzf** displays a list of revisions with commit - information on each line. Each line contains the following elements: - - **Graph Characters**: - **@**: Marks the working copy - **○**: Indicates a mutable commit, a commit that has not been pushed to a - remote yet - **◆**: Indicates an immutable commit, that has been pushed to a remote or occurs - in the ancestry of a tag. In `jj`, the set of immutable commits can be - configured via the `revset-aliases."immutable_heads()"` config - **Change ID**: The (mostly unique) identifier to track this change across commits - **Username**: The abbreviated username of the author - **Date**: The day when the commit was authored - **Commit ID**: The unique hash for this commit and its meta data - **Refs**: Any tags or bookmarks associated with the revisions - **Message**: A brief description of the changes made in the revisions - -## PREVIEW WINDOW - - The preview window on the right displays detailed information for the - currently selected revisions. The meaning of the preview items are as follows: - - **First Line**: The `jj log -T builtin_log_oneline` output for the selected commit - **Change ID**: The `jj` revision identifier for this revisions - **Commit ID**: The unique identifier for the Git commit - **Refs**: Tags and bookmarks (similar to branch names) for this revisions - **Immutable**: A boolean indication for immutable revisions - **Parents**: A list of parent revisions (more than one for merge commits) - **Author**: The author of the revision, including name and email, timestamp - **Committer**: The committer, including name and email, timestamp - **Message**: Detailed message describing the changes made in the revision - **File List**: A list of files modified by this revision - **Diff**: A `jj diff` view of changes introduced by the revision - -## COMMAND EXECUTION - - For all repository-modifying commands, **jj-fzf** prints the actual `jj` commands - executed to stderr. This output aids users in learning how to use `jj` directly - to achieve the desired effects, can be useful when debugging and helps users - determine which actions they might wish to undo. Most commands can also be run - via the command line, using: `jj-fzf <command> <revision>` - -## KEY BINDINGS - - Most **jj-fzf** commands operate on the currently selected revision and - are made available via the following keyboard shortcuts: -' -HELP_OUTRO=' -## SEE ALSO - - For screencasts, workflow suggestions or feature requests, visit the - **jj-fzf** project page at: https://github.com/tim-janik/jj-fzf - For revsets, see: https://martinvonz.github.io/jj/latest/revsets -' +# == MULTISELECT == +if test -n "$MULTISELECT" ; then + KEYBINDINGS=$(declare -p MULTIKEYBINDINGS) && declare -A KEYBINDINGS="${KEYBINDINGS#*=}" # copy MULTIKEYBINDINGS -> KEYBINDINGS +else + for key in "${!MULTIKEYBINDINGS[@]}"; do # merge MULTIKEYBINDINGS -> KEYBINDINGS + KEYBINDINGS["$key"]="${MULTIKEYBINDINGS[$key]}" + done +fi # == --help == HELPKEYS=$(declare -p KEYBINDINGS) && declare -A HELPKEYS="${HELPKEYS#*=}" # copy KEYBINDINGS -> HELPKEYS -if test -n "$SHOWHELP" ; then +if test -n "$HELPKEYBINDINGS" ; then # Key bdingins only shown in long form help HELPKEYS[Shift-↑]='preview-up' HELPKEYS[Ctrl-↑]='preview-up' @@ -1652,23 +1542,26 @@ DOC['clear-filter']='Discard the current *fzf* query string.' HELPKEYS[Alt-H]='toggle-show-keys' DOC['toggle-show-keys']='Display or hide the list of avilable key bindings, persist the setting in `jj-fzf.show-keys` of the `jj` user config.' +else + test -n "$MULTISELECT" && { + HELPKEYS[Alt-M]='multi-select-exit' + HELPKEYS[TAB]='toggle-select' + } fi DISPLAYKEYS="${!HELPKEYS[@]}" DISPLAYKEYS=$(sort <<<"${DISPLAYKEYS// /$'\n'}" | grep -vF 'Ctrl-Alt-') -if test -n "$SHOWHELP" ; then +if test -n "$HELPKEYBINDINGS" ; then tty -s <&1 && COLOR=true || { COLOR=false; JJFZFPAGER=cat; } test -z "$COLORALWAYS" || COLOR=true ( : - echo -n "$HELP_INTRO" for k in $DISPLAYKEYS ; do NAME="${HELPKEYS[$k]}" - echo && echo "**$k:** _$NAME""_" + echo && echo "## $k: _$NAME""_" D="${DOC[$NAME]:-}" test -z "$D" || echo "$D" | fold -s -w78 | gsed 's/^/ /' done - echo "$HELP_OUTRO" - ) | sedmarkdown | $JJFZFPAGER + ) exit 0 fi @@ -1751,8 +1644,12 @@ fun="${KEYBINDINGS[$k]}" postcmd="" [[ " $FIRSTS " == *" $fun "* ]] && postcmd="+first" + [[ " $SILENTREFRESH " == *" $fun "* ]] && EXECUTE=execute-silent || EXECUTE=execute [[ " $NEXTS " == *" $fun "* ]] && postcmd="+down" - BIND+=( --bind "${k,,}:execute( $SELF $fun {} {q} )$EXECKILLME+$RELOAD$postcmd" ) + [[ " $SILENTREFRESH " == *" $fun "* ]] && REPAINT=refresh-preview || REPAINT="$RELOAD" + [[ -v MULTIKEYBINDINGS["$k"] ]] && FUNARGS='{+}' || FUNARGS='{} {q}' + [[ -n "$POSTCMDEXIT" ]] && postcmd=+abort + BIND+=( --bind "${k,,}:$EXECUTE( $SELF $MULTISELECT $fun $FUNARGS )$EXECKILLME+$REPAINT$postcmd" ) done # == FZF == @@ -1762,15 +1659,17 @@ "${FZFSETTINGS[@]}" "${FZFEXTRAS[@]}" \ --bind "ctrl-u:clear-query+clear-selection+clear-screen" \ --bind "ctrl-z:execute( $JJSUBSHELL )+execute-silent( jj --no-pager status )+$RELOAD" \ + --bind "alt-m:abort" \ --bind "f5:$RELOAD" \ --bind "enter:execute( $SELF logrev {} {q} )$EXECKILLME+$RELOAD" \ "${BIND[@]}" \ --bind "ctrl-r:transform-query( $SELF revset-filter {q} )+become( exec $SELF )" \ --preview " exec $SELF preview {} {q} " \ --header "$(list_key_bindings)" --header-first \ - --bind "alt-h:transform-header:$SELF --key-bindings --key-toggle" \ - --prompt " $(fzflog --revsetname) > " \ - --no-tac --no-sort +m + --bind "alt-h:transform-header:$SELF $MULTISELECT --key-bindings --key-toggle" \ + --prompt " $(fzflog --revsetname) ${MULTISELECT/-m/(MULTI)}> " \ + --no-tac --no-sort +m $MULTISELECT + # Notes: # * Do not use 'exec' as last command, otherwise trap-handlers are skipped. # * Ctrl-R: This must be rebound to run transform-query, ideally we would just transform-query+transform-prompt+reload diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/tests/utils.sh new/jj-fzf-0.32.0/tests/utils.sh --- old/jj-fzf-0.25.0/tests/utils.sh 2025-01-23 03:33:26.000000000 +0100 +++ new/jj-fzf-0.32.0/tests/utils.sh 2025-08-14 03:36:35.000000000 +0200 @@ -3,7 +3,7 @@ # == Check Dependencies == jj --help >/dev/null PATH="$SCRIPTDIR/..:$PATH" # ensure jj-fzf is in $PATH -jj-fzf --help >/dev/null +jj-fzf --version >/dev/null # == VARIABLE Setup == export JJ_FZF_ERROR_DELAY=0 # instant errors for testing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jj-fzf-0.25.0/version.sh new/jj-fzf-0.32.0/version.sh --- old/jj-fzf-0.25.0/version.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/jj-fzf-0.32.0/version.sh 2025-08-14 03:36:35.000000000 +0200 @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0 +set -Eeuo pipefail + +# Commit information provided by git-archive in export-subst format string, see gitattributes(5) +read VDESCRIBE VDATE <<<' $Format: %(describe:match=v[0-9]*.[0-9]*.[0-9]*) %ci $ ' + +# Use baked-in version info if present +! [[ "$VDATE" =~ % ]] && + echo "${VDESCRIBE#v} $VDATE" && exit + +# Use version info from git repository, needs non-shallow clones +# Prefer exact tags (even if light, like nightly) over annotated tags +cd $(dirname $(readlink -f "$0")) +VDESCRIBE=$(git describe --exact-match --tags --match='v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || + git describe --match='v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null) && + echo "${VDESCRIBE#v} `git -P log -1 --pretty=%ci`" && exit + +# Fallback, unversioned +echo "0.0.0-unversioned0 2001-01-01 01:01:01 +0000" ++++++ jj-fzf.obsinfo ++++++ --- /var/tmp/diff_new_pack.NTrkKY/_old 2025-08-21 17:00:31.604973872 +0200 +++ /var/tmp/diff_new_pack.NTrkKY/_new 2025-08-21 17:00:31.608974040 +0200 @@ -1,5 +1,5 @@ name: jj-fzf -version: 0.25.0 -mtime: 1737599606 -commit: 5fae4591a73b1ff1fb38633f97b902a78b8dd81d +version: 0.32.0 +mtime: 1755135395 +commit: 01e93e5640b41a753cc1bca69dc1b225ce645115