diff --git a/LICENSE b/LICENSE index e508978a..ab428e27 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2017-2021 Wenxuan Zhang +Copyright © 2017-2026 Wenxuan Zhang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal diff --git a/README.md b/README.md index f888f0b4..12016674 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,12 @@ If you're having issues after updating, and commands such as `forgit::add` or al - **Interactive `git commit --fixup=reword && git rebase -i --autosquash` selector** (`grw`) +- **Interactive `git worktree` selector** (`gwt`) + + Select a worktree and `cd` into it. This command can only be used via the alias as it needs to change the current shell's working directory. + +- **Interactive `git worktree remove` selector** (`gwd`) + # ⌨ Keybindings | Key | Action | @@ -189,14 +195,18 @@ If you're having issues after updating, and commands such as `forgit::add` or al | Alt - W | Toggle preview wrap | | Ctrl - S | Toggle sort | | Ctrl - R | Toggle selection | -| Ctrl - Y | Copy commit hash/stash ID* | +| Ctrl - Y | Copy commit hash/stash ID/worktree path1 | | Ctrl - K / P | Selection move up | | Ctrl - J / N | Selection move down | | Alt - K / P | Preview move up | | Alt - J / N | Preview move down | | Alt - E | Open file in default editor (when possible) | +| Alt - L | Toggle worktree lock/unlock2 | + +1 Available when the selection contains a commit hash, stash ID, or worktree path. + +2 Only available in the worktree browser (`gwt`). -\* Available when the selection contains a commit hash or a stash ID. For Linux users `FORGIT_COPY_CMD` should be set to make copy work. Example: `FORGIT_COPY_CMD='xclip -selection clipboard'`. # ⚙ Options @@ -240,6 +250,8 @@ forgit_blame=gbl forgit_fixup=gfu forgit_squash=gsq forgit_reword=grw +forgit_worktree=gwt +forgit_worktree_delete=gwd ``` ## git integration @@ -302,6 +314,7 @@ These are passed to the according `git` calls. | `gsq` | `FORGIT_SQUASH_GIT_OPTS` | | `grw` | `FORGIT_REWORD_GIT_OPTS` | | `gcp` | `FORGIT_CHERRY_PICK_GIT_OPTS` | +| `gwd` | `FORGIT_WORKTREE_DELETE_GIT_OPTS` | ## pagers @@ -362,6 +375,8 @@ Customizing fzf options for each command individually is also supported: | `gsq` | `FORGIT_SQUASH_FZF_OPTS` | | `grw` | `FORGIT_REWORD_FZF_OPTS` | | `gcp` | `FORGIT_CHERRY_PICK_FZF_OPTS` | +| `gwt` | `FORGIT_WORKTREE_FZF_OPTS` | +| `gwd` | `FORGIT_WORKTREE_DELETE_FZF_OPTS` | Complete loading order of fzf options is: diff --git a/bin/git-forgit b/bin/git-forgit index f54845f2..1b252c48 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -31,9 +31,12 @@ $FORGIT_FZF_DEFAULT_OPTS _forgit_warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; } _forgit_info() { printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$@" >&2; } -_forgit_inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null; } +_forgit_inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null 2>&1; } +_forgit_inside_git_dir() { git rev-parse --is-inside-git-dir >/dev/null 2>&1; } +_forgit_inside_git_repo() { _forgit_inside_work_tree || _forgit_inside_git_dir; } # tac is not available on OSX, tail -r is not available on Linux, so we use either of them _forgit_reverse_lines() { tac 2> /dev/null || tail -r; } +_forgit_strip_ansi() { local ESC=$'\033'; sed "s/${ESC}\[[0-9;]*m//g"; } _forgit_previous_commit() { # "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead @@ -151,6 +154,33 @@ _forgit_is_file_tracked() { git ls-files "$1" --error-unmatch &> /dev/null } +# List branches with current branch first (for use as fzf header) +# Usage: _forgit_branch_list [git-branch-options...] +# +# Note: We explicitly print the current branch first rather than using +# `LC_ALL=C sort -k1.1,1.1 -rs` because git branch output has three possible +# prefixes: '*' (current), '+' (checked out in a worktree), and ' ' (other). +# Sorting by the first character doesn't reliably place '*' first when '+' +# is present, since their ASCII order (* < +) conflicts with the desired order. +_forgit_branch_list() { + local current + current=$(git branch --show-current) + if [[ -n "$current" ]]; then + printf '\e[90m%s\e[0m\n' "* $current" + else + printf '\e[90m%s\e[0m\n' "* (HEAD detached at $(git rev-parse --short HEAD))" + fi + git branch --color=always "$@" | grep -v '^\*' | grep -v ' -> ' +} + +# Extract branch name from git branch output +# Handles ANSI escape codes, prefix characters (* + ' '), and symbolic refs (->) +_forgit_extract_branch_name() { + _forgit_strip_ansi | + sed -E 's/^[*+ ] //; s/ -> .*//' | + awk '{print $1}' +} + _forgit_list_files() { local rootdir rootdir=$(git rev-parse --show-toplevel) @@ -686,7 +716,9 @@ _forgit_cherry_pick() { } _forgit_cherry_pick_from_branch_preview() { - git log --right-only --color=always --cherry-pick --oneline "$1"..."$2" + local branch + branch=$(echo "$2" | _forgit_extract_branch_name) + git log --right-only --color=always --cherry-pick --oneline "$1"..."$branch" } _forgit_cherry_pick_from_branch() { @@ -703,7 +735,7 @@ _forgit_cherry_pick_from_branch() { opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --header-lines=1 - --preview=\"$FORGIT cherry_pick_from_branch_preview '$base' {1}\" + --preview=\"$FORGIT cherry_pick_from_branch_preview '$base' {}\" $FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS " # loop until either the branch selector is closed or a commit to be cherry @@ -711,10 +743,7 @@ _forgit_cherry_pick_from_branch() { while true do if [[ -z $input_branch ]]; then - branch="$(git branch --color=always --all | - LC_ALL=C sort -k1.1,1.1 -rs | - FZF_DEFAULT_OPTS="$opts" fzf | - awk '{print $1}')" + branch="$(_forgit_branch_list --all | FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_branch_name)" else branch=$input_branch fi @@ -888,13 +917,13 @@ _forgit_checkout_branch() { opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --header-lines=1 - --preview=\"$FORGIT branch_preview {1}\" + --preview=\"$FORGIT branch_preview {}\" $FORGIT_CHECKOUT_BRANCH_FZF_OPTS " _forgit_checkout_branch_branch_git_opts=() _forgit_parse_array _forgit_checkout_branch_branch_git_opts "$FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS" - branch="$(git branch --color=always "${_forgit_checkout_branch_branch_git_opts[@]:---all}" | LC_ALL=C sort -k1.1,1.1 -rs | - FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')" + branch="$(_forgit_branch_list "${_forgit_checkout_branch_branch_git_opts[@]:---all}" | + FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_branch_name)" [[ -z "$branch" ]] && return 1 # track the remote branch if possible @@ -934,13 +963,13 @@ _forgit_switch_branch() { opts=" $FORGIT_FZF_DEFAULT_OPTS +s +m --tiebreak=index --header-lines=1 - --preview=\"$FORGIT branch_preview {1}\" + --preview=\"$FORGIT branch_preview {}\" $FORGIT_SWITCH_BRANCH_FZF_OPTS " _forgit_switch_branch_branch_git_opts=() _forgit_parse_array _forgit_switch_branch_branch_git_opts "$FORGIT_SWITCH_BRANCH_BRANCH_GIT_OPTS" - branch="$(git branch --color=always "${_forgit_switch_branch_branch_git_opts[@]:---all}" | LC_ALL=C sort -k1.1,1.1 -rs | - FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')" + branch="$(_forgit_branch_list "${_forgit_switch_branch_branch_git_opts[@]:---all}" | + FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_branch_name)" [[ -z "$branch" ]] && return 1 # track the remote branch if possible @@ -1012,9 +1041,11 @@ _forgit_checkout_commit() { } _forgit_branch_preview() { + local branch + branch=$(echo "$1" | _forgit_extract_branch_name) # the trailing '--' ensures that this works for branches that have a name # that is identical to a file - git log "$1" "${_forgit_log_preview_options[@]}" -- + git log "$branch" "${_forgit_log_preview_options[@]}" -- } _forgit_git_branch_delete() { @@ -1031,14 +1062,10 @@ _forgit_branch_delete() { opts=" $FORGIT_FZF_DEFAULT_OPTS +s --multi --tiebreak=index --header-lines=1 - --preview=\"$FORGIT branch_preview {1}\" + --preview=\"$FORGIT branch_preview {}\" $FORGIT_BRANCH_DELETE_FZF_OPTS " - - for branch in $(git branch --color=always | - LC_ALL=C sort -k1.1,1.1 -rs | - FZF_DEFAULT_OPTS="$opts" fzf | - awk '{print $1}') + for branch in $(_forgit_branch_list | FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_branch_name) do _forgit_git_branch_delete "$branch" done @@ -1234,6 +1261,146 @@ _forgit_paths_list() { find "$path" -name "*$ext" -print |sed -e "s#$ext\$##" -e 's#.*/##' -e '/^$/d' | sort -fu } +# Parse git worktree list --porcelain output and format it for display +# Output format: [XY] /path/to/worktree (branch) 3 hours ago +# X: '*' = current worktree, ' ' = other +# Y: 'L' (yellow) = locked, 'P' (yellow) = prunable, ' ' = normal +# When both locked and prunable, 'L' takes precedence +_forgit_worktree_list() { + local worktree head branch locked prunable line relative_date + local _cyan=$'\033[36m' _gray=$'\033[90m' _yellow=$'\033[33m' _reset=$'\033[0m' + local current_worktree + current_worktree=$(git rev-parse --show-toplevel 2>/dev/null) + git worktree list --porcelain | while IFS= read -r line; do + case "$line" in + "worktree "*) + worktree="${line#worktree }" + head="" branch="" locked="" prunable="" + ;; + "HEAD "*) + head="${line#HEAD }" + ;; + "branch "*) + branch="${line#branch refs/heads/}" + ;; + "detached") + branch="detached" + ;; + "locked"*) + locked="${_yellow}L${_reset}" + ;; + "prunable"*) + prunable="${_yellow}P${_reset}" + ;; + "") + relative_date=$(git log -1 --format='%cr' "$head" 2>/dev/null) + local current_marker=" " lock_marker=" " + [[ "$worktree" == "$current_worktree" ]] && current_marker="*" + [[ -n "$prunable" ]] && lock_marker="$prunable" + [[ -n "$locked" ]] && lock_marker="$locked" + printf "[%s%s] %s ${_cyan}(%s)${_reset} ${_gray}%s${_reset}\n" \ + "$current_marker" "$lock_marker" "$worktree" "${branch:-HEAD}" "$relative_date" + ;; + esac + done +} + +# Return deletable worktrees (exclude main worktree which is the first one) +_forgit_worktree_list_deletable() { + _forgit_worktree_list | tail -n +2 +} + +# Extract worktree path from formatted line (strip ANSI codes, skip 5-char prefix '[XY] ') +# TODO: awk '{print $1}' breaks on paths containing spaces or parentheses. +# Consider switching _forgit_worktree_list to a tab-delimited format so we can +# use 'cut -f1' (or awk -F'\t') for reliable path extraction. +_forgit_extract_worktree_path() { + _forgit_strip_ansi | cut -c6- | awk '{print $1}' +} + +# Copy worktree path to clipboard +_forgit_worktree_yank_path() { + echo "$1" | _forgit_extract_worktree_path | ${FORGIT_COPY_CMD:-pbcopy} +} + +# Toggle worktree lock status (check 3rd char 'L' in prefix '[XY]') +_forgit_worktree_toggle_lock() { + local line="$1" worktree stripped + worktree=$(echo "$line" | _forgit_extract_worktree_path) + stripped=$(echo "$line" | _forgit_strip_ansi) + if [[ "${stripped:2:1}" == "L" ]]; then + git worktree unlock "$worktree" + else + git worktree lock "$worktree" + fi +} + +# Preview function for worktree +_forgit_worktree_preview() { + local worktree + worktree=$(echo "$1" | _forgit_extract_worktree_path) + [[ ! -d "$worktree" ]] && echo "Worktree directory not found: $worktree" && return 1 + + local status_output + status_output=$(git -c color.status=always -C "$worktree" status -s 2>/dev/null) + [[ -n "$status_output" ]] && echo "$status_output" && echo "" + git -C "$worktree" log --oneline -n 200 --color=always 2>/dev/null +} + +# Git worktree delete wrapper +_forgit_git_worktree_delete() { + _forgit_worktree_delete_git_opts=() + _forgit_parse_array _forgit_worktree_delete_git_opts "$FORGIT_WORKTREE_DELETE_GIT_OPTS" + git worktree remove "${_forgit_worktree_delete_git_opts[@]}" "$@" +} + +# git worktree browser +# Note: we intentionally do NOT use --header-lines=1 here, because the current +# worktree (listed first) should remain selectable for operations like lock/unlock. +_forgit_worktree() { + _forgit_inside_git_repo || return 1 + local opts worktree + [[ $# -ne 0 ]] && { git worktree "$@"; return $?; } + + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +s +m --tiebreak=index + --preview=\"$FORGIT worktree_preview {}\" + --bind=\"ctrl-y:execute-silent($FORGIT worktree_yank_path {})\" + --bind=\"alt-l:execute-silent($FORGIT worktree_toggle_lock {})+reload($FORGIT worktree_list)\" + $FORGIT_WORKTREE_FZF_OPTS + " + worktree=$(_forgit_worktree_list | FZF_DEFAULT_OPTS="$opts" fzf) + [[ -z "$worktree" ]] && return 1 + echo "$worktree" | _forgit_extract_worktree_path +} + +# git worktree delete selector +_forgit_worktree_delete() { + _forgit_inside_git_repo || return 1 + local opts worktrees + [[ $# -ne 0 ]] && { _forgit_git_worktree_delete "$@"; return $?; } + + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +s --multi --tiebreak=index + --preview=\"$FORGIT worktree_preview {}\" + --bind=\"ctrl-y:execute-silent($FORGIT worktree_yank_path {})\" + $FORGIT_WORKTREE_DELETE_FZF_OPTS + " + + worktrees=() + while IFS='' read -r line; do + [[ -n "$line" ]] && worktrees+=("$(echo "$line" | _forgit_extract_worktree_path)") + done < <(_forgit_worktree_list_deletable | FZF_DEFAULT_OPTS="$opts" fzf) + + [[ ${#worktrees[@]} -eq 0 ]] && return 1 + + for worktree in "${worktrees[@]}"; do + _forgit_git_worktree_delete "$worktree" + done +} + check_prequisites() { local installed_fzf_version local higher_fzf_version @@ -1307,6 +1474,8 @@ PUBLIC_COMMANDS=( "show" "stash_show" "stash_push" + "worktree" + "worktree_delete" ) PRIVATE_COMMANDS=( @@ -1338,6 +1507,10 @@ PRIVATE_COMMANDS=( "edit_diffed_file" "edit_add_file" "pager" + "worktree_preview" + "worktree_yank_path" + "worktree_toggle_lock" + "worktree_list" ) # Check if the script is being sourced. This is necessary for unit tests where diff --git a/completions/_git-forgit b/completions/_git-forgit index ee80c88b..9a1d9a7b 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -18,6 +18,10 @@ _git-stash-show() { _alternative "files:filename:($(git stash list | sed -n -e 's/:.*//p'))" } +_git-worktrees() { + _alternative "worktrees:worktree:($(git worktree list --porcelain 2>/dev/null | grep '^worktree ' | cut -d' ' -f2-))" +} + # The completions for git already define a _git-diff completion function, but # it provides the wrong results when called from _git-forgit because it heavily # depends on the context it's been called from (usage of $curcontext and @@ -79,6 +83,8 @@ _git-forgit() { 'squash:git squash' 'stash_show:git stash viewer' 'stash_push:git stash push selector' + 'worktree:git worktree browser' + 'worktree_delete:git worktree remove selector' ) _describe -t commands 'git forgit' subcommands ;; @@ -102,6 +108,8 @@ _git-forgit() { squash) _git-log ;; stash_show) _git-stash-show ;; show) _git-show ;; + worktree) ;; + worktree_delete) _git-worktrees ;; esac } @@ -131,6 +139,7 @@ compdef _git-log forgit::reword compdef _git-log forgit::squash compdef _git-stash-show forgit::stash::show compdef _git-show forgit::show +compdef _git-worktrees forgit::worktree::delete # this is the case of calling the command and pressing tab # the very first time of a shell session, we have to manually diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 7a508ce7..050783a4 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -35,6 +35,11 @@ _git_stash_show() __gitcomp_nl "$(__git stash list | sed -n -e 's/:.*//p')" } +_git_worktrees() +{ + __gitcomp_nl "$(__git worktree list --porcelain 2>/dev/null | grep '^worktree ' | cut -d' ' -f2-)" +} + # Completion for git-forgit # This includes git aliases, e.g. "alias.cb=forgit checkout_branch" will # correctly complete available branches on "git cb". @@ -80,6 +85,8 @@ _git_forgit() squash stash_show stash_push + worktree + worktree_delete " case ${cword} in @@ -108,6 +115,7 @@ _git_forgit() show) _git_show ;; squash) _git_log ;; stash_show) _git_stash_show ;; + worktree_delete) _git_worktrees ;; esac ;; *) @@ -146,6 +154,7 @@ then __git_complete forgit::show _git_show __git_complete forgit::squash _git_log __git_complete forgit::stash::show _git_stash_show + __git_complete forgit::worktree::delete _git_worktrees # Completion for forgit plugin shell aliases if [[ -z "$FORGIT_NO_ALIASES" ]]; then @@ -169,5 +178,6 @@ then __git_complete "${forgit_show}" _git_show __git_complete "${forgit_squash}" _git_log __git_complete "${forgit_stash_show}" _git_stash_show + __git_complete "${forgit_worktree_delete}" _git_worktrees fi fi diff --git a/completions/git-forgit.fish b/completions/git-forgit.fish index 48f995cf..7827f241 100644 --- a/completions/git-forgit.fish +++ b/completions/git-forgit.fish @@ -8,7 +8,7 @@ function __fish_forgit_needs_subcommand for subcmd in add blame branch_delete checkout_branch checkout_commit checkout_file checkout_tag \ cherry_pick cherry_pick_from_branch clean diff fixup ignore log reflog rebase reset_head \ - revert_commit reword squash stash_show stash_push switch_branch + revert_commit reword squash stash_show stash_push switch_branch worktree worktree_delete if contains -- $subcmd (commandline -opc) return 1 end @@ -16,6 +16,10 @@ function __fish_forgit_needs_subcommand return 0 end +function __fish_forgit_worktrees + git worktree list --porcelain 2>/dev/null | string match -r '^worktree .+' | string replace 'worktree ' '' +end + # Load helper functions in git completion file not functions -q __fish_git && source $__fish_data_dir/completions/git.fish @@ -46,6 +50,8 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a squash -d 'git squas complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_show -d 'git stash viewer' complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_push -d 'git stash push selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a switch_branch -d 'git switch branch selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree -d 'git worktree browser' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree_delete -d 'git worktree remove selector' complete -c git-forgit -n '__fish_seen_subcommand_from add' -a "(complete -C 'git add ')" complete -c git-forgit -n '__fish_seen_subcommand_from branch_delete' -a "(__fish_git_local_branches)" @@ -68,3 +74,4 @@ complete -c git-forgit -n '__fish_seen_subcommand_from squash' -a "(complete -C complete -c git-forgit -n '__fish_seen_subcommand_from stash_show' -a "(__fish_git_complete_stashes)" complete -c git-forgit -n '__fish_seen_subcommand_from stash_push' -a "(__fish_git_files modified deleted modified-staged-deleted)" complete -c git-forgit -n '__fish_seen_subcommand_from switch_branch' -a "(complete -C 'git switch ')" +complete -c git-forgit -n '__fish_seen_subcommand_from worktree_delete' -a "(__fish_forgit_worktrees)" diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 2ffd4299..a0205e25 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -30,6 +30,15 @@ end # alias `git-forgit` to the full-path of the command alias git-forgit "$FORGIT" +function forgit::worktree + if test (count $argv) -ne 0 + git-forgit worktree $argv + return $status + end + set -l tree (git-forgit worktree) + test -n "$tree"; and cd "$tree" +end + # register abbreviations if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_add; or string collect "ga") git-forgit add @@ -56,4 +65,6 @@ if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_revert_commit; or string collect "grc") git-forgit revert_commit abbr -a -- (string collect $forgit_blame; or string collect "gbl") git-forgit blame abbr -a -- (string collect $forgit_checkout_tag; or string collect "gct") git-forgit checkout_tag + abbr -a -- (string collect $forgit_worktree; or string collect "gwt") forgit::worktree + abbr -a -- (string collect $forgit_worktree_delete; or string collect "gwd") git-forgit worktree_delete end diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index 012c354f..a928fa16 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -160,6 +160,17 @@ forgit::attributes() { "$FORGIT" attributes "$@" } +forgit::worktree() { + if [[ $# -ne 0 ]]; then "$FORGIT" worktree "$@"; return $?; fi + local tree + tree=$("$FORGIT" worktree) || return $? + [[ -n "$tree" ]] && builtin cd "$tree" || return 1 +} + +forgit::worktree::delete() { + "$FORGIT" worktree_delete "$@" +} + # register aliases # shellcheck disable=SC2139 if [[ -z "$FORGIT_NO_ALIASES" ]]; then @@ -188,6 +199,8 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin export forgit_squash="${forgit_squash:-gsq}" builtin export forgit_reword="${forgit_reword:-grw}" builtin export forgit_blame="${forgit_blame:-gbl}" + builtin export forgit_worktree="${forgit_worktree:-gwt}" + builtin export forgit_worktree_delete="${forgit_worktree_delete:-gwd}" builtin alias "${forgit_add}"='forgit::add' builtin alias "${forgit_reset_head}"='forgit::reset::head' @@ -213,5 +226,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin alias "${forgit_squash}"='forgit::squash' builtin alias "${forgit_reword}"='forgit::reword' builtin alias "${forgit_blame}"='forgit::blame' + builtin alias "${forgit_worktree}"='forgit::worktree' + builtin alias "${forgit_worktree_delete}"='forgit::worktree::delete' fi diff --git a/tests/branch-helpers.test.sh b/tests/branch-helpers.test.sh new file mode 100644 index 00000000..87521284 --- /dev/null +++ b/tests/branch-helpers.test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + source bin/git-forgit +} + +# --- _forgit_extract_branch_name --- + +# @data_provider provider_extract_branch_name +function test_forgit_extract_branch_name() { + local -r input="$1" + local -r expected="$2" + local actual + actual=$(echo "$input" | _forgit_extract_branch_name) + assert_same "$expected" "$actual" +} + +function provider_extract_branch_name() { + bashunit::data_set " main" "main" + bashunit::data_set "* main" "main" + bashunit::data_set "+ feature" "feature" + bashunit::data_set " remotes/origin/HEAD -> origin/main" "remotes/origin/HEAD" + bashunit::data_set " remotes/origin/feature/foo" "remotes/origin/feature/foo" +} + +# --- _forgit_strip_ansi --- + +# @data_provider provider_strip_ansi +function test_forgit_strip_ansi() { + local -r input="$1" + local -r expected="$2" + local actual + actual=$(printf '%b' "$input" | _forgit_strip_ansi) + assert_same "$expected" "$actual" +} + +function provider_strip_ansi() { + bashunit::data_set '\e[32mfoo\e[0m' "foo" + bashunit::data_set '\e[1;31mbar\e[0m' "bar" + bashunit::data_set 'no-ansi' "no-ansi" +} + +# --- _forgit_extract_worktree_path --- + +# @data_provider provider_extract_worktree_path +function test_forgit_extract_worktree_path() { + local -r input="$1" + local -r expected="$2" + local actual + actual=$(echo "$input" | _forgit_extract_worktree_path) + assert_same "$expected" "$actual" +} + +function provider_extract_worktree_path() { + bashunit::data_set '[* ] /tmp/foo (main) 3 hours ago' "/tmp/foo" + bashunit::data_set '[ ] /tmp/bar (feature) 1 day ago' "/tmp/bar" +}