File jj-fzf-0.25.0.obscpio of Package jj-fzf

07070100000000000041ED0000000000000000000000026791AA7600000000000000000000000000000000000000000000001600000000jj-fzf-0.25.0/.github07070100000001000041ED0000000000000000000000026791AA7600000000000000000000000000000000000000000000002000000000jj-fzf-0.25.0/.github/workflows07070100000002000081ED0000000000000000000000016791AA7600001AEC000000000000000000000000000000000000002A00000000jj-fzf-0.25.0/.github/workflows/ircbot.py#!/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()
07070100000003000081A40000000000000000000000016791AA7600000752000000000000000000000000000000000000002C00000000jj-fzf-0.25.0/.github/workflows/testing.yml# 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"
07070100000004000081A40000000000000000000000016791AA7600000006000000000000000000000000000000000000001900000000jj-fzf-0.25.0/.gitignorewiki/
07070100000005000081A40000000000000000000000016791AA7600004155000000000000000000000000000000000000001600000000jj-fzf-0.25.0/LICENSEMozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.
07070100000006000081A40000000000000000000000016791AA76000003ED000000000000000000000000000000000000001700000000jj-fzf-0.25.0/Makefile# 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
07070100000007000081A40000000000000000000000016791AA7600001BDB000000000000000000000000000000000000001600000000jj-fzf-0.25.0/NEWS.md## JJ-FZF 0.25.0 - 2025-01-23

### Added:
* Fzflog: use jjlog unless jj-fzf.fzflog-depth adds bookmark ancestry
* Use author.email().local(), required by jj-0.25
* Absorb: unconditionally support absorb
* Evolog: add Alt-J to inject a historic commit
* Evolog: add Enter to browse detailed evolution with patches
* Add Ctrl-T evolog dialog with detailed preview
* Add content-diff to jj describe
* Add ui.default-description to commit messages
* Display 'private' as a flag in preview
* Add jj-am.sh to apply several patches in email format
* Add jj-undirty.el, an elisp hook to auto-snapshot after saving emacs buffers

### Changed:
* Always cd to repo root, so $PWD doesn't vanish
* Adjust Makefile to work with macOS, #6
* Merging: prefer (master|main|trunk) as UPSTREAM
* Make sure to use gsed
* Check-gsed: show line numbers
* Echo_commit_msg: strip leading newline from ui.default-description
* Flags: display hidden, divergent, conflict
* Cut off the preview after a few thausand lines
* Split-files: try using `jj diff` instead of `git diff-tree`
* Use JJ_EDITOR to really override th JJ editor settings
* Honor the JJ_EDITOR precedence
* Show content diff when editing commit message
* Adjust Bookmark, Commit, Change ID descriptions
* Display 'immutable' as a flag in preview
* Fzflog: silence deprecation warnings on stderr
* Include fzflog error messages in fzf input if any
* Unset FZF_DEFAULT_COMMAND in subshells

### Fixed:
* Fix RESTORE-FILE title
* Properly parse options --help, --key-bindings, --color=always
* Echo_commit_msg: skip signoff if no files changed

### Deprecation:
* Deprecate Alt-S for restore-file
* Deprecate Ctrl-V for gitk

### Breaking:
* Depend on jj-0.25.0
* Op-log: use Alt-J to inject an old working copy as historic commit
* Alt-Z: subshells will always execute in the repository root dir

### Contributors

Thanks to everyone who made this release happen!

* Tim Janik (@tim-janik)
* Douglas Stephen (@dljsjr)

## JJ-FZF 0.24.0 - 2024-12-12

### Added:
* Added Alt-O: Absorb content diff into mutable ancestors
* Added `jj op show -p` as default op log preview (indicates absorbed changes)
* Added marker based multi-step undo which improved robustness
* Op-log: Added restore (Alt-R), undo memory reset (Alt-K) and op-diff (Ctrl-D)
* Added RFC-1459 based simple message IRC bot for CI notifications
* Added checks for shellcheck-errors to CI
* Creating a Merge commit can now automatically rebase (Alt-R) other work
* Added duplicate (Alt-D) support to rebase (including descendants)
* Added auto-completion support to bookmarks set/move (Alt-B)
* Reparenting: added Alt-P to simplify-parents after `jj rebase`
* Implemented faster op log preview handling
* New config `jj-fzf.fzflog-depth` to increase `fzflog` depth
* Ctrl-I: add diff browser between selected revision and working copy
* F5: trigger a reload (shows concurrent jj repo changes)
* Support rebase with --ignore-immutable via Alt-I
* Implement adaptive key binding display (Alt-H)
* Ctrl-H: show extended jj-fzf help via pager
* Broadened divergent commit support: squash-into-parent, describe, log
* Started adding unit tests and automated unit testing in CI
* Introduced Makefile with rules to check, install, uninstall

### Breaking:
* Depend on jj-0.24.0 and require fzf-0.43.0
* Removed Alt-U for `jj duplicate`, use rebase instead: Alt-R Alt-D
* Assert that bash supports interactive mode with readline editing
* Check-deps: check dependencies before installing
* Rebase: rename rebasing to `jj-fzf rebase`
* Rebase: apply simplify-parents to the rebased revision only
* Rename 'edit' (from 'edit-workspace')
* Rename revset-assign → revset-filter
* Op-log: Ctrl-S: Preview "@" at a specific operation via `jj show @`
  (formerly Ctrl-D)

### Changed:
* Avoid JJ_CONFIG overrides in all places
* Support ui.editor, ui.diff-editor and other settings
* Squash-into-parent: use `jj new -A` to preserve change_id
* Jump to first when reparenting and after rebase
* Ctrl-P: jj git fetch default remote, not all
* Support deletion of conflicted bookmarks
* Line Blame: skip signoff and empty lines

### Fixed:
* Avoid slowdowns during startup
* Fixed some cases of undesired snapshotting
* Lots of fixes and improvements to allow automated testing
* Minor renames to make shellcheck happy
* Log: Ctrl-L: fix missing patch output
* Ensure `jj log` view change_id width matches jj log default width


## JJ-FZF 0.23.0 - 2024-11-11

Development version - may contain bugs or compatibility issues.

### Breaking:
* Depend on jj-0.23.0
* Remove experimental line-history command

### Added:
* Support 'gsed' as GNU sed binary name
* Support line blame via: jj-fzf +<line> <gitfile>
* Support '--version' to print version
* Define revset `jjlog` to match `jj log`
* Define revset `fzflog` as `jjlog` + tags + bookmarks
* Display `jj log -r fzflog` revset by default
* Store log revset in --repo `jj-fzf.revsets.log`
* Ctrl-R: reload log with new revset from query string

### Changed:
* Require 'gawk' as GNU awk binary
* Ctrl-Z: use user's $SHELL to execute a subshell
* Shorten preview diffs with --ignore-all-space
* Show error with delay after failing jj commands
* Restore-file: operate on root relative file names
* Split-files: operate on root relative file names
* Fallback to @ if commands are called without a revision
* Allow user's jj config to take effect in log display
* Unset JJ_CONFIG in Ctrl+Z subshell
* Rebase: Alt-P: toggle simplify-parents (off by default)
* Reduce uses of JJ_CONFIG (overrides user configs)

### Fixed:
* Split-files: use Git diff-tree for a robust file list
* Ensure that internal sub-shell is bash to call functions, #1
* Clear out tags in screencast test repo
* Various smaller bug fixes
* Add missing --ignore-working-copy in some places
* Fix git_head() expression for jj-0.23.0

### Removed:
* Remove unused color definitions
* Skip explicit jj git import/export statements
* Skip remove-parent in screencast, use simplify-parents

### Contributors

Thanks to everyone who made this release happen!

* Török Edwin (@edwintorok)
* Tim Janik (@tim-janik)


## JJ-FZF 0.22.0 - 2024-11-05

First project release, depending on jj-0.22.0, including the following commands:
- *Alt-A:* abandon
- *Alt-B:* bookmark
- *Alt-C:* commit
- *Alt-D:* delete-refs
- *Alt-E:* diffedit
- *Alt-F:* split-files
- *Alt-I:* split-interactive
- *Alt-K:* backout
- *Alt-L:* line-history
- *Alt-M:* merging
- *Alt-N:* new-before
- *Alt-P:* reparenting
- *Alt-Q:* squash-into-parent
- *Alt-R:* rebasing
- *Alt-S:* restore-file
- *Alt-T:* tag
- *Alt-U:* duplicate
- *Alt-V:* vivifydivergent
- *Alt-W:* squash-@-into
- *Alt-X:* swap-commits
- *Alt-Z:* undo
- *Ctrl-↑:* preview-up
- *Ctrl-↓:* preview-down
- *Ctrl-A:* author-reset
- *Ctrl-D:* describe
- *Ctrl-E:* edit-workspace
- *Ctrl-F:* file-editor
- *Ctrl-H:* help
- *Ctrl-L:* log
- *Ctrl-N:* new
- *Ctrl-O:* op-log
- *Ctrl-P:* push-remote
- *Ctrl-T:* toggle-evolog
- *Ctrl-U:* clear-filter
- *Ctrl-V:* gitk

