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
 

Reply via email to