#!/usr/bin/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/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
