From ac132b7bbac643877721a902dde9df466de38e52 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Mon, 1 Sep 2025 21:31:18 +0200 Subject: [PATCH 1/5] Meta: Add bashunit tests to CI workflow and implement a first test case (#464) --- .github/pull_request_template.md | 2 ++ .github/workflows/ci.yaml | 6 +++++ .gitignore | 1 + tests/fzf.test.sh | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 tests/fzf.test.sh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a2da841d..dd8426af 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,6 +6,7 @@ - [ ] I have performed a self-review of my code - [ ] I have commented my code in hard-to-understand areas +- [ ] I have added unit tests for my code - [ ] I have made corresponding changes to the documentation ## Description @@ -18,6 +19,7 @@ - [ ] New feature - [ ] Refactor - [ ] Breaking change +- [ ] Test - [ ] Documentation change ## Test environment diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14abff55..df9bd5a2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,16 +39,22 @@ jobs: sudo apt -y update sudo apt -y install zsh fish shellcheck fi + curl -s https://bashunit.typeddevs.com/install.sh | bash -s 0.23.0 - name: Show version run: | bash --version; echo zsh --version; echo fish --version; echo + shellcheck --version; echo + lib/bashunit --version; echo - name: Shellcheck run: shellcheck forgit.plugin.sh bin/git-forgit + - name: Unit tests + run: lib/bashunit . + - name: Test bash run: bash forgit.plugin.sh diff --git a/.gitignore b/.gitignore index eace2b11..814f6201 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ ._zinit/ *.zwc +lib/ diff --git a/tests/fzf.test.sh b/tests/fzf.test.sh new file mode 100644 index 00000000..067764f9 --- /dev/null +++ b/tests/fzf.test.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +function test_exit_when_fzf_is_not_installed() { + # Error code 127 = "command not found" + mock fzf "return 127" + + output=$(bin/git-forgit) + + assert_general_error + assert_contains "fzf is not installed" "$output" +} + +# @data_provider fzf_versions_below_required_version +function test_exit_when_fzf_version_is_below_required_version() { + mock "fzf" "echo '$1'" + + output=$(bin/git-forgit) + + assert_general_error + assert_contains "fzf version 0.49.0 or higher is required" "$output" +} + +function fzf_versions_below_required_version() { + echo "0.0.2" + echo "0.5.0" + echo "0.30.0" + echo "0.48.9" +} + +# @data_provider fzf_versions_satisfying_required_version +function test_pass_when_fzf_version_satisfies_required_version() { + mock "fzf" "echo '$1'" + + output=$(bin/git-forgit) + + assert_general_error + assert_contains "missing command" "$output" +} + +function fzf_versions_satisfying_required_version() { + echo "0.49.0" + echo "0.49.1" + echo "0.80.0" + echo "1.1.0" +} From d5961bee7e5f45539e5537b6a60852e6135a6553 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Sat, 6 Sep 2025 13:25:02 +0200 Subject: [PATCH 2/5] Refactor: Move global code into a main function (#464) This is a prequisite for being able to source the script in order to implement unit tests for individual functions. --- bin/git-forgit | 90 ++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/bin/git-forgit b/bin/git-forgit index 0fb65b2f..c2567e1c 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -12,27 +12,7 @@ # This gives users the choice to set aliases inside of their git config instead # of their shell config if they prefer. -# Check if fzf is installed -installed_fzf_version=$(fzf --version 2>/dev/null | awk '{print $1}') -if [[ -z "$installed_fzf_version" ]]; then - echo "fzf is not installed. Please install fzf first." - exit 1 -fi - -# Check fzf version -required_fzf_version="0.49.0" -higher_fzf_version=$(printf '%s\n' "$required_fzf_version" "$installed_fzf_version" | sort -V | tail -n1) -if [[ "$higher_fzf_version" != "$installed_fzf_version" ]]; then - echo "fzf version $required_fzf_version or higher is required. You have $installed_fzf_version." - exit 1 -fi - -# Set shell for fzf preview commands -SHELL="$(which bash)" -export SHELL - -# Get absolute forgit path -FORGIT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/$(basename -- "${BASH_SOURCE[0]}") +REQUIRED_FZF_VERSION="0.49.0" FORGIT_FZF_DEFAULT_OPTS=" $FZF_DEFAULT_OPTS @@ -1185,7 +1165,54 @@ _forgit_paths_list() { find "$path" -name "*$ext" -print |sed -e "s#$ext\$##" -e 's#.*/##' -e '/^$/d' | sort -fu } -public_commands=( +check_prequisites() { + local installed_fzf_version + local higher_fzf_version + + # Check if fzf is installed + installed_fzf_version=$(fzf --version 2>/dev/null | awk '{print $1}') + if [[ -z "$installed_fzf_version" ]]; then + echo "fzf is not installed. Please install fzf first." + exit 1 + fi + + # Check fzf version + higher_fzf_version=$(printf '%s\n' "$REQUIRED_FZF_VERSION" "$installed_fzf_version" | sort -V | tail -n1) + if [[ "$higher_fzf_version" != "$installed_fzf_version" ]]; then + echo "fzf version $REQUIRED_FZF_VERSION or higher is required. You have $installed_fzf_version." + exit 1 + fi +} + +main() { + local cmd="$1" + shift + + check_prequisites + + # Set shell for fzf preview commands + SHELL="$(which bash)" + export SHELL + + # Get absolute forgit path + FORGIT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/$(basename -- "${BASH_SOURCE[0]}") + + # shellcheck disable=SC2076 + if [[ ! " ${PUBLIC_COMMANDS[*]} " =~ " ${cmd} " ]] && [[ ! " ${PRIVATE_COMMANDS[*]} " =~ " ${cmd} " ]]; then + if [[ -z "$cmd" ]]; then + printf "forgit: missing command\n\n" + else + printf "forgit: '%s' is not a valid forgit command.\n\n" "$cmd" + fi + printf "The following commands are supported:\n" + printf "\t%s\n" "${PUBLIC_COMMANDS[@]}" + exit 1 + fi + + _forgit_"${cmd}" "$@" +} + +PUBLIC_COMMANDS=( "add" "attributes" "blame" @@ -1212,7 +1239,7 @@ public_commands=( "stash_push" ) -private_commands=( +PRIVATE_COMMANDS=( "add_preview" "blame_preview" "branch_preview" @@ -1243,19 +1270,4 @@ private_commands=( "pager" ) -cmd="$1" -shift - -# shellcheck disable=SC2076 -if [[ ! " ${public_commands[*]} " =~ " ${cmd} " ]] && [[ ! " ${private_commands[*]} " =~ " ${cmd} " ]]; then - if [[ -z "$cmd" ]]; then - printf "forgit: missing command\n\n" - else - printf "forgit: '%s' is not a valid forgit command.\n\n" "$cmd" - fi - printf "The following commands are supported:\n" - printf "\t%s\n" "${public_commands[@]}" - exit 1 -fi - -_forgit_"${cmd}" "$@" +main "${@}" From a27da47aa59278e66d05aa918d580fa4c2fb4575 Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Sat, 6 Sep 2025 13:25:23 +0200 Subject: [PATCH 3/5] Chore: Do not execute any code when forgit is sourced (#464) In order to run unit tests for individual functions we have to be able to source the script without triggering the main execution flow. --- bin/git-forgit | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/git-forgit b/bin/git-forgit index c2567e1c..024988d1 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -1270,4 +1270,10 @@ PRIVATE_COMMANDS=( "pager" ) +# Check if the script is being sourced. This is necessary for unit tests where +# we do not want to execute the main function. +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + return 0 +fi + main "${@}" From b244e344f59a735aae2afcfea8b0b8de9e8107bc Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Sat, 6 Sep 2025 13:26:56 +0200 Subject: [PATCH 4/5] Meta: Add unit test for _forgit_get_files_from_diff_line function (#464) Update CI to install latest bashunit beta version instead of fixed 0.23.0 version because we need the new "data_set" function for parameterized tests. --- .github/workflows/ci.yaml | 2 +- tests/helper-functions.test.sh | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/helper-functions.test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index df9bd5a2..035759e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,7 @@ jobs: sudo apt -y update sudo apt -y install zsh fish shellcheck fi - curl -s https://bashunit.typeddevs.com/install.sh | bash -s 0.23.0 + curl -s https://bashunit.typeddevs.com/install.sh | bash -s beta - name: Show version run: | diff --git a/tests/helper-functions.test.sh b/tests/helper-functions.test.sh new file mode 100644 index 00000000..30096c3f --- /dev/null +++ b/tests/helper-functions.test.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + source bin/git-forgit +} + +# @data_provider provider_diff_lines +function test_forgit_get_files_from_diff_line() { + local -r input="$1" + shift + local -r expected=("$@") + local -a actual=() + local i + + while IFS= read -r line; do + actual+=( "$line" ) + done < <( echo -n "$input" | _forgit_get_files_from_diff_line | xargs -0 -n 1 echo ) + + # Compare array sizes + assert_same "${#expected[@]}" "${#actual[@]}" + + # Compare array elements + for i in "${!expected[@]}"; do + assert_same "${expected[i]}" "${actual[i]}" + done +} + +function provider_diff_lines() { + data_set "[A] newfile" "newfile" + data_set "[D] oldfile with spaces" "oldfile with spaces" + data_set "[R100] file -> another file" "file" "another file" + data_set "[M] \"file with\ttab.txt\"" "\"file with\ttab.txt\"" +} From d646d391e4aeca5685858fcb9696570d9799939c Mon Sep 17 00:00:00 2001 From: carlfriedrich Date: Sat, 6 Sep 2025 14:24:14 +0200 Subject: [PATCH 5/5] Refactor: extract function to remove status from diff line (#464) --- bin/git-forgit | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bin/git-forgit b/bin/git-forgit index 024988d1..3ae4e516 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -232,6 +232,14 @@ _forgit_reflog() { return $fzf_exit_code } +_forgit_remove_status_from_diff_line() { + # Remove the status prefix from a diff line, e.g. + # [M] somefile + # becomes + # somefile + sed 's/^[[:space:]]*\[[A-Z0-9]*\][[:space:]]*//' +} + _forgit_get_files_from_diff_line() { # Construct a null-terminated list of the filenames # The input looks like one of these lines: @@ -244,13 +252,13 @@ _forgit_get_files_from_diff_line() { # oldfile\0 # We have to do a two-step sed -> tr pipe because OSX's sed implementation does # not support the null-character directly. - sed 's/^[[:space:]]*\[[A-Z0-9]*\][[:space:]]*//' | sed 's/ -> /\n/' | tr '\n' '\0' + _forgit_remove_status_from_diff_line | sed 's/ -> /\n/' | tr '\n' '\0' } _forgit_get_single_file_from_diff_line() { # Similar to the function above, but only gets a single file from a single line # Gets the new name of renamed files - sed 's/^[[:space:]]*\[[A-Z0-9]*\][[:space:]]*//' | sed 's/.*-> //' + _forgit_remove_status_from_diff_line | sed 's/.*-> //' } _forgit_exec_diff() {