See also `jj-fzf --help` or the wiki page
[jj-fzf-help](https://github.com/tim-janik/jj-fzf/wiki/jj-fzf-help) for detailed descriptions.
07070100000008000081A40000000000000000000000016791AA7600001936000000000000000000000000000000000000001800000000jj-fzf-0.25.0/README.md<!-- BADGES -->
[![License][mpl2-badge]][mpl2-url]
[![Issues][issues-badge]][issues-url]
[![Irc][irc-badge]][irc-url]

<!-- HEADING -->
JJ-FZF
======

![JJ-FZF Intro](https://github.com/user-attachments/assets/a4e248d1-15ef-4967-bc8a-35783da45eaa)
**JJ-FZF Introduction:** [Asciicast](https://asciinema.org/a/684019) [MP4](https://github.com/user-attachments/assets/1dcaceb0-d7f0-437e-9d84-25d5b799fa53)

<!-- ABOUT -->
## About jj-fzf

`JJ-FZF` is a text UI for [jj](https://martinvonz.github.io/jj/latest/) based on [fzf](https://junegunn.github.io/fzf/), implemented as a bash shell script.
The main view centers around `jj log`, providing previews for the `jj diff` or `jj obslog` of every revision.
Several key bindings are available to quickly perform actions such as squashing, swapping, rebasing, splitting, branching, committing, abandoning revisions and more.
A separate view for the operations log `jj op log` enables fast previews of old commit histories or diffs between operations, making it easy to `jj undo` any previous operation.
The available hotkeys are displayed onscreen for simple discoverability.
The commands and key bindings can also be displayed with `jj-fzf --help` and are documented in the wiki: [jj-fzf-help](https://github.com/tim-janik/jj-fzf/wiki/jj-fzf-help)

The `jj-fzf` script is implemented in bash-5.1, using fzf and jj with git.
Command line tools like sed, grep, gawk are assumed to provide GNU tool semantics.

<!-- USAGE -->
## Usage

Start `jj-fzf` in any `jj` repository and study the keybindings.
Various `jj` commands are accesible through `Alt` and `Ctrl` key bindings.
The query prompt can be used to filter the *oneline* revision display from the `jj log` output and
the preview window shows commit and diff information.
When a key binding is pressed to modify the history, the corresponding `jj` command with its
arguments is displayed on stderr.

<!-- FEATURES -->
## Features

### Splitting Commits

This screencast demonstrates how to handle large changes in the working copy using `jj-fzf`.
It begins by splitting individual files into separate commits (`Alt+F`), then interactively splits (`Alt+I`) a longer diff into smaller commits.
Diffs can also be edited using the diffedit command (`Alt+E`) to select specific hunks.
Throughout, commit messages are updated with the describe command (`Ctrl+D`),
and all changes can be undone step by step using `Alt+Z`.

![Splitting Commits](https://github.com/user-attachments/assets/d4af7859-180e-4ecf-872c-285fbf72c81f)
**Splitting Commits:** [Asciicast](https://asciinema.org/a/684020) [MP4](https://github.com/user-attachments/assets/6e1a837d-4a36-4afd-ad7e-d1ce45925011)

### Merging Commits

This screencast demonstrates how to merge commits using the `jj-fzf` command-line tool.
It begins by selecting a revision to base the merge commit on, then starts the merge dialog with `Alt+M`.
For merging exactly 2 commits, `jj-fzf` suggests a merge commit message and opens the text editor before creating the commit.
More commits can also be merged, and in such cases, `Ctrl+D` can be used to describe the merge commit afterward.

![Mergin Commits](https://github.com/user-attachments/assets/47be543f-4a20-42a2-929b-e9c53ad1f896)
**Mergin Commits:** [Asciicast](https://asciinema.org/a/685133) [MP4](https://github.com/user-attachments/assets/7d97f37f-c623-4fdb-a2de-8860bab346a9)

### Rebasing Commits

This screencast demonstrates varies ways of rebasing commits (`Alt+R`) with `jj-fzf`.
It begins by rebasing a single revision (`Alt+R`) before (`Ctrl+B`) and then after (`Ctrl+A`) another commit.
After that, it moves on to rebasing an entire branch (`Alt+B`), including its descendants and ancestry up to the merge base, using `jj rebase --branch <b> --destination <c>`.
Finally, it demonstrates rebasing a subtree (`Alt+S`), which rebases a commit and all its descendants onto a new commit.

![Rebasing Commits](https://github.com/user-attachments/assets/d2ced4c2-79ec-4e7c-b1e0-4d0f37d24d70)
**Rebasing Commits:** [Asciicast](https://asciinema.org/a/684022) [MP4](https://github.com/user-attachments/assets/32469cab-bdbf-4ecf-917d-e0e1e4939a9c)

### "Mega-Merge" Workflow

This screencast demonstrates the [Mega-Merge](https://ofcr.se/jujutsu-merge-workflow) workflow, which allows to combine selected feature branches into a single "Mega-Merge" commit that the working copy is based on.
It begins by creating a new commit (`Ctrl+N`) based on a feature branch and then adds other feature branches as parents to the commit with the parent editor (`Alt+P`).
As part of the workflow, new commits can be squashed (`Alt+W`) or rebased (`Alt+R`) into the existing feature branches.
To end up with a linear history, the demo then shows how to merge a single branch into `master` and rebases everything else to complete a work phase.

![Mega-Merge Workflow](https://github.com/user-attachments/assets/f944afa2-b6ea-438d-802b-8af83650a65f)
**Mega-Merge:** [Asciicast](https://asciinema.org/a/685256) [MP4](https://github.com/user-attachments/assets/eb1a29e6-b1a9-47e0-871e-b2db5892dbf1)

<!-- CONTRIB -->
## Contrib Directory

The `contrib/` directory contains additional tools or scripts that complement the main jj-fzf functionality.
These scripts are aimed at developers and provide useful utilities for working with jj.

* **jj-am.sh:** A very simple script that allows to apply patches to a jj repository.
  `Usage: ~/jj-fzf/contrib/jj-am.sh [format-patch-file...]`

* **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"))`

<!-- LICENSE -->
## License

This application is licensed under
[MPL-2.0](https://github.com/tim-janik/anklang/blob/master/LICENSE).


<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[irc-badge]: https://img.shields.io/badge/Live%20Chat-Libera%20IRC-blueviolet?style=for-the-badge
[irc-url]: https://web.libera.chat/#Anklang
[issues-badge]: https://img.shields.io/github/issues-raw/tim-janik/tools.svg?style=for-the-badge
[issues-url]: https://github.com/tim-janik/tools/issues
[mpl2-badge]: https://img.shields.io/static/v1?label=License&message=MPL-2&color=9c0&style=for-the-badge
[mpl2-url]: https://github.com/tim-janik/tools/blob/master/LICENSE
<!-- https://github.com/othneildrew/Best-README-Template -->
07070100000009000041ED0000000000000000000000026791AA7600000000000000000000000000000000000000000000001600000000jj-fzf-0.25.0/contrib0707010000000A000081ED0000000000000000000000016791AA76000008A0000000000000000000000000000000000000001F00000000jj-fzf-0.25.0/contrib/jj-am.sh#!/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 ; }
VERSION=0.2.0

# == Help ==
show_help()
{
  cat <<-__EOF__
	Usage: $SCRIPTNAME [OPTIONS...] PATCHFILE...

	Apply one or more patch files (from git-format-patch) to a jj repository.

	Options:
	  -h, --help     Display this help and exit
	  --version      Display version information and exit
	Arguments:
	  PATCHFILE      Path to a patch file containing commit message and diff
	__EOF__
}

# == Parse Options ==
MBOXES=()
while test $# -ne 0 ; do
  case "$1" in \
    --version)  echo "$SCRIPTNAME $VERSION"; exit ;;
    -h|--help)	show_help; exit 0 ;;
    -*)		die "unknown option: $1" ;;
    *)		MBOXES+=("$1") ;;
  esac
  shift
done

# == Functions ==
# Create temporary dir, assigns $TEMPD
temp_dir()
{
  test -n "${TEMPD:-}" || {
    TEMPD="`mktemp --tmpdir -d $SCRIPTNAME-XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/$SCRIPTNAME.pid
  }
}
# Create new commit
jj_commit()
(
  # collect commit infor from header
  HEADER="$1" BODY="$(<"$2")" PATCH="$3"
  AUTHOR="$(sed -nr '/^Author: /{ s/^[^:]*: //; p; q; }' < $HEADER)"
  EMAIL="$(sed -nr '/^Email: /{ s/^[^:]*: //; p; q; }' < $HEADER)"
  DATE="$(sed -nr '/^Date: /{ s/^[^:]*: //; p; q; }' < $HEADER)"
  DATE="$(date --rfc-3339=ns -d "$DATE")"
  SUBJECT="$(sed -nr '/^Subject: /{ s/^[^:]*: //; p; q; }' < $HEADER)"
  export JJ_TIMESTAMP="$DATE"
  test -z "$BODY" && NL='' || NL=$'\n\n'
  ARGS=(
    --config-toml "user.name=\"$AUTHOR\""
    --config-toml "user.email=\"$EMAIL\""
    --message="$SUBJECT$NL$BODY"
  )
  # create commit
  jj new "${ARGS[@]}"
  # try patch
  patch -p1 < "$PATCH"
)

# == Process ==
temp_dir	# for $TEMPD
for mbox in "${MBOXES[@]}" ; do
  echo "Apply: ${mbox##*/}"
  rm -f "$TEMPD/header" "$TEMPD/body" "$TEMPD/patch"
  git mailinfo -b -u --encoding=POSIX.UTF-8 "$TEMPD/body" "$TEMPD/patch" > "$TEMPD/header" < "$mbox"
  jj_commit "$TEMPD/header" "$TEMPD/body" "$TEMPD/patch"
done
# snapshot last patch
jj status
0707010000000B000081A40000000000000000000000016791AA76000004BD000000000000000000000000000000000000002400000000jj-fzf-0.25.0/contrib/jj-undirty.el;; This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0

;; == jj-undirty ==
;; Update JJ repo after saving a buffer
(defun jj-undirty()
  "Execute `jj status` to snapshot the current repository.
This function checks if the current buffer resides in a JJ repository,
and if so executes `jj status` while logging the command output to
the '*jj-undirty*' buffer.
This function is most useful as hook, to frequently snapshot the
workgin copy and update the JJ op log after files have been modified:
  (add-hook 'after-save-hook 'jj-undirty)"
  (interactive)
  (when (locate-dominating-file "." ".jj")	; detect JJ repo
    (progn
      (let ((absfile (buffer-file-name))
	    (buffer (get-buffer-create "*jj-undirty*"))
 	    (process-connection-type nil))	; use a pipe instead of a pty
	(with-current-buffer buffer
	  (goto-char (point-max))		; append to end of buffer
	  (insert "\n# jj-undirty: after-save-hook: " absfile "\njj status\n")
	  (start-process "jj status" buffer	; asynchronous snapshotting
 			 "jj" "--no-pager" "status" "--color=never")
	  ))))
  )

;; Detect JJ repo and snapshot on every save
(add-hook 'after-save-hook 'jj-undirty)
;; (remove-hook 'after-save-hook 'jj-undirty)
0707010000000C000081ED0000000000000000000000016791AA7600010E3A000000000000000000000000000000000000001500000000jj-fzf-0.25.0/jj-fzf#!/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"

# == only jj-root cannot vanish during checkouts ==
JJROOT=$(jj --ignore-working-copy root) &&
  cd "$JJROOT" ||	# always ensure root relative paths
    die "$PWD: not a JJ repository"

# == PREVIEW fast path ==
JJFZF_PRIVATE="$(jj config get --ignore-working-copy --no-pager git.private-commits 2>/dev/null)" &&
  [[ "$JJFZF_PRIVATE" =~ ^[.a-z_()-]+$ ]] || JJFZF_PRIVATE=''	# only supported unquoted revset names
JJ_FZF_SHOWDETAILS='
concat(
  builtin_log_oneline,
  "Change ID: " ++ self.change_id() ++ "\n",
  "Commit ID: " ++ commit_id ++ "\n",
  "Flags:     ", separate(" ",
    if(immutable, label("node immutable", "immutable")),
    if(hidden, label("hidden", "hidden")),
    if(divergent, label("divergent", "divergent")),
    if(conflict, label("conflict", "conflict")),
    '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', 'private')), }"'
  ) ++ "\n",
  surround("Refs:      ", "\n", separate(" ", local_bookmarks, remote_bookmarks, tags)),
  "Parents:  " ++ self.parents().map(|c| " " ++ c.change_id()) ++ "\n",
  "Author:    " ++ format_detailed_signature(author) ++ "\n",
  "Committer: " ++ format_detailed_signature(committer)  ++ "\n\n",
  indent("    ",
    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
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
  if [[ "${2:-} " =~ $REVPAT ]]					# match beginning of jj log line
  then
    REVISION="${BASH_REMATCH[1]}"
    if [[ "${BASH_REMATCH[2]}" == '??' ]]			# divergent change_id
    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
    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
  else								# no valid revision
    true
  fi
  exit 0
fi
export OPRPAT='^[^a-z0-9]*([0-9a-f]{9,})[?]*\ '	# line start, ignore --graph, parse hex letters, space separator
export HEX7PAT='\ ([0-9a-f]{7,})\ ' # space enclosed hexadecimal pattern
case "${1:-}" in
  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
      } ; exit ;;
  preview_opshow)
    [[ " ${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 --no-graph -s -r "@"
	jj --no-pager --ignore-working-copy --at-op "${BASH_REMATCH[1]}" --color=always show -T ' "\n" ' -r "@"
      } ; exit ;;
  preview_oppatch)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --color=always op show -p "${BASH_REMATCH[1]}"
      } | head -n 4000 ; exit ;;
  preview_opdiff)
    [[ " ${2:-} " =~ $OPRPAT ]] && {
	jj --no-pager --ignore-working-copy --color=always op diff -f "${BASH_REMATCH[1]}" -t @
      } ; exit ;;
  preview_evolog)
    [[ " ${2:-} " =~ $HEX7PAT ]] && {
      jj --no-pager --ignore-working-copy evolog --color=always -n1 -p -T "$JJ_FZF_SHOWDETAILS" -r "${BASH_REMATCH[1]}" |
	head -n 4000
    } ; 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
while test $# -ne 0 ; do
  case "$1" in \
    -h|--help)		SHOWHELP=t ;;
    --key-bindings)	SHOWKEYBINDINGS=t ;;
    --version)		echo "$SCRIPTNAME $VERSION"; exit ;;
    --oneshot)		ONESHOT=true ;; # auto-exit after first command
    --color=always)	COLORALWAYS=t ;;
    *)         		break ;;
  esac
  shift
done

# == Config ==
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"
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'
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=(
  --ansi --no-mouse -x -e --track
  --info default
  --layout reverse-list
  --scroll-off 3
  --bind "alt-up:offset-up"
  --bind "alt-down:offset-down"
  --bind "ctrl-x:jump"
  --bind "ctrl-z:execute( $JJSUBSHELL )"
  --bind='f11:change-preview-window(bottom,75%,border-horizontal|)'
  --preview-window 'wrap,right,border-left'
  --bind=ctrl-alt-x:"execute-silent($INFO_BINDING)+clear-screen"
)
FZFPOPUP=(fzf "${FZFSETTINGS[@]}" --margin "0,3%,5%,3%" --border)
TEMPD=
# for function exports to work sub-shell must be bash too
export SHELL=bash

# == JJ CONFIG ==
# parsable version of builtin_log_oneline; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_ONELINE='
if(root,
  format_root_commit(self),
  label(if(current_working_copy, "working_copy"),
    concat(
      separate(" ",
        format_short_change_id_with_hidden_and_divergent_info(self),
        if(author.email(), author.email().local(), email_placeholder),
        author.timestamp().local().format("%Y-%m-%d"),
        format_short_commit_id(commit_id),
        bookmarks,
        tags,
        working_copies,
        if(git_head, label("git_head", "git_head()")),
        if(conflict, label("conflict", "conflict")),
        if(empty, label("empty", "(empty)")),
        '"${JJFZF_PRIVATE:+ if(self.contained_in('$JJFZF_PRIVATE') && !immutable, label('committer', '🌟')), }"'
        if(description,
          description.first_line(),
          label(if(empty, "empty"), description_placeholder),
        ),
      ) ++ "\n",
    ),
  )
)'
# builtin_log_oneline with commit_id *before* other tags/bookmarks/etc and force committer().timestamp
EVOLOG_ONELINE='
if(root,
  format_root_commit(self),
  label(if(current_working_copy, "working_copy"),
    concat(
      separate(" ",
        format_short_change_id_with_hidden_and_divergent_info(self),
        if(author.email(), author.email().local(), email_placeholder),
        format_timestamp(self.committer().timestamp()),
        format_short_commit_id(commit_id),
        bookmarks,
        tags,
        working_copies,
        if(git_head, label("git_head", "git_head()")),
        if(conflict, label("conflict", "conflict")),
        if(empty, label("empty", "(empty)")),
        if(description,
          description.first_line(),
          label(if(empty, "empty"), description_placeholder),
        ),
      ) ++ "\n",
    ),
  )
)'

# == Utils ==
# Create temporary dir, assigns $TEMPD
temp_dir()
{
  test -n "$TEMPD" || {
    TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/jj-fzf.pid
  }
}
# Match JJ revision as first ASCII word (e.g. as in builtin_log_oneline)
export OPPAT='^[^a-z()0-9]*([0-9a-f]{9,})\ '
# Try to extract non-divergent revision or parse expression
xrev_maybe()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RV='@' || RV=
  # or match abbreviated change_id pattern
  if test -z "$RV" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECHANGE='if(self.divergent(), "", change_id)'
    # only allow non-divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECHANGE" 2>/dev/null) || :
  fi
  # or match syntactically valid expressions
  test -z "$RV" && # divergent matches produce concatenated change_ids
    RV=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T change_id 2>/dev/null) || :
  # final validation that $RV is indeed a unique identifier for a non-divergent change_id
  test -n "$RV" &&
    jj --no-pager --ignore-working-copy log --no-graph -T change_id -r "$RV" 2>/dev/null # pass on exit status
)
# Extract non-divergent revision or show error
xrev()
(
  xrev_maybe "$@" ||
    ERROR "failed to parse revision: ${1:-}"
)
FUNCTIONS+=( 'xrev' )
# Extract commit_id or show error
xrev_as_commit()
(
  # accept not-divergent working copy
  [[ " $* " =~ ^\ +\@ ]] &&
    RC='@' || RC=
  # or match abbreviated change_id pattern
  if test -z "$RC" && [[ " $* " =~ $REVPAT ]] ; then
    UNIQUECOMMIT='if(self.divergent(), "", commit_id)'
    # check for divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " ${BASH_REMATCH[1]} " -T "$UNIQUECOMMIT" 2>/dev/null) || :
    test -n "$RC" ||	# non-divergent, else fallback to commit hash parsing
      RC=$(echo " $* " | grep -Po '(?<= )[a-f0-9]{8,}(?= )') || :
  fi
  # or match syntactically valid expressions
  test -z "$RC" && # divergent matches produce concatenated commit_ids
    RC=$(jj log --no-pager --ignore-working-copy --no-graph -r " $* " -T commit_id 2>/dev/null) || :
  # final validation that $RC is indeed a unique identifier for a single commit
  test -n "$RC" &&
    jj --no-pager --ignore-working-copy log --no-graph -T commit_id -r "$RC" 2>/dev/null ||
      ERROR "failed to parse commit id: ${1:-}"
)
FUNCTIONS+=( 'xrev_as_commit' )
# Yield the revision change_id or a commit_id if it is divergent
xrev_or_commit()
(
  xrev_maybe "$@" ||
    xrev_as_commit "$@"
)
FUNCTIONS+=( 'xrev_or_commit' )
# Look up full commit hash via JJ commit_id
rev_commitid()	( xrev_as_commit "$@" )
# Print first bookmark or the revision itself
rev_bookmark1()	( $JJFZFSHOW -T 'concat(separate(" ",bookmarks), " ", change_id)' -r "$1" | awk '{print $1;}' )
# Get revision description
rev_description() ( $JJFZFSHOW -T 'concat(description)' -r "$1" )

