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 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:** [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:** [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:** [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:** [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