# Condense commit empty/description/parent state into a key word
rev_edpstate()
(
  export EDPSTATE='separate("-", if(empty, "empty", "diff"), if(description, "description", "silent"), "p" ++ self.parents().len()) ++ "\n"'
  $JJFZFSHOW -r "$1" -T "$EDPSTATE" # empty-description-p2 diff-silent-p1 etc
)

# List parents of a revision
rev_parents()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "all: $1-" -T 'change_id++"\n"'
)

# List children of a revision
rev_children()
(
  jj --no-pager --ignore-working-copy log --no-graph -r "all: $1+" -T 'change_id++"\n"'
)

# join_args <joiner> [args...]
join_args()
{
  local j="${1:-}" first="${2:-}"
  if shift 2; then
    printf "%s" "$first" "${@/#/$j}"
  fi
}

# reverse_array ORIG REVERSED - copy the elements from ORIG in REVERSED in reverse order
reverse_array()
{
  local -n array_O_=$1
  local -n array_R_=$2
  # Loop in reverse order
  for ((i=${#array_O_[@]}-1; i>=0; i--)); do
    array_R_+=("${array_O_[i]}")
  done
}

# diff_arrays BEFORE AFTER RESULT - store the elements from AFTER without elements from BEFORE in RESULT
diff_arrays()
{
  local -n array_O_=$1
  local -n array_N_=$2
  local -n array_R_=$3
  declare -A counts_
  # Mark elements in A
  for elem in "${array_O_[@]}" ; do
    counts_["$elem"]=1
  done
  # Add all of B to C if not in A
  for elem in "${array_N_[@]}"; do
    test -z "${counts_[$elem]:-}" &&
      array_R_+=("$elem") # || echo "SKIP: $elem : ${counts_[$elem]:-}"
  done
  true
}

# backward_chronologic [REVISIONS] - produce revisions in backwards chronological order
backward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r all:"$ORREVS" -T 'change_id ++ "\n"'
)

# forward_chronologic [REVISIONS] - produce revisions in chronological order
forward_chronologic()
(
  test $# -ge 1 || return
  ORREVS=$(join_args '|' "$@")
  jj --no-pager --ignore-working-copy log --no-graph -r all:"$ORREVS" -T 'change_id ++ "\n"' --reversed
)

# Require .git directory and set GIT_DIR
require_git_dir()
{
  test -e "$JJROOT/.git" &&
    export GIT_DIR="$JJROOT/.git" || {
      test -e "$JJROOT/.jj/repo/store/git" &&
	export GIT_DIR="$JJROOT/.jj/repo/store/git" ||
	  die "$PWD: failed to find .git store"
    }
}

# Write revision from `jj new -m $3 --no-edit -B $2` to $1
jj_new_before_no_edit()
{
  local -n result_=$1 # nameref
  local R="$(xrev "${2:-}")" # must use revision to find new parents
  local M="${3:-}"
  # record base commit parents before/after
  local A=( $(rev_parents "$R") )
  ( set -x
    jj new --no-edit --message="$M" --insert-before "$R" # --no-pager
  ) || die
  local B=( $(rev_parents "$R") )
  local C=() && diff_arrays A B C
  [ ${#C[@]} -eq 1 ] ||
    die "failed to find newly created revision"
  result_="${C[0]}"
}

# Exit the current shell with an error message and delay
ERROR()
{
  FUNC="${FUNC:-$0}"
  echo "ERROR: ${FUNC:+$FUNC:}" "$*" >&2
  # Wait a few seconds unless the user presses Enter
  read -t "${JJ_FZF_ERROR_DELAY:-2}"
  exit
}

# == Helpers ==
# Echo signoff
echo_signoff()
(
  JJFZF_SIGNOFF=true	# config get jjfzf.signoff
  if test "${JJFZF_SIGNOFF:-true}" == true ; then
    echo # separating newline before signoff section
    $JJFZFSHOW -T 'format_detailed_signature(author) ++ "\n"' -r @ |
      gsed -e 's/>.*/>/ ; s/^/Signed-off-by: /'
  fi
)
# Echo current or default message
echo_commit_msg()
(
  R="$1"
  if test "$R" != --merge ; then
    S=$(rev_edpstate "$R")
    # keep any existing message
    [[ $S =~ -silent- ]] || {
      rev_description "$R"
      return
    }
    # list parents
    PARENTS=( $(jj --no-pager --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r all:"$R-" --reversed) )
  else # --merge
    shift
    PARENTS=( $(forward_chronologic "$@") )
  fi
  # Create merge message
  if test "$R" == --merge -o "${#PARENTS[@]}" -ge 2 ; then
    SEP="^^^^^^^^^"
    NEWCOMMITS=()
    for p in "${PARENTS[@]}"; do
      NEWCOMMITS+=( $(rev_commitid $p) )
    done
    MERGE_BASE=$(git merge-base --octopus "${NEWCOMMITS[@]}")
    echo -e "# $SEP DRAFT:  merge" "${PARENTS[@]}" "$SEP # DELETE THIS"
    test "${#PARENTS[@]}" -le 2 &&
      echo "Merge branch '`rev_bookmark1 ${PARENTS[1]}`' into '`rev_bookmark1 ${PARENTS[0]}`'" ||
	echo "Merge branches:" "${PARENTS[@]}"
    for c in "${NEWCOMMITS[@]}"; do
      test "$c" == "$MERGE_BASE" && continue
      test "${#PARENTS[@]}" -le 2 &&
	echo -e "\n* Branch commit log:" || # "$c ^$MERGE_BASE"
	  echo -e "\n* Branch '`rev_bookmark1 $c`' commit log:"
      git log --pretty=$'\f%s%+b' $c ^$MERGE_BASE |
	gsed '/^\([A-Z][a-z0-9-]*-by\|Cc\):/d' | # strip Signed-off-by:
	gsed '/^$/d ; s/^/\t/ ; s/^\t\f$/  (no description)/ ; s/^\t\f/  /'
    done
    echo_signoff
  else # Commit message based on files
    # start with file name prefixes
    FILES=()
    readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | gsed 's/^\w //')
    test ${#FILES[@]} -gt 0 &&
      printf "%s: \n" "${FILES[@]}" ||
	echo ""
    { jj config --no-pager get 'ui.default-description' 2>/dev/null || : ; } | gsed '1{/^$/d}'
    test ${#FILES[@]} -le 0 ||
      echo_signoff
  fi
)
# Run user editor: user_editor_on_var <FILE> <VARIABLE> [COMMIT]
user_editor_on_var()
{
  local FILE="$1" COMMIT="${3:-}" N=
  declare -n _ueovMSG="$2"			# <VARIABLE> alias
  # create msg file
  temp_dir
  local TEMPFILE="$TEMPD/$FILE"
  cat >"$TEMPFILE" <<<"$_ueovMSG"
  test -z "$COMMIT" || {
    jj diff --ignore-working-copy --no-pager --color=never -r "$COMMIT" |
      head -n 4000 > "$TEMPFILE.diff"
    test -s "$TEMPFILE.diff" && {
      echo
      echo '# -------- >8 -------- >8 -------- 8< -------- 8< --------'
      echo '# Everything below the snippet mark will be ignored'
      echo '#'
      echo '# Content diff of this revision:'
      cat "$TEMPFILE.diff"
    }
    rm -f "$TEMPFILE.diff"
  } >> "$TEMPFILE"
  # edit commit msg
  test -n "${JJ_EDITOR:-}" || # https://jj-vcs.github.io/jj/latest/config/#editor
    JJ_EDITOR="$(jj config get ui.editor 2>/dev/null || echo "${VISUAL:-${EDITOR:-pico}}")"
  $JJ_EDITOR "$TEMPFILE" &&
    N="$(cat "$TEMPFILE")" && {
      test "$_ueovMSG" != "$N" &&
	_ueovMSG="$(gsed -r '/^# -+ >8 -+ >8 -+ 8< -+ 8< -+/Q' < "$TEMPFILE")"
      rm -f "$TEMPFILE"
      return 0
    }
  rm -f "$TEMPFILE"
  return 1
}
# Read input with completion: RESULT="$(PROMPT=… INIT=… read_completing [words…])"
read_completing()
(
  WORDS=( "$@" )
  _read_completion() {
    local line="$READLINE_LINE" point="$READLINE_POINT"
    local cur="${line:0:$point}" # Cut word at point
    # Extract current completion word
    cur="${cur##* }"
    # Generate completions
    local compreply=( $(compgen -W "${WORDS[*]}" -- "${cur}" || :) )
    if test ${#compreply[@]} -ne 1 ; then
      printf "%s\n" "${compreply[@]}" | column >&2
    else # Use unique completion
      local oldlen=${#cur}
      # Replace current word with the completion
      READLINE_LINE="${line:0:$((point - oldlen))}${compreply[0]}${line:$point}"
      READLINE_POINT=$(( point + ${#READLINE_LINE} - ${#line} ))
    fi
    true # Return false aborts readline
  }
  set -o emacs # Use emacs readline mode
  bind -x '"\t": _read_completion'
  READOPTS=()
  test -z "${PROMPT:-}" || READOPTS+=(-p "$PROMPT")
  test -z "${INIT:-}" || READOPTS+=(-i "$INIT")
  read -e "${READOPTS[@]}" INPUT
  test -z "$INPUT" ||
    printf "%s\n" "$INPUT"
)

# == Functions ==
declare -A KEYBINDINGS
FIRSTS=""
NEXTS=""

# fzflog revset aliases
revsets_toml()
(
  FZFLOG_DEPTH="$(jj --ignore-working-copy config get jj-fzf.fzflog-depth 2>/dev/null || echo 0)"
  echo "revset-aliases.fzflog = ''' jjlog | ancestors(bookmarks() | remote_bookmarks(), $FZFLOG_DEPTH) ''' "
  echo "revset-aliases.jjlog = ''' $(jj --ignore-working-copy config get revsets.log 2>/dev/null || echo ..) '''"
  echo "template-aliases.'format_short_change_id(id)' = 'id.shortest(8)'"
  echo "template-aliases.'format_short_commit_id(id)' = 'id.shortest(8)'"
)

# fzflog [--revsetname] [rev] - revision log for fzf
fzflog()
(
  # SEE ALSO: jj config get revsets.log
  [[ "${1:-}" == --revsetname ]] && { REVSETNAME=true; shift; } || REVSETNAME=false
  [[ $# -ge 1 ]] &&
    REVSETS_LOG="$1" ||
      REVSETS_LOG=$(jj --ignore-working-copy config get 'jj-fzf.revsets.log' 2>/dev/null || :)
  test -n "$REVSETS_LOG" || REVSETS_LOG="fzflog"
  if $REVSETNAME ; then
    echo "$REVSETS_LOG"
  else
    jj --no-pager --ignore-working-copy ${JJFZF_ATOP:+--at-op $JJFZF_ATOP} \
       --config-toml "$(revsets_toml)" \
       log --color=always -T "$JJ_FZF_ONELINE" -r "$REVSETS_LOG" 2>/dev/null
  fi
)
FUNCTIONS+=( 'fzflog' )

# revset-filter <revset> - assign to jj-fzf.revsets.log
DOC['revset-filter']='Restart `jj-fzf` using the current query string as new revset for this repository.'
revset-filter()
(
  REVSET="$1"
  jj --config-toml "$(revsets_toml)" --no-pager --ignore-working-copy log --no-graph -T '' -r "$REVSET" >/dev/null 2>&1 ||
    REVSET=fzflog
  ( set -x
    jj --ignore-working-copy config set --repo 'jj-fzf.revsets.log' "$REVSET"
  ) || ERROR
)
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.'
abandon()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj abandon -r "$R" ) ||
    sleep 1
)
KEYBINDINGS["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).'
bookmark()
(
  R="$(xrev_or_commit "${1:-@}")"
  #echo "# Existing Bookmarks:" && jj --no-pager --ignore-working-copy bookmark list
  readarray -t BOOKMARKS < <(jj --no-pager --ignore-working-copy bookmark list -T 'self.name()++"\n"' | sort | uniq)
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "::$R|$R+" -T 'bookmarks++"\n"' | gsed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && INIT="${NEAREST[0]}" || INIT=""
  PROMPT='Bookmark Name: '
  echo "# Assign Bookmark to:"
  jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T builtin_log_oneline
  # Read bookmark with completion
  B="$(read_completing "${BOOKMARKS[@]}")"
  B="${B%% *}" && B="${B##* }" && test -z "$B" && return
  # See https://git-scm.com/docs/git-check-ref-format
  INVALIDPAT='(//|\.\.|/\.|[ :^~?*]|\[|^/|/$|\.$|^@$|@\{|\\|'$'[\x01-\x1f])'
  [[ "$B" =~ $INVALIDPAT ]] && {
    echo "$SELF: bookmark contains invalid characters: $B" >&2
    false || ERROR
  }
  ( set -x
    jj bookmark set -r "$R" --allow-backwards "$B"
  ) || ERROR
  # jj git export --quiet
)
KEYBINDINGS["Alt-B"]="bookmark"

# Commit (full)
DOC['commit']='Use `jj commit` to describe the currently selected revision and create a new child revision as working-copy.'
commit()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  if test "$R" == "$W" -a "$IMMU" != true ; then
    user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
      test "$O" != "$MSG" ||
	ERROR "Commit cancelled by user"
      ( set -x
	jj commit --message="$MSG"
      ) || sleep 1
  else # R is not @, may be immutable
    [[ $IMMU =~ ^true ]] || {
      user_editor_on_var "COMMIT-$R.txt" MSG "$R" &&
	test "$O" != "$MSG" ||
	  ERROR "Commit cancelled by user"
      test "$O" != "$MSG" &&
	( set -x
	  jj describe --no-edit -r "$R" --message="$MSG"
	) || sleep 1
    }
    # open new empty working copy commit
    jj new "$R"
  fi
)
KEYBINDINGS["Alt-C"]="commit"		FIRSTS="$FIRSTS commit"

# Delete Bookmarks and Tags
DOC['delete-refs']='Use `jj bookmark list+delete` to list, selected and delete bookmarks and tags.'
delete-refs()
(
  R="$(xrev_or_commit "${1:-@}")"
  # find first local bookmark in $R, use as query arg
  readarray -t NEAREST < <(jj --no-pager --ignore-working-copy log --no-graph -r "$R" -T 'local_bookmarks++"\n"' | gsed -r 's/\b \b/\n/g; s/\*$//; s/\b@.*//; /^$/d')
  [[ ${#NEAREST[@]} -ge 1 ]] && B=(-q "${NEAREST[0]}") || B=()
  require_git_dir # exports GIT_DIR
  # select bookmark or tag
  DELETELINE=$(
    "${FZFPOPUP[@]}" \
      --border-label '-[ DELETE BOOKMARK/TAG ]-' --color=border:red,label:red \
      --prompt "Delete > " \
      --header $'\n'"Delete selected Bookmark or Tag" --header-first \
      --no-tac --no-sort +m \
      "${B[@]}" \
      < <(
      # list local bookmarks
      jj --ignore-working-copy bookmark list | # gsed reorders conflicted
	gsed -r ':0; /^\s/!s/ \(conflicted\):/: (conflicted)/; N; $!b0; s/\n\s+/ /g' |
	while read MARK rest ; do
	  printf "%-32s [bookmark] %s\n" "${MARK%:}" "$rest"
	done
      echo
      # list git tags
      git tag -n1 | while read MARK rest ; do
	printf "%-32s [tag] %s\n" "$MARK" "$rest"
      done
    ) )
  # delete given bookmark/tag line
  read MARK WHAT rest <<<"$DELETELINE"
  case "$WHAT" in
    "[bookmark]")	( set -x && jj bookmark delete exact:"$MARK" ) || ERROR ;;
    "[tag]")		( set -x && git tag -d "$MARK" ) || ERROR ;;
  esac
)
KEYBINDINGS["Alt-D"]="delete-refs"

# diffedit
DOC['diffedit']='Use `jj diffedit` to select parts of the content diff to be kept in the currently selected revision.'
diffedit()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj diffedit -r "$R"
  ) || sleep 1
)
KEYBINDINGS["Alt-E"]="diffedit"

# Reset commit author
DOC['author-reset']='Use `jj describe --reset-author` to reset the author and email of the currently selected revision.'
author-reset()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj describe --reset-author --no-edit -r "$R"
  ) ||
    sleep 1
)
KEYBINDINGS["Ctrl-A"]="author-reset"

# Describe Commit Message
DOC['describe']='Use `jj describe` to describe the currently selected revision (or divergent commit).'
describe()
(
  R="$(xrev_or_commit "${1:-@}")"
  MSG="$(echo_commit_msg "$R")"
  O="$MSG"
  user_editor_on_var "CHANGE-$R.txt" MSG "$R" ||
    ERROR "Describe cancelled by user"
  test "$O" != "$MSG" ||
    return
  (set -x
   jj describe --no-edit -r "$R" --message="$MSG"
  ) || ERROR
)
KEYBINDINGS["Ctrl-D"]="describe"

# File Editor
DOC['file-editor']='Use `jj edit` to switch to the currently selected revision and opens the files touched by this revision in `$EDITOR`.'
file-editor()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  # read files edited by revision
  readarray -t FILES < <(jj --ignore-working-copy log --no-graph -r "$R" -T '' -s | gsed 's/^\w //')
  # make sure to edit revision
  test "$W" == "$R" || (
    IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
    [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
    set -x
    jj $CMD -r "$R"
  )
  ( set -x
    ${EDITOR:-nano} "${FILES[@]}"
  )
)
KEYBINDINGS["Ctrl-F"]="file-editor"

# Help with JJ commands
DOC['help']='Show the *jj-fzf* help and key binding commands.'
help()
(
  $SELF --help "$@"
)
KEYBINDINGS["Ctrl-H"]="help"

# Split change
DOC['split-interactive']='Use `jj split` to interactively select content diff hunks to be split into a new commit. No text editor is invoked and the new commit gets an empty description.'
split-interactive()
(
  R="$(xrev "${1:-@}")"
  # To avoid message editing, truncate all but the first (original) description
  temp_dir
  cat > $TEMPD/noeditor <<-\__EOF__
	#!/usr/bin/env bash
	set -Eeuo pipefail #-x
	TRUNCATE=n
	test $TRUNCATE == y && echo -n > "$1" || :
	gsed 's/TRUNCATE=./TRUNCATE=y/' -i "$0"
	__EOF__
  chmod +x $TEMPD/noeditor
  export JJ_EDITOR="$TEMPD/noeditor" # Override ui.editor to implement --split-with-no-description
  ( set -x
    jj split --interactive -r "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-I"]="split-interactive"

# Diff Browser
DOC['diff']='Use `jj diff` to view differences between the currently selected revision and the working copy.'
diff()
(
  R="$(xrev_or_commit "${1:-@-}" 2>/dev/null)" || exit # invalid revision
  W="$(xrev_or_commit "@")" || ERROR
  REVS=( $(forward_chronologic "$R" "$W") )
  test "${#REVS[@]}" -ge 2 || REVS+=( "${REVS[0]}" )
  (
    # set -x
    jj --color=always log -r "${REVS[0]} | ${REVS[1]}" -T builtin_log_oneline # | gsed -r '/[k-xyz]/!d; s/ +/  /'
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}" --stat
    echo
    jj diff --ignore-working-copy --color=always --from "${REVS[0]}" --to "${REVS[1]}"
  ) 2>&1 | $JJFZFPAGER
)
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.'
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") )
  ( 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]}"
  ) || die
)
KEYBINDINGS["Alt-K"]="backout"		FIRSTS="$FIRSTS backout"

# Line Blame: jj-fzf +<line> <gitfile>
if [[ $# == 2 ]] && [[ "${1:0:1}" == + ]] ; then
  absroot="$(readlink -f "$JJROOT")"
  absfile="$(readlink -f "$2")"
  [[ $absfile == $absroot/* ]] && {
    echo absroot=$absroot
    echo absf=$absfile
    file="${absfile:((1+${#absroot}))}"
    echo file=${absfile:((1+${#absroot}))}
    jj --no-pager status
    COMMIT="$(rev_commitid @)"
    EMPTY=$'^[| \033\[0-9;m]*$' # anchored pattern for empty line with git log graph chars
    SIGBY=$'^[| \033\[0-9;m]*Signed-off-by:.*@.*$' # anchored pattern for Signed-off-by
    grep -s -n '' "$file" /dev/null |
    "${FZFPOPUP[@]}" \
      --border-label '-[ LINE HISTORY (EXPERIMENTAL) ]-' --color=border:yellow,label:yellow \
      --preview " git log --graph --no-patch -M -C --find-copies-harder --pretty='%C(blue)%h %C(yellow)%aL %C(reset)%B' -L{2}:{1} --color $COMMIT | gsed -nre '/($EMPTY|$SIGBY)/!p; /$EMPTY/{ p; :NEXT n; /($EMPTY|$SIGBY)/b NEXT; p; }' " \
      --bind "enter:execute( git log -M -C --find-copies-harder -L{2},+7:{1} --color $COMMIT | $JJFZFPAGER)" \
      --header "File Line History" \
      --no-tac --no-sort +m -d: \
      --track --bind 'focus:clear-query+unbind(focus)' \
      -q "${absfile:((1+${#absroot}))}:${1:1}:"
  }
  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()
(
  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
)
KEYBINDINGS["Alt-M"]="merging"		FIRSTS="$FIRSTS merging"

# 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.'
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
)
KEYBINDINGS["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).'
new-after()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new --insert-after "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-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).'
new()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  ( set -x
    jj new "$R"
  ) || sleep 1
)
KEYBINDINGS["Ctrl-N"]="new"		FIRSTS="$FIRSTS new"

# JJ_FZF_OP_LOG_ONELINE16 - Oneline op log with 16 character ids, parsed later on; https://github.com/martinvonz/jj/blob/main/cli/src/config/templates.toml
JJ_FZF_OP_LOG_ONELINE16='
label(if(current_operation, "current_operation"),
  coalesce(
    if(root, format_root_operation(self)),
    concat(
      separate(" ", self.id().short(16), self.user(), self.time().start().ago()), " ",
      self.description().first_line(), " ",
      if(self.tags(), self.tags().first_line()),
    )
  )
)'
OP_LOG_FIRSTLINE='self.id() ++ ": " ++ self.description().first_line() ++ "\n"'

# Show `jj op log` but mark undone operations with '⋯'
op_log_oneline()
(
  temp_dir
  # Determine range of undo operations
  if LAST_OPID=$(jj --no-pager --ignore-working-copy config get jj-fzf.last-undo 2>/dev/null) &&
      jj --no-pager --ignore-working-copy op log -n1 --no-graph -T "$OP_LOG_FIRSTLINE" | grep -qF ": undo operation $LAST_OPID" ; then
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16" |
      gsed -r "1,/${LAST_OPID:0:16}\b/s/([@○])/⋯/" # ⮌ ⋯ ⤺↶
  else
    jj --no-pager --ignore-working-copy op log --color=always -T "$JJ_FZF_OP_LOG_ONELINE16"
  fi
)
FUNCTIONS+=( 'op_log_oneline' )

# Oplog
DOC['op-log']='Use `jj op log` to browse the recent operations log. Use hotkeys to change the preview between diff, history and oplog entry mode. Undo the selected operation or restore its working copy into a new commit.'
op-log()
(
  temp_dir
  echo > $TEMPD/oplog.env
  H=$'\n'
  H="$H"$'Ctrl-D: Preview the differences of an operation via `jj op diff -f <op> -t @`\n'
  H="$H"$'Ctrl-L: Preview history at a specific operation via `jj log -r ..`\n'
  H="$H"$'Ctrl-P: Preview changes in an operation with patch via `jj op show -p <op>`\n'
  H="$H"$'Ctrl-S: Preview "@" at a specific operation via `jj show @`\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject working copy of the selected operation as historic commit before @\n'
  H="$H"$'Alt-K: Kill undo memory (marked `⋯`), to restart undo at the top\n'
  H="$H"$'Alt-R: Restore repository to the selected operation via `jj op restore`\n'
  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"
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label '-[ OP-LOG ]-' --color=border:bright-yellow,label:bright-yellow \
    --prompt "Operation > " \
    --header "$H" --header-first \
    --bind "ctrl-d:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_opdiff/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-l:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_oplog/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-p:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_oppatch/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "ctrl-s:execute-silent( gsed 's/^VIEW=.*/VIEW=preview_opshow/' -i $TEMPD/oplog.env )+refresh-preview" \
    --bind "alt-j:execute( $SELF restore-commit {} )+abort" \
    --bind "alt-k:execute( $SELF undo-reset {} )+$RELOAD" \
    --bind "alt-r:execute( $SELF op-restore {} )+abort" \
    --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} )" \
    --preview-window 'nowrap,right,border-left' \
    --preview "[[ {} =~ $OPPAT ]] || exit; export JJFZF_ATOP=\"\${BASH_REMATCH[1]}\" && . $TEMPD/oplog.env && $SELF \$VIEW {}" \
    --no-tac --no-sort +m
  # TODO: remove alt-w in jj-fzf-0.26
)
KEYBINDINGS["Ctrl-O"]="op-log"

undo-op()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  ( set -x
    jj op undo $OP
  ) || ERROR
)
FUNCTIONS+=( 'undo-op' )

restore-commit()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  COMMIT="$(jj --no-pager --ignore-working-copy --at-op $OP show --tool true -T commit_id -r @)"
  echo "# $SELF: insert working copy commit (${COMMIT:0:12}) from operation ${OP:0:12} before @"
  ( set -x
    jj new --no-edit --insert-before @
    jj restore --from "$COMMIT" --to @- --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'restore-commit' )

op-restore()
(
  [[ "$*" =~ $OPPAT ]] && OP="${BASH_REMATCH[1]}" || return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following jj op restore"
  ( set -x
    jj op restore "$OP"
  ) || ERROR
)
FUNCTIONS+=( 'op-restore' )

# Show `jj evolog`
evolog_oneline()
(
  R="$1"
  jj evolog --no-pager --ignore-working-copy --color=always -T "$EVOLOG_ONELINE" -r "$R"
)
FUNCTIONS+=( 'evolog_oneline' )

# Inject historic commit of a revision
evolog-inject()
(
  R="$(xrev "${1:-}")"
  [[ " $2 " =~ $HEX7PAT ]] || die "missing commit"
  C="$(xrev_as_commit "${BASH_REMATCH[1]}")"
  MSG="$(rev_description "$C")"
  NEWREV=
  jj_new_before_no_edit NEWREV "$R" "$MSG"
  ( set -x
    jj restore --from "$C" --to "$NEWREV" --restore-descendants
  ) || ERROR
)
FUNCTIONS+=( 'evolog-inject' )

# Show `jj evolog`
evolog_pager()
(
  [[ " $* " =~ $HEX7PAT ]] && {
    # builtin_log_detailed
    jj --no-pager --ignore-working-copy evolog --color=always -p -r "${BASH_REMATCH[1]}" -T "$JJ_FZF_SHOWDETAILS" 2>&1 |
      $JJFZFPAGER
  }
)
FUNCTIONS+=( 'evolog_pager' )

# Evolog
DOC['evolog']='Use `jj evolog` to browse the evolution of the selected revision. Inject historic commits into the ancestry without changing descendants.'
evolog()
{
  R="$(xrev_or_commit "${1:-@}")"
  temp_dir
  H=$'\n'
  H="$H"$'Enter: Browse evolog with diff\n'
  H="$H"$'\n'
  H="$H"$'Alt-J: Inject evolog entry as historic commit before the revision without changing it.\n'
  export FZF_DEFAULT_COMMAND="$SELF evolog_oneline $R"
  RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
  "${FZFPOPUP[@]}" \
    --border-label "-[ EVOLOG $R ]-" --color=border:yellow,label:bright-yellow \
    --prompt "Evolog > " \
    --header "$H" --header-first \
    --bind "enter:execute( $SELF evolog_pager {} )" \
    --bind "alt-j:execute( $SELF evolog-inject $R {} )+abort" \
    --preview-window 'nowrap,right,border-left' \
    --preview "$SELF preview_evolog {}" \
    --no-tac --no-sort +m
}
KEYBINDINGS["Ctrl-T"]="evolog"

# Split files
DOC['split-files']='Use `jj split` in a loop to split each file modified by the currently selected revision into its own commit.'
split-files()
(
  R="$(xrev "${1:-@}")"
  # read files affected by $R
  mapfile -t MAPFILE < <(jj diff --name-only -r "$(rev_commitid "$R")")
  [[ ${#MAPFILE[@]} -gt 1 ]] ||
    return
  # show undo hint
  echo "# jj op restore $(jj op log -n1 --no-graph -T 'self.id().short()') # <- command to undo the following split"
  # create n-1 new commits from n files
  while [[ ${#MAPFILE[@]} -gt 1 ]] ; do
    unset 'MAPFILE[-1]' # unset 'MAPFILE[${#MAPFILE[@]}-1]'
    export JJ_EDITOR='true' # Override ui.editor to implement --split-with-no-description
    ( set -x
      jj split -r "$R" -- "${MAPFILE[@]}"
    ) || ERROR
  done
)
KEYBINDINGS["Alt-F"]="split-files"

# Fetch and push to remote Git repositories
DOC['push-remote']='Use `jj git fetch` and `jj git push --tracked` to update the local and remote repositories. Pushing needs confirmation after a dry-run.'
push-remote()
(
  ( set -x
    jj git fetch
    jj git push --tracked --dry-run
  ) || ERROR
  read -p 'Try to push to remote? ' YN
  [[ "${YN:0:1}" =~ [yY] ]] ||
    exit
  ( set -x
    jj git push --tracked
  ) || ERROR
)
KEYBINDINGS["Ctrl-P"]="push-remote"

# Absorb a content diff into mutable ancestors
absorb()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj absorb --from "$R"
  ) || ERROR
)
DOC['absorb']='Use `jj absorb` to split the content diff of the current revision and squash pieces into related mutable ancestors.'
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.'
squash-into-parent()
(
  R="$(xrev_or_commit "${1:-@}")"
  W="$(xrev_or_commit "@")"
  if test "$W" == "$R" ; then
    # 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
    # from @. This new working copy will receive any children from the original
    # 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
)
KEYBINDINGS["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.'
squash-@-into()
(
  R="$(xrev "${1:-@}")"
  W="$(xrev "@")"
  test "$R" == "$W" && return
  # See squash-into-parent, for why we need `new --insert-before` when squashing @.
  ( set -x
    jj new --insert-before @
    jj squash --from "$W" --into "$R"
  ) || ERROR
)
KEYBINDINGS["Alt-W"]="squash-@-into"

# Reparent a revision
DOC['reparenting']='Start a dialog to add/delete parents of the current revision. Also supports `jj simplify-parents` after reparenting.'
reparenting()
(
  SRC="$(xrev "${1:-@}")"
  IMMU=$($JJFZFSHOW -r "$SRC" -T 'if(immutable, "true")')
  test "$IMMU" != true || exit 0
  temp_dir
  jj --no-pager --ignore-working-copy log --no-graph -T 'change_id ++ "\n"' -r all:"$SRC-" > $TEMPD/reparenting.lst
  echo 'OP="|"'		>  $TEMPD/reparenting.env
  echo 'SIMPLIFY=false'	>> $TEMPD/reparenting.env
  export SRC TEMPD
  # Parse jj log lines into reparenting.revs
  reparenting_revs()
  {
    mapfile -t PARENTS < $TEMPD/reparenting.lst && PARENTS="( $(join_args '|' "${PARENTS[@]}") )"
    test "$OP" == '|' && FILTER="~ ($SRC|$PARENTS)" || FILTER="& $PARENTS"
    for ARG in "$@" ; do
      [[ "$ARG" =~ $REVPAT ]] || continue
      R=$(jj --no-pager --ignore-working-copy log --no-graph -r "${BASH_REMATCH[1]} $FILTER" -T change_id)
      test -z "$R" ||
	echo "$R"
    done > $TEMPD/reparenting.revs
    mapfile -t REVS < $TEMPD/reparenting.revs && EXPR="$SRC-"
    test "${#REVS[@]}" -ge 1 && EXPR="$SRC- $OP ( $(join_args '|' "${REVS[@]}") )"
    # sort, so we generally merge younger branches into older branches
    forward_chronologic "$EXPR" > $TEMPD/newparents.lst
  }
  # Preview reparenting command for reparenting.revs
  reparenting_cmd()
  (
    echo
    echo "CHANGE PARENTS:"
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst && NEWPARENTS="$(join_args ' | ' "${NEWPARENTS[@]}")"
    echo "jj rebase --source \"$SRC\" --destination \\"
    echo "  \"all: $NEWPARENTS\""
    $SIMPLIFY && echo "jj simplify-parents --revisions \"$SRC\\"
    echo
    echo "SOURCE REVISION:"
    jj --no-pager --ignore-working-copy log --color=always -T builtin_log_oneline -r all:"$SRC | $SRC-"
    echo
    test "$OP" == '|' && deladd='ADD' || deladd='REMOVE'
    echo "$deladd PARENTS:"
    test "$OP" == '|' && deladd='+ ' || deladd='- '
    while read R ; do
      echo -n "$deladd"
      jj --no-pager --ignore-working-copy log --color=always --no-graph -T builtin_log_oneline -r "$R"
    done < $TEMPD/reparenting.revs
  )
  # Provide functions for FZF
  export -f reparenting_revs reparenting_cmd join_args forward_chronologic backward_chronologic reverse_array
  H=$'\n'
  H="$H""Alt-A: ADD    - Add currently selected revisions as new parents"$'\n'
  H="$H""Alt-D: DEL    - Delete selected revisions from current list of parents"$'\n'
  H="$H""Alt-P: SIMPLIFY-PARENTS - Use simplify-parents after reparenting"$'\n'
  export FZF_DEFAULT_COMMAND="$SELF fzflog"
  # FZF select parents
  "${FZFPOPUP[@]}" \
      --border-label '-[ CHANGE PARENTS ]-' --color=border:cyan,label:cyan \
      --preview ". $TEMPD/reparenting.env && reparenting_revs {+} && reparenting_cmd" \
      --prompt "Parents > " \
      --header "$H" --header-first \
      --bind "alt-a:execute-silent( gsed 's/^OP=.*/OP=\"|\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-d:execute-silent( gsed 's/^OP=.*/OP=\"~\"/' -i $TEMPD/reparenting.env )+refresh-preview" \
      --bind "alt-p:execute-silent( gsed 's/^SIMPLIFY=false/SIMPLIFY_=/; s/^SIMPLIFY=true/SIMPLIFY=false/; s/^SIMPLIFY_=/SIMPLIFY=true/' -i $TEMPD/reparenting.env )+refresh-preview" \
      -m --color=pointer:grey \
      --no-tac --no-sort > $TEMPD/selections.txt &&
    mapfile -t selections < $TEMPD/selections.txt &&
    source $TEMPD/reparenting.env &&
    reparenting_revs "${selections[@]}" &&
    mapfile -t NEWPARENTS < $TEMPD/newparents.lst &&
    test "${#NEWPARENTS[@]}" -gt 0 ||
      exit # Reparenting cancelled
  # Re-parent revisions
  ( set -x
    # Ordering is not preserved with 'all:(.|.|.)', only with -d. -d. -d.
    jj rebase --source "$SRC" "${NEWPARENTS[@]/#/-d}"
  ) || ERROR
  # simplify-parents
  ! $SIMPLIFY || (
    set -x
    jj simplify-parents --revisions "$SRC"
  ) || ERROR
)
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.'
rebase()
(
  S="$(xrev "${1:-@}")"
  temp_dir
  echo > $TEMPD/rebase.env
  echo 'DP='			>> $TEMPD/rebase.env
  echo 'FR=--branch'		>> $TEMPD/rebase.env
  echo 'TO=--destination'	>> $TEMPD/rebase.env
  echo 'SP=false'		>> $TEMPD/rebase.env
  echo 'II='			>> $TEMPD/rebase.env
  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)"'
  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-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'
  H="$H""Alt-S: SOURCE    - Rebase specified revision together with descendants"$'\n'
  H="$H""Ctrl-A: AFTER       - The revision to insert after"$'\n'
  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]}" ||
      exit 0
  REV="$(xrev "$REV")"
  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=()
    ( 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
  # simplify-parents
  ! $SP || (
    set -x
    jj simplify-parents --revisions "$S"
  ) || ERROR
)
KEYBINDINGS["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.'
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
)
KEYBINDINGS["Alt-S"]="restore-file"

# Tag Creation
DOC['tag']='EXPERIMENTAL: Enter a tag name to create a new unsigned, annotated tag at the selected revision with `git tag`.'
tag()
(
  R="$(xrev "${1:-@}")"
  C="$(rev_commitid "$R")"
  require_git_dir
  read -p 'Tag Name: ' B &&
    test -n "$B" ||
      return
  M="$(git log -1 --oneline "$C")"
  ( set -x
    git tag -a "$B" -m "$M" "$C"
  ) || ERROR
  #  jj git import --quiet
)
KEYBINDINGS["Alt-T"]="tag"

# Log single change
logrev()
(
  R="$(xrev_or_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"
  ) | $JJFZFPAGER
)
FUNCTIONS+=( 'logrev' )

# Log flat change history
DOC['log']='Use `jj log` to browse the history including patches, starting from the selected revision (or divergent commit).'
log()
{
  R="$(xrev_or_commit "${1:-@}")"
  jj log --ignore-working-copy --color=always --no-graph -T "$JJ_FZF_SHOWDETAILS" -r "::$R" -s -p --ignore-space-change \
    | $JJFZFPAGER
}
KEYBINDINGS["Ctrl-L"]="log"

# vivifydivergent
DOC['vivifydivergent']='When a revision has more than one visible commit, it becomes a divergent revision. This command uses `jj new+squash …` to create a new *change_id* for the currently selected revision, effectively resolving the divergence.'
vivifydivergent()
(
  # fetch commit_id of a divergent revision
  COMMIT="$(xrev_as_commit "${1:-@}")" &&
    WCOPY="$(xrev_as_commit "@")" ||
      die 'no divergent revision'
  # leave working copy alone, unless it is $1
  test "$COMMIT" == "$WCOPY" && NOEDIT= || NOEDIT=--no-edit
  echo "# $SELF vivifydivergent $COMMIT" >&2
  jj --no-pager log --no-graph -T builtin_log_oneline -r "$COMMIT" # --ignore-working-copy
  export JJ_EDITOR='true' # Override ui.editor to implement --squash-with-no-description
  ( set -x
    jj new --insert-after "$COMMIT" $NOEDIT
    jj squash --from "$COMMIT" --into "$COMMIT+"
  ) || ERROR
)
KEYBINDINGS["Alt-V"]="vivifydivergent"	NEXTS="$NEXTS vivifydivergent"

# Gitk View
DOC['gitk']='DEPRECATED: Start `gitk` to browse the *Git* history of the repository.'
gitk()
(
  R="$(xrev "${1:-@}")"
  # jj git export --quiet
  COMMIT="$(rev_commitid "$R")"
  git update-index --refresh || :
  #test -e "$JJROOT/.jj/repo/store/git" && export GIT_DIR="$JJROOT/.jj/repo/store/git" || export GIT_DIR="$JJROOT/.git"
  # readarray -t HEADS < <( jj --ignore-working-copy log --no-graph -T 'commit_id ++ "\n"' -r ' heads(..) ' )
  # beware gitk is executable and sh function
  ( set -x
    exec gitk --branches --tags --remotes --select-commit=$COMMIT $COMMIT HEAD -- # "${HEADS[@]}"
  ) || ERROR
  # jj git import --quiet
)
KEYBINDINGS["Ctrl-V"]="gitk"

# 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()
(
  R="$(xrev_or_commit "${1:-@}")" ||
    die "no such revision"
  IMMU=$($JJFZFSHOW -r "$R" -T 'if(immutable, "true")')
  [[ $IMMU =~ ^true ]] && CMD='new' || CMD='edit'
  ( set -x
    jj $CMD -r "$R"
  ) || ERROR
)
KEYBINDINGS["Ctrl-E"]="edit"

# Swap Commits
DOC['swap-commits']='Use `jj rebase --insert-before` to quickly swap the currenly selected revision with the revision immediately before it.'
swap-commits()
(
  R="$(xrev "${1:-@}")"
  ( set -x
    jj rebase -r "$R" --insert-before "$R-"
  ) || ERROR
)
KEYBINDINGS["Alt-X"]="swap-commits"

# Undo last JJ op
DOC['undo']='Use `jj op undo` to undo the last operation performed by `jj` that was not previously undone.'
undo()
(
  TSELFID='self.id() ++ "\n"'
  if LAST_OPID=$(jj --no-pager --ignore-working-copy config get jj-fzf.last-undo 2>/dev/null) &&
      jj --no-pager --ignore-working-copy op log -n1 --no-graph -T "$OP_LOG_FIRSTLINE" | grep -qF ": undo operation $LAST_OPID" ; then
    # last operation in op log was undo of operation $LAST_OPID
    NEXT_OP="$LAST_OPID-"
  else
    LAST_OPID="<none>"
    NEXT_OP="@"
  fi
  NEXT_OP_ID="$(jj --no-pager --ignore-working-copy op log --at-operation="$NEXT_OP" -n1 --no-graph -T "$TSELFID")"
  echo "# $SELF: jj-fzf.last-undo=${LAST_OPID:0:20} next-undo=${NEXT_OP_ID:0:20}"
  ( set -x
    jj op undo "$NEXT_OP_ID"
  ) || ERROR
  jj --no-pager --ignore-working-copy config set --repo jj-fzf.last-undo "$NEXT_OP_ID"
  # 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"

# Reset undo memory
undo-reset()
(
  jj --no-pager --ignore-working-copy config unset --repo jj-fzf.last-undo
)
FUNCTIONS+=( 'undo-reset' )

# Minimal Markdown transformations for the terminal
sedmarkdown()
(
  B=$'\e[1m'              # Bold
  T=$'\e[32;1;4m'         # Title
  H=$'\e[1;4m'            # Heading
  C=$'\e[36m'             # Code
  I=$'\e[3m'              # Italic
  U=$'\e[4m'              # Underline
  Z=$'\e[0;24m'           # Reset
  W='[][<>{}A-Z| $@○◆a-z0-9/ ↑←↓→-⇿ :….()+-]'      # Word-like chars (english)
  SEDSCRIPT="
    s/\r\`\`\`+\w*(([^\`]*|\`[^\`])+)\r\`\`\`+/$C\1$Z\n/g       # Code block with backticks
    s/\r~~~+\w*(([^~]*|~[^~])+)\r~~~+/$C\1$Z\n/g                # Code block with tilde
    s/(^|\r)# ([^\r]+)[ #]*\r/\1$T\2$Z\r/g                      # Title Heading
    s/(^|\r)##+ ([^\r]+)[ #]*\r/\1$H\2$Z\r/g                    # Headings
    s/(\r\s?\s?)[-*] (\w+\b:)?/\1$B* \2$Z/g                     # List bullet
    s/(\s)\*\*($W+)\*\*/\1$B\2$Z/g                              # Bold
    s/(\s)\*($W+)\*([^*])/\1$I\2$Z\3/g                          # Italic
    s/(\s)_($W+)_([^_])/\1$U\2$Z\3/g                            # Underline
    s/(\s)\`($W+)\`([^\`])/\1$C\2$Z\3/g                         # Code
    s/\r?<!--([^-]|-[^-]|--[^>])*-->//g                         # Html Comments
    s,(\bhttps?://[^ ()\r]+),$U\1$Z,g                           # Link
  "
  tr \\n \\r |
    { $COLOR && gsed -re "$SEDSCRIPT" || cat ; } |
    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
'

# == --help ==
HELPKEYS=$(declare -p KEYBINDINGS) && declare -A HELPKEYS="${HELPKEYS#*=}"	# copy KEYBINDINGS -> HELPKEYS
if test -n "$SHOWHELP" ; then
  # Key bdingins only shown in long form help
  HELPKEYS[Shift-↑]='preview-up'
  HELPKEYS[Ctrl-↑]='preview-up'
  DOC['preview-up']='Scroll the preview window.'
  HELPKEYS[Shift-↓]='preview-down'
  HELPKEYS[Ctrl-↓]='preview-down'
  DOC['preview-down']='Scroll the preview window.'
  HELPKEYS[Ctrl-U]='clear-filter'
  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.'
fi
DISPLAYKEYS="${!HELPKEYS[@]}"
DISPLAYKEYS=$(sort <<<"${DISPLAYKEYS// /$'\n'}" | grep -vF 'Ctrl-Alt-')
if test -n "$SHOWHELP" ; 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""_"
      D="${DOC[$NAME]:-}"
      test -z "$D" ||
	echo "$D" | fold -s -w78 | gsed 's/^/  /'
    done
    echo "$HELP_OUTRO"
  ) | sedmarkdown | $JJFZFPAGER
  exit 0
fi

# == --key-bindings ==
list_key_bindings()
{
  LINES="${LINES:-$JJFZF_LINES}" COLUMNS="${COLUMNS:-$JJFZF_COLUMNS}" # unset by transform-header()
  test "$COLUMNS" -ge 218 && W=4 || {
      test "$COLUMNS" -ge 166 && W=3 || {
	  test "$COLUMNS" -ge 114 && W=2 || W=1; }; }
  [[ ${#DISPLAYKEYS} -gt $(($LINES * $W * 2)) ]] && {
    echo "Ctrl-H: help"		# no space left for jj-fzf.show-keys toggle
    exit 0
  }
  SHOW_KEYS="$(jj --ignore-working-copy config get 'jj-fzf.show-keys' 2>/dev/null || echo true)"
  [[ "$*" =~ --key-toggle ]] && {
    SHOW_KEYS="$(echo "$SHOW_KEYS" | gsed 's/^false/x/; s/^true/false/; s/^x/true/')"
    jj --ignore-working-copy config set --user 'jj-fzf.show-keys' "$SHOW_KEYS"
  }
  $SHOW_KEYS || {
    echo "Ctrl-H: help  Alt-H: show-keys"
    exit 0
  }
  OUTPUT=""
  i=0; WHITE="                                                                                "
  for k in $DISPLAYKEYS ; do
    S="$k: ${HELPKEYS[$k]}"	# printf(1) cannot count UTF-8 continuation chars (0x80-0xBF)
    test ${#S} -lt 26 && S="$S${WHITE:0:$(( 26 - ${#S} ))}"	# so, format like %-26s
    OUTPUT="$OUTPUT$S" #$HIGH"
    i=$(($i+1))
    test 0 == $(($i % $W)) &&
      OUTPUT="$OUTPUT"$'\n' ||
	OUTPUT="$OUTPUT "
  done
  echo -n "$OUTPUT"
}
if test -n "$SHOWKEYBINDINGS" ; then
  list_key_bindings "$@"
  exit 0
fi

# == Function calling ==
if [[ "${1:-}" =~ ^[a-z0-9A-Z_+@-]+ ]] && [[ " ${KEYBINDINGS[*]} ${FUNCTIONS[*]} " =~ \ $1\  ]] ; then
  # Sync JJ working-copy before and after func, according to user config, but avoid paging
  ( set -e
    jj status --no-pager >/dev/null
    trap 'jj status --no-pager >/dev/null' 0 HUP INT QUIT TRAP USR1 PIPE TERM
    FUNC="$1" "$@"
  ) # preserves $FUNC exit status
  exit $?
fi

# == Sync ==
# Sync JJ before starting FZF, so user snapshot config and snapshot errors take effect
( set -x
  jj --no-pager status
) || exit $?

# === TEMPD ==
if test -z "${TEMPD:-}" ; then
  temp_dir
  export JJFZF_OUTER_TEMPD="$TEMPD" JJFZF_COLUMNS="$COLUMNS" JJFZF_LINES="$LINES"
fi
FZFEXTRAS=()
EXECKILLME=
$ONESHOT && {
  echo > "$TEMPD/killme.0"		# ignore first :focus:
  echo "$$" > "$TEMPD/killme.pid"	# then kill FZF
  FZFEXTRAS+=(
    --bind "start:execute( ps -o ppid= \$\$ > $TEMPD/killme.pid )"
    --bind "focus:execute-silent( test -e $TEMPD/killme.0 && rm -f $TEMPD/killme.0 || rm -f $TEMPD/killme.pid )"
  )
  EXECKILLME="+execute( test -e $TEMPD/killme.pid && kill -1 \$(<$TEMPD/killme.pid) )"
}

# == BIND COMMANDS ==
RELOAD='reload(eval "$FZF_DEFAULT_COMMAND")'
BIND=()
for k in "${!KEYBINDINGS[@]}" ; do
  fun="${KEYBINDINGS[$k]}"
  postcmd=""
  [[ " $FIRSTS " == *" $fun "* ]] && postcmd="+first"
  [[ " $NEXTS " == *" $fun "* ]] && postcmd="+down"
  BIND+=( --bind "${k,,}:execute( $SELF $fun {} {q} )$EXECKILLME+$RELOAD$postcmd" )
done

# == FZF ==
export FZF_DEFAULT_COMMAND="$SELF fzflog"
fzflog 2>&1 |
  fzf \
  "${FZFSETTINGS[@]}" "${FZFEXTRAS[@]}" \
  --bind "ctrl-u:clear-query+clear-selection+clear-screen" \
  --bind "ctrl-z:execute( $JJSUBSHELL )+execute-silent( jj --no-pager status )+$RELOAD" \
  --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
# 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
#   but that crashes fzf-0.44.1 when the cursor position is after the new revset length, so we use become().
# * Avoid needless $($SELF...) invocations, these cause significant slowdowns during startup
0707010000000D000041ED0000000000000000000000026791AA7600000000000000000000000000000000000000000000001A00000000jj-fzf-0.25.0/screencasts0707010000000E000081ED0000000000000000000000016791AA760000055C000000000000000000000000000000000000002300000000jj-fzf-0.25.0/screencasts/intro.sh#!/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 ; }
ABSPATHSCRIPT=`readlink -f "$0"`
SCRIPTDIR="${ABSPATHSCRIPT%/*}"


# == functions and setup for screencasts ==
source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*}
# fast_timings

# SCRIPT
make_repo IntroDemo gitdev jjdev
start_asciinema IntroDemo 'jj-fzf' Enter
P  # S; T "jj-fzf"; Enter

# FILTER
X 'JJ-FZF shows and filters the `jj log`, hotkeys are used to run JJ commands'
K Down; S; K Down; P; K Down; S; K Down; P; K Down; P;
X 'The preview on the right side shows commit information and the content diff'
K Up; P; K Up; S; K Up; P; K Up; S; K Up; P;
X 'Type keywords to filter the log'
T 'd'; S; T 'o'; S; T 'm'; S; T 'a'; S; T 'i'; S; T 'n'; S; P
K BSpace 6; P

# OP-LOG
X 'Ctrl+O shows the operation log'
K C-o; P
K Down 11
X 'Ctrl+D and Ctrl+L display diff or log'
K C-d; P
K Up 11
K C-g; P

# COMMIT / DESCRIBE
# REBASE -r
# BOOKMARK + DEL
# PUSH

# HELP
X 'Ctrl+H shows the help for all hotkeys'
K C-h; P
K C-Down 11; P
# T 'g'; S; T 'i'; S; T 't'; S; P; K C-u; P; P; K Down; P; K Down; P; P;
K C-g; P


# EXIT
P
stop_asciinema
render_cast "$ASCIINEMA_SCREENCAST"
#stop_asciinema && render_cast "$ASCIINEMA_SCREENCAST" && exit
0707010000000F000081ED0000000000000000000000016791AA76000010E3000000000000000000000000000000000000002700000000jj-fzf-0.25.0/screencasts/megamerge.sh#!/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 ; }
ABSPATHSCRIPT=`readlink -f "$0"`
SCRIPTDIR="${ABSPATHSCRIPT%/*}"

# == functions and setup for screencasts ==
source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*}
# fast_timings

# CLONE REPO
DIR=MegaMergeDemo
( rm -rf $DIR
  set -x
  git clone --no-hardlinks --single-branch --branch trunk $(cd  $SCRIPTDIR && git rev-parse --git-dir) $DIR
  cd $DIR
  git update-ref refs/remotes/origin/trunk f2c149e
  git tag -d `git tag`
  # git reset --hard f2c149e
  jj git init --colocate
  jj b s trunk -r f2c149e --allow-backwards
  jj bookmark track trunk@origin
  jj new -r f2c149e
  jj b c two-step-duplicate-and-backout -r 7d3dae8
  jj abandon b19d586:: && jj rebase -s bf7fd9d -d f2c149e
  jj b c bug-fixes -r f93824e
  jj abandon 56a3cbb:: && jj rebase -s bed3bcd -d f2c149e
  jj abandon 249a167:: # jj b c screencast-scripts -r 69fd52e
  # jj abandon 4951884:: && jj rebase -s 249a167 -d f2c149e
  jj abandon 5cf1278:: # jj b c readme-screencasts -r 8c3d950
  # jj abandon 66eb19d:: && jj rebase -s 5cf1278 -d f2c149e
  jj b c homebrew-fixes -r c1512f4
  jj abandon 5265ff6::
  jj new @-
)

# SCRIPT
start_asciinema $DIR 'jj-fzf' Enter
X 'The "Mega-Merge" workflow operates on a selection of feature branches'

# FIRST NEW
X 'Use Ctrl+N to create a new commit based on a feature branch'
K PageUp Down; P
K C-n; P
X 'Use Ctrl+D to give the Mega-Merge head a unique marker'
K C-d
T $'= = = = = = = =\n'; P;
K C-x; P		# nano

# ADD PARENTS
X 'Alt+P starts the Parent editor for the selected commit'
K M-p; P
X 'Alt+A and Alt+D toggle between adding and deleting parents'
K M-d; P; K M-a; P; K M-d; P; K M-a; P
X 'Pick branches and use Tab to add parents'
#K Down; K Tab; P	# readme-screencasts
Q "two-step-duplicate-and-backout"; K Tab; P
Q "bug-fixes"; K Tab; P
Q "homebrew-fixes"; K Tab; P
X 'Enter: run `jj rebase` to add the selected parents'
K Enter; P
X 'The working copy now contains 3 feature branches'

# NEW COMMIT
X 'Ctrl+N starts a new commit'
K C-n; P
X 'Ctrl+Z starts a subshell'
K C-z; P
T '(echo; echo "## Multi-merge") >>README.md && exit'; P; K Enter; P
X 'Alt+C starts the text editor and creates a commit'
K M-c; K End; P
T 'start multi-merge section'; P
K C-x; P		# nano

# ADD BRANCH
K PageUp; K Down 2
X 'Alt+N: Insert a new parent (adds a branch to merge commits)'
K M-n; P
Q "\ @\ "
X 'Alt+B: Assign/move a bookmark to a commit'
K M-b; T 'cleanup-readme'; P; K Enter

# REBASE before
K PageUp; P
X 'Alt+R allows rebasing a commit into a feature branch'
K M-r;
X 'Use Alt+R and Ctrl+B to rebase a single revision before another'
K M-r; P
Q "\ @\ "	# "cleanup-readme"
K C-b; P
X 'Enter: rebase with `jj rebase --revisions --insert-before`'
K Enter; P

# SQUASH COMMIT
K PageUp
X 'Ctrl+N starts a new commit'
K C-n; P
X 'Ctrl+Z starts a subshell'
K C-z; P
T '(echo; echo "Alt+P enables the Multi-Merge workflow.") >>README.md && exit'; P; K Enter; P
K C-d End; P
T 'describe Alt+P'; P
K C-x; S		# nano
X 'The working copy changes can be squashed into a branch'
Q "cleanup-readme"; P
X 'Alt+W: squash the contents of the working copy into the selected revision'
K M-w; P; P;
X 'The commit now contains the changes from the working copy'
K PageUp; P;
X 'The working copy is now empty'

# UPSTREAM-MERGE
X "Let's merge the new branch into upstream and linearize history"
Q "cleanup-readme"; P
X 'Alt+M: start merge dialog'
K M-m Down 3; P
X 'Alt+U: upstream merge - add tracked bookmark to merge parents'
K M-u; P
X 'Enter: edit commit message and create upstream merge'
K Enter; P; K C-k; P; K C-x; P      # nano

# REBASE MegaMerge head
K PageUp; K Down 2; P
X 'Alt+R: rebase the Mega-Merge head onto the working copy'
K M-r; P
X "Alt+P: simplify-parents after rebase to remove old parent edges"
K M-p; P
X "Enter: rebase onto 'trunk' and also simplify parents"
K Enter; P

# NEW
K PageUp
X 'Use Ctrl+N to prepare the next commit'
K C-n; P

# OUTRO
X "The new feature can be pushed with 'trunk' and the Mega-Merge head is rebased"
P; P

# EXIT
P
stop_asciinema
render_cast "$ASCIINEMA_SCREENCAST"
ffmpeg -ss 00:02:32 -i megamerge.mp4 -frames:v 1 -q:v 2 -y megamerge230.jpg
07070100000010000081ED0000000000000000000000016791AA7600000697000000000000000000000000000000000000002500000000jj-fzf-0.25.0/screencasts/merging.sh#!/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 ; }
ABSPATHSCRIPT=`readlink -f "$0"`
SCRIPTDIR="${ABSPATHSCRIPT%/*}"


# == functions and setup for screencasts ==
source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*}
# fast_timings

# SCRIPT
make_repo -3tips MergingDemo gitdev jjdev
start_asciinema MergingDemo 'jj-fzf' Enter

# GOTO rev
X 'To create a merge commit, pick the first commit to be merged'
Q0 "trunk"; S

# MERGE-2
X 'Alt+M starts the Merge dialog'
K M-m; P
K 'Down'; K 'Down'
Q "jjdev"
X 'Tab selects another revision to merge with'
K Tab; P
X 'Enter starts the text editor to describe the merge'
K Enter; P
K C-k; P; K C-x; S	# nano
X 'The newly created merge commit is now the working copy'

# UNDO
X 'Alt+Z will undo the last operation (the merge)'
K M-z ; P
X 'The repository is back to 3 unmerged branches'

# MERGE-3
X 'Select a revision to merge'
K Down; K Down; K Down
Q0 "gitdev"; S
X 'Alt+M starts the Merge dialog, now for an octopus merge'
K M-m; P
K Down; Q0 "trunk"; S
K Tab; S
K Down; Q0 "jjdev"; S
X 'Tab again selects the third revision'
K Tab; S
X 'Enter starts the text editor to describe the merge'
K Enter; P
K C-k; P; K C-x; S	# nano
X 'The newly created merge commit is now the working copy'
X 'Ctrl+D starts the text editor to alter the description'
K C-d; P
K C-k 16
T "Merge 'gitdev' and 'jjdev' into 'trunk'"; P
K C-x; S		# nano
X 'This is an Octopus merge, a commit can have any number of parents'
P; P

# EXIT
P
stop_asciinema
render_cast "$ASCIINEMA_SCREENCAST"
07070100000011000081ED0000000000000000000000016791AA7600001CCE000000000000000000000000000000000000002500000000jj-fzf-0.25.0/screencasts/prepare.sh# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0

export JJ_CONFIG=/dev/null # ignore user config
readonly SESSION="$1"
readonly ASCIINEMA_SCREENCAST=$(readlink -f "./$SESSION")
export SESSION ASCIINEMA_SCREENCAST

# == deps ==
for cmd in nano tmux asciinema agg gif2webp gnome-terminal ffmpeg ; do
  command -V $cmd || die "missing command: $cmd"
done
for font in \
  /usr/share/fonts/truetype/firacode/FiraCode-Retina.ttf \
    /usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf
do
  cp $font .
done

# == Aux Funcs ==
# Create temporary dir, assigns $TEMPD
temp_dir()
{
  test -n "${TEMPD:-}" || {
    TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/jj-fzf.pid
    echo "$$" > $TEMPD/$SCRIPTNAME.pid
  }
}

# rtrim, then count chars
crtrim()
(
  V="$*"
  V="${V%"${V##*[![:space:]]}"}"
  echo "${#V}"
)

# == Config + Timings ==
W=138 H=40 Z=0.9
t=0.050		# typing delay
k=0.2000150	# special key delay
s=0.250		# synchronizing delay, dont shorten
p=0.9990750	# user pause
w=1.500025	# info pause

# Use fast timings for debugging
fast_timings()
{
  t=0.01
  #k=0.015
  p=$s
  w=0.05
}

# == screencast commands ==
# type text
T()
{
  txt="$*"
  for (( i=0; i<${#txt}; i++ )); do
    chr="${txt:$i:1}"
    if test "$chr" == ';'; then
      tmux send-keys -t $SESSION -H $(printf %x "'$chr'")
    else
      tmux send-keys -t $SESSION -l "$chr"
    fi
    sleep $t
  done
}
# send key
K()
(
  while test $# -ge 1 ; do
    KEY="$1"; shift
    [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] &&
      { N="$1"; shift; } ||
	N=1
    for (( i=0 ; i<$N; i++ )); do
      tmux send-keys -t $SESSION "$KEY"
      sleep $k
    done
  done
)
Enter() { K "Enter" ; P; }
# synchronize (with other programs)
S()
{ sleep $s ; }
# pause (for user to observe)
P()
{ sleep $p ; }
# kill-line + type-text + kill-line
Q()
{ K C-U; T "$*"; K C-U; S; }	# fzf-query + Ctrl+U
# Q without delays
Q0()
{ tmux send-keys -t $SESSION C-U; tmux send-keys -t $SESSION -l "$*"; tmux send-keys -t $SESSION C-U; }
# Ctrl-Alt-X type-text Ctrl-g
X()
{
  K C-M-x ;
  (export t=$(echo "$t / 2" | bc -l) ; T "$*        ")
  sleep $(echo "`crtrim "$*"` * $w / 50" | bc -l)
  P ; K C-g ; S
}

# Find PID of asciinema for the current $SESSION
find_asciinema_pid()
{
  ps --no-headers -ao pid,comm,args |
    awk "/asci[i]nema rec.*\\<$SESSION\\>/{ print \$1 }"
}
# Start recording with asciinema in a dedicated terminal, using $W x $H, etc
start_asciinema()
{
  DIR="$(readlink -f "${1:-.}")" ; shift
  temp_dir
  # Simplify nano exit to Ctrl+X without 'y' confirmation
  echo -e "set saveonexit"							>  $TEMPD/nanorc
  echo "unset HISTFILE"					       			>  $TEMPD/bashrc
  echo "PS1='\[\033[01;34m\]\W\[\033[00m\]\$ '"					>> $TEMPD/bashrc
  echo "export EDITOR='/usr/bin/env nano --rcfile $TEMPD/nanorc'"		>> $TEMPD/bashrc
  echo "export JJFZF_SHELL='/usr/bin/env bash --rcfile $TEMPD/bashrc -i'"	>> $TEMPD/bashrc
  # stert new screencast session
  tmux kill-session -t $SESSION 2>/dev/null || :
  ( cd "$DIR"
    export JJ_CONFIG=/dev/null
    tmux new-session -P -d -x $W -y $H -s $SESSION
  ) >$TEMPD/session
  echo "tmux-session: $SESSION"
  tmux set-option -t $SESSION status off
  tmux send-keys -t $SESSION "source $TEMPD/bashrc"$'\n' ; sleep 0.1
  tmux resize-window -t $SESSION -x $W -y $H ; sleep 0.1
  tmux send-keys -t $SESSION $'clear\n' ; sleep 0.1
  while [ $# -gt 0 ] ; do
    tmux send-keys -t $SESSION "$1"
    shift
  done
  sleep 0.1
  gnome-terminal --geometry $W"x"$H -t $SESSION --zoom $Z  -- \
		 asciinema rec --overwrite "$ASCIINEMA_SCREENCAST.cast" -c "tmux attach-session -t $SESSION -f read-only"
  while test -z "$(find_asciinema_pid)" ; do
    sleep 0.2 # dont save PID, this might be an early pid still forking
  done
}
# Stop recording
stop_asciinema()
(
  set -Eeuo pipefail -x
  PID=$(find_asciinema_pid)	# PID=$(tmux list-panes -t $SESSION -F '#{pane_pid}')
  kill -9 $PID	# abort asciinema, so last frame is preserved
  tmux kill-session -t $SESSION
)
# Stop recording and render screencast output files
render_cast()
(
  set -Eeuo pipefail # -x
  SCREENCAST="$1"
  test -r "$SCREENCAST.cast" || die "missing file: $SCREENCAST.cast"
  # sed '$,/"\[exited]/d' "$SCREENCAST.cast"
  # --font-family "DejaVu Sans Mono" --idle-time-limit 1 --fps-cap 60 --renderer resvg
  # asciinema-agg
  agg \
    --theme asciinema --speed 1 \
    --font-family "Fira Code Retina" \
    --font-dir $PWD --font-size 16 \
    "$SCREENCAST.cast" "$SCREENCAST.gif"
  ( set -x
    # -preset slower -preset veryslow -x264opts opencl
    time ffmpeg -hwaccel auto -i "$SCREENCAST.gif" \
	 -c:v libx264 -crf 24 -tune animation -preset placebo \
	 -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
         -y "$SCREENCAST.mp4" &
    gif2webp "$SCREENCAST.gif" -min_size -metadata all -o "$SCREENCAST.webp" &
    wait
  )
  ls -l "$SCREENCAST"*
  command -V notify-send 2>/dev/null && notify-send -e -i system-run -t 5000 "Screencast ready: $SCREENCAST"
  true
)

# == repo commands ==
# Usage: make_repo [-quitstage] [repo] [brancha] [branchb]
make_repo()
(
  [[ "${1:-}" =~ ^- ]] && { DONE="${1:1}"; shift; } || DONE=___
  R="${1:-repo0}"
  A="${2:-deva}"
  B="${3:-devb}"

  rm -rf $R/
  mkdir $R
  ( # set -x
    cd $R
    git init -b trunk
    echo -e "# $R\n\nHello Git World" > README
    git add README && git commit -m "README: hello git world"
    G=`git log -1 --pretty=%h`
    [[ $DONE =~ root ]] && exit

    git switch -C $A
    echo -e "Git was here" > git-here.txt
    git add git-here.txt && git commit -m "git-here.txt: Git was here"
    echo -e "\n## Copying Restricted\n\nCopying prohibited." >> README
    git add README && git commit -m "README: copying restricted"
    L=`git log -1 --pretty=%h`   # L=`jj log --no-graph -T change_id -r @-`
    echo -e "Two times" >> git-here.txt
    git add git-here.txt && git commit -m "git-here.txt: two times"
    [[ $DONE =~ $A ]] && exit

    jj git init --colocate
    jj new $G
    sed -r "s/Git/JJ/" -i README
    jj commit -m "README: jj repo"
    echo -e "\n## Public Domain\n\nDedicated to the Public Domain under the Unlicense: https://unlicense.org/UNLICENSE" >> README
    jj commit -m "README: public domain license"
    echo -e "JJ was here" > jj-here.txt
    jj file track jj-here.txt && jj commit -m "jj-here.txt: JJ was here"
    jj bookmark set $B -r @-
    [[ $DONE =~ $B ]] && exit

    jj new trunk
    echo -e "---\ntitle: Repo README\n---\n\n" > x && sed '0rx' -i README && rm x
    jj commit -m "README: yaml front-matter"

    [[ $DONE =~ 3tips ]] && jj abandon -r $L # allow conflict-free merge of 3tips
    sed '/title:/i Date: today' -i README
    jj commit -m "README: add date to front-matter"
    jj bookmark set trunk --allow-backwards -r @-
    [[ $DONE =~ 3tips ]] && exit

    jj new $A $B -m "Merging '$A' and '$B'"
    M1=`jj log --no-graph -T change_id -r @`
    [[ $DONE =~ merged ]] && exit

    jj backout -r $L -d @ && jj edit @+ && jj rebase -r @ --insert-after $A-
    jj rebase -b trunk -d @
    [[ $DONE =~ backout ]] && exit

    jj new trunk $M1 -m "Merge into trunk"
    [[ $DONE =~ squashall ]] && (
      EDITOR=/bin/true jj squash --from 'root()+::@-' --to @ -m ""
      jj bookmark delete trunk gitdev jjdev
    )

    true
  )

  ls -ald $R/*
)
07070100000012000081ED0000000000000000000000016791AA7600000BC8000000000000000000000000000000000000002600000000jj-fzf-0.25.0/screencasts/rebasing.sh#!/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 ; }
ABSPATHSCRIPT=`readlink -f "$0"`
SCRIPTDIR="${ABSPATHSCRIPT%/*}"

# == functions and setup for screencasts ==
source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*}
# fast_timings

# CLONE REPO
( rm -rf dest
  git clone --no-hardlinks --single-branch --branch trunk $(cd  $SCRIPTDIR && git rev-parse --git-dir) dest
  cd dest
  git update-ref refs/remotes/origin/trunk 97d796b
  git reset --hard 5265ff6
  jj git init --colocate
  jj b s trunk -r 97d796b --allow-backwards
  jj new -r f2c149e
  jj abandon 5265ff6
  jj b c splittingdemo -r 9325d16
  jj b c diffedit -r 685fd50
  jj b c homebrew-fixes -r c1512f4
  jj rebase -r splittingdemo -d f3b860c # -> -A -B 685fd50
  jj rebase -s homebrew-fixes-  -d 8f18758
  jj new @-
)

# SCRIPT
start_asciinema dest 'jj-fzf' Enter

# REBASE -r -A
X 'To rebase commits, navigate to the target revision'
K Down 10; P	# splittingdemo
X 'Alt+R starts the Rebase dialog'
K M-r; P
X 'Alt+B: --branch  Alt+R: --revisions  Alt+S: --source'
K M-b; P; K M-s; P; K M-r; P; K M-b; P; K M-s; P; K M-r; P
X 'Select destination revision'
K Down 3; P	# diffedit
X 'Ctrl+A: --insert-after  Ctrl+B: --insert-before  Ctrl+D: --destination'
K C-b; P; K C-a; P; K C-d; P; K C-b; P; K C-a; P
X 'Enter: run `jj rebase` to rebase with --revisions --insert-after'
K Enter; P
X 'Revision "splittingdemo" was inserted *after* "diffedit"'
P; P

# UNDO
X 'To start over, Alt+Z will undo the last rebase'
K M-z; P
P; P

# REBASE -r -B
X 'Alt+R starts the Rebase dialog'
K M-r; P
X 'Alt+B: --branch  Alt+R: --revisions  Alt+S: --source'
K M-b; P; K M-s; P; K M-r; P; K M-b; P; K M-s; P; K M-r; P
X 'Select destination revision'
K Down 3; P	# diffedit
X 'Ctrl+A: --insert-after  Ctrl+B: --insert-before  Ctrl+D: --destination'
K C-a; P; K C-b; P; K C-d; P; K C-a; P; K C-b; P
X 'Enter: run `jj rebase` to rebase with --revisions --insert-before'
K Enter; P
X 'Revision "splittingdemo" was inserted *before* "diffedit"'
P; P

# REBASE -b -d
X 'Select the "homebrew-fixes" bookmark to rebase'
K Down 7; P	# homebrew-fixes
X 'Alt+R starts the Rebase dialog'
K M-r; P
X 'Keep `jj rebase --branch --destination` at its default'
K Down; P	# @-
X 'Enter: rebase "homebrew-fixes" onto HEAD@git'
K Enter PageUp; P
X 'The "homebrew-fixes" branch was moved on top of HEAD@git'
P; P

# REBASE -s -d
X 'Or, select a "homebrew-fixes" ancestry commit to rebase'
K PageUp; K Down; P	# homebrew-fixes-
X 'Alt+R starts the Rebase dialog'
K M-r; P
X 'Use Alt+S for `jj rebase --source --destination` to rebase a subtree'
K Down 9; P	# @-
K M-s; P
X 'Enter: rebase the "homebrew-fixes" subtree onto "merge-commit-screencast"'
K Enter; P
K Down 7; P
X 'The rebase now moved the "homebrew-fixes" parent commit and its descendants'
P; P

# EXIT
P
stop_asciinema
render_cast "$ASCIINEMA_SCREENCAST"
07070100000013000081ED0000000000000000000000016791AA760000087B000000000000000000000000000000000000002700000000jj-fzf-0.25.0/screencasts/splitting.sh#!/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 ; }
ABSPATHSCRIPT=`readlink -f "$0"`
SCRIPTDIR="${ABSPATHSCRIPT%/*}"


# == functions and setup for screencasts ==
source $SCRIPTDIR/prepare.sh ${SCRIPTNAME%%.*}
# fast_timings

# SCRIPT
make_repo -squashall SplittingDemo gitdev jjdev
start_asciinema SplittingDemo 'jj-fzf' Enter

X 'When the working copy has lots of changes in lots of files...'

# SPLIT FILES
X 'Alt+F can split the current revision into one commit per file'
K M-f; P
K Down; P; K Down; P; K Down; P
K Up  ; P; K Up  ; P; K Up  ; P

# DESCRIBE
K Down; P
X 'Ctrl+D opens the text editor to describe the commit'
K C-d; S; K End; P
T 'marker left by jj'; P
K C-x; P	# nano

# ABANDON
K Down; P
X 'Alt+A abandons a commit'
K M-a; P

# SPLIT INTERACTIVELY
K Home; P
X 'Alt+I starts `jj split` interactively'
X 'Use Mouse Clicks to explore the interactive editor'
K M-i
P
tmux send-keys -H 1b 5b 4d 20 24 21    1b 5b 4d 23 24 21   # FILE
P
tmux send-keys -H 1b 5b 4d 20 2a 21    1b 5b 4d 23 2a 21   # EDIT
P
tmux send-keys -H 1b 5b 4d 20 32 21    1b 5b 4d 23 32 21  # SELECT
P
tmux send-keys -H 1b 5b 4d 20 3a 21    1b 5b 4d 23 3a 21   # VIEW
P
tmux send-keys -H 1b 5b 4d 20 3a 21    1b 5b 4d 23 3a 21   # VIEW (hides)
P
T 'F'; P
K Down
K Down
K Enter
K Enter
K Enter
K Enter
K Enter
K Enter; P
T 'ac'; P
X 'With the diff split up, each commit can be treated individually'

# DESCRIBE
K Down; P
K C-d; S; K End; P
T 'add brief description'; P
K C-x; P	# nano

# DESCRIBE
K Up; P
K C-d; S; K End; P
T 'add front-matter + date'; P
K C-x; P	# nano

# DIFF-EDIT
X 'Alt+E starts `jj diffedit` to select diff hunks to keep'
K M-e; P;
K F; K a; K Down 3; K Space; P
K c; P
K C-d; S; K End; P
K BSpace 7; P
K C-x; P	# nano

# UNDO
X 'Or, use Alt+Z Alt+Z to undo the last 2 steps and keep the old front-matter'
K M-z ; P
K M-z ; P

# NEW
K Home
X 'Create a new, empty change with Ctrl+N to edit the next commit'
K C-n; P

# EXIT
P
stop_asciinema
render_cast "$ASCIINEMA_SCREENCAST"
07070100000014000041ED0000000000000000000000026791AA7600000000000000000000000000000000000000000000001400000000jj-fzf-0.25.0/tests07070100000015000081ED0000000000000000000000016791AA7600000AA9000000000000000000000000000000000000001E00000000jj-fzf-0.25.0/tests/basics.sh#!/usr/bin/env bash
# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
set -Eeuo pipefail #-x
SCRIPTNAME="${0##*/}" && SCRIPTDIR="$(readlink -f "$0")" && SCRIPTDIR="${SCRIPTDIR%/*}"

source $SCRIPTDIR/utils.sh

# == TESTS ==
test-functions-fail-early()
(
  cd_new_repo
  # Check `jj-fzf describe` does not continue with $EDITOR
  # once an invalid change_id has been encountered.
  export JJ_CONFIG='' EDITOR='echo ERRORINERROR'
  OUT="$(set +x; jj-fzf describe 'zzzzaaaa' 2>&1)" && E=$? || E=$?
  assert_nonzero $E
  assert1error "$OUT"
  ! grep -Eq 'ERRORINERROR' <<<"$OUT" ||
    die "${FUNCNAME[0]}: detected nested invocation, output:"$'\n'"$(echo "$OUT" | sed 's/^/> /')"
)
TESTS+=( test-functions-fail-early )

test-edit-new()
(
  cd_new_repo
  mkcommits 'Ia' 'Ib' 'Ia ->Ic' 'Ib|Ic ->Id'
  assert_commit_count $((2 + 4))
  git tag IMMUTABLE `get_commit_id Id` && jj_status
  assert_commit_count $((2 + 5))
  mkcommits A B 'A ->C' 'B|C ->D'
  assert_commit_count $((2 + 5 + 4))
  jj-fzf edit 'C' >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 4))
  assert_@ `get_commit_id C` && assert_@- `get_commit_id A`
  jj-fzf edit 'Ic' >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 4 + 1))
  assert_@- `get_commit_id Ic`
  jj-fzf new '@' >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 4 + 1 + 1))
  assert_commits_eq @-- `get_commit_id Ic`
)
TESTS+=( test-edit-new )

test-undo-undo-redo()
(
  cd_new_repo
  mkcommits A B 'A ->C' 'B|C ->D' E
  assert_commit_count $((2 + 5))
  ( jj new -m U1 && jj new -m U2 && jj new -m U3 ) >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2`
  jj-fzf undo >$DEVERR 2>&1 && assert_commit_count $((2 + 5 + 2))
  jj-fzf undo >$DEVERR 2>&1 && assert_commit_count $((2 + 5 + 1))
  assert_@ `get_commit_id U1` && assert_@- `get_commit_id E`
  jj new >$DEVERR 2>&1 # resets undo pointer
  assert_commit_count $((2 + 5 + 1 + 1))
  jj-fzf undo >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 1)) && assert_@ `get_commit_id U1` && assert_@- `get_commit_id E`
  jj-fzf undo >$DEVERR 2>&1
  jj-fzf undo >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 3))
  assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2`
  jj-fzf undo >$DEVERR 2>&1
  jj-fzf undo >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 1)) && assert_@ `get_commit_id U1` && assert_@- `get_commit_id E`
  jj-fzf undo-reset >$DEVERR 2>&1 # resets undo pointer
  jj-fzf undo >$DEVERR 2>&1
  jj-fzf undo >$DEVERR 2>&1
  assert_commit_count $((2 + 5 + 3)) && assert_@ `get_commit_id U3` && assert_@- `get_commit_id U2`
)
TESTS+=( test-undo-undo-redo )

# == RUN ==
temp_dir
for TEST in "${TESTS[@]}" ; do
  $TEST
  printf '  %-7s %s\n' OK "$TEST"
done
tear_down
07070100000016000081A40000000000000000000000016791AA7600000F62000000000000000000000000000000000000001D00000000jj-fzf-0.25.0/tests/utils.sh# This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0

# == Check Dependencies ==
jj --help >/dev/null
PATH="$SCRIPTDIR/..:$PATH"	# ensure jj-fzf is in $PATH
jj-fzf --help >/dev/null

# == VARIABLE Setup ==
export JJ_FZF_ERROR_DELAY=0 # instant errors for testing
TEMPD=

# == OPTIONS ==
DEVERR=/dev/null
[[ " $* " =~ -x ]] && {
  PS4="+ \${BASH_SOURCE[0]##*/}:\${LINENO}: "
  DEVERR=/dev/stderr
  set -x
}
[[ " $* " =~ -v ]] &&
  DEVERR=/dev/stderr

# == Utils ==
die()
{
  local R=$'\033[31m' Z=$'\033[0m'
  [ -n "$*" ] &&
    echo "${BASH_SOURCE[1]}:${BASH_LINENO[0]}:${FUNCNAME[1]}: $R**ERROR**:$Z ${*:-aborting}" >&2;
  exit 127
}
die-() # die, using *caller* as origin
{
  local R=$'\033[31m' Z=$'\033[0m'
  [ -n "$*" ] &&
    echo "${BASH_SOURCE[2]}:${BASH_LINENO[1]}:${FUNCNAME[2]}: $R**ERROR**:$Z ${*:-aborting}" >&2;
  exit 127
}
temp_dir()
{
  test -n "$TEMPD" || {
    TEMPD="`mktemp --tmpdir -d jjfzf0XXXXXX`" || die "mktemp failed"
    trap "rm -rf '$TEMPD'" 0 HUP INT QUIT TRAP USR1 PIPE TERM
    echo "$$" > $TEMPD/jjfzf-tests.pid
  }
}

# == Repository ==
tear_down()
(
  REPO="${1:-repo}"
  test -n "$TEMPD" &&
    rm -rf $TEMPD/$REPO
)
clear_repo()
(
  REPO="${1:-repo}"
  test -n "$TEMPD" || die "missing TEMPD"
  cd $TEMPD/
  rm -rf $TEMPD/$REPO
  mkdir $TEMPD/$REPO
  cd $TEMPD/$REPO
  git init >$DEVERR 2>&1
  jj git init --colocate >$DEVERR 2>&1
  echo "$PWD"
)
cd_new_repo()
{
  RP=$(clear_repo "$@")
  cd "$RP"
}
mkcommits()
( # Create empty test commits with bookamrks
  while test $# -ne 0 ; do
    P=@ && [[ "$1" =~ (.+)-\>(.+) ]] &&
      P="${BASH_REMATCH[1]}" C="${BASH_REMATCH[2]}" || C="$1"
    shift
    jj --no-pager new -m="$C" -r all:"$P"
    jj bookmark set -r @ "$C"
  done >$DEVERR 2>&1		# mkcommits A B 'A|B ->C'
)
get_commit_id()
(
  REF="$1"
  COMMIT_ID=$(jj --ignore-working-copy log --no-graph -T commit_id -r "description(exact:\"$REF\n\")" 2>/dev/null) &&
    test -n "$COMMIT_ID" ||
      COMMIT_ID=$(jj --ignore-working-copy log --no-graph -T commit_id -r "$REF") || exit
  echo "$COMMIT_ID"
)
get_change_id()
(
  COMMIT_ID=$(get_commit_id "$@")
  UNIQUECHANGE='if(self.divergent(), "", change_id)'
  # only allow non-divergent: https://martinvonz.github.io/jj/latest/FAQ/#how-do-i-deal-with-divergent-changes-after-the-change-id
  CHANGE_ID=$(jj --ignore-working-copy log --no-graph -T "$UNIQUECHANGE" -r " $COMMIT_ID ") || exit
  echo "$CHANGE_ID"
)
commit_count()
(
  R="${1:-::}"
  jj --ignore-working-copy log --no-graph -T '"\n"' -r "$R" | wc -l
)
jj_log()
(
  jj --ignore-working-copy log -T builtin_log_oneline -r ::
)
jj_status()
(
  jj status >$DEVERR 2>&1
)

# == Assertions ==
assert_commit_count()
(
  V="$1"
  C="$(commit_count "${2:-::}")"
  test "$C" -eq "$V" ||
    die- "assert_commit_count: mismatch: $C == $V"
)
assert_@()
(
  V="$1"
  C="$(get_change_id '@')"
  test "$C" == "$V" && return
  C="$(get_commit_id '@')"
  test "$C" == "$V" && return
  die- "assert_@: mismatch: $C == $V"
)
assert_@-()
(
  V="$1"
  C="$(get_change_id '@-')"
  test "$C" == "$V" && return
  C="$(get_commit_id '@-')"
  test "$C" == "$V" && return
  die- "assert_@-: mismatch: $C == $V"
)
assert_commits_eq()
(
  U="$1"
  V="$2"
  C="$(get_commit_id "$U")"
  D="$(get_commit_id "$V")"
  test "$C" == "$D" ||
    die- "assert_commits_eq: mismatch: $C == $D"
)
assert_nonzero()
{
  V="$1"
  test 0 != "$V" ||
    die- "assert_nonzero: mismatch: 0 != $V"
}
assert_zero()
{
  V="$1"
  test 0 == "$V" ||
    die- "assert_zero: mismatch: 0 == $V"
}
assert0error()
{
  ! grep -Eq '\bERROR:' <<<"$*" ||
    die- "assert0error: unexpected ERROR message: $*"
}
assert1error()
{
  grep -Eq '\bERROR:' <<<"$*" ||
    die- "assert1error: missing mandatory ERROR message: $*"
}

# == Errors ==
bash_error()
{
  local code="$?" D=$'\033[2m' Z=$'\033[0m'
  echo "$D${BASH_SOURCE[1]}:${BASH_LINENO[0]}:${FUNCNAME[1]}:trap: exit status: $code$Z" >&2
  exit "$code"
}
trap 'bash_error' ERR
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!279 blocks
openSUSE Build Service is sponsored by