From 4046467de680f7debf962ebf11d9cd7641f3028c Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 11 Feb 2026 16:57:08 -0600 Subject: [PATCH 1/4] forgit: Add interactive restore command for discarding changes Currently there is no interactive way to selectively discard modifications to working tree files. Users must either manually type file paths with git restore or use git checkout, which is the older discouraged syntax. This makes it cumbersome to selectively discard changes when reviewing multiple modified files. Add _forgit_restore function that provides an fzf-based selector for modified files with diff preview. This follows the same pattern as existing forgit commands like reset_head and checkout_file, allowing users to interactively review diffs before discarding changes. The command is exposed as 'grs' alias and integrated with shell completion systems across bash/zsh/fish. Signed-off-by: Javier Tia --- LICENSE | 2 +- README.md | 5 +++++ bin/git-forgit | 35 +++++++++++++++++++++++++++++++++++ completions/_git-forgit | 3 +++ completions/git-forgit.bash | 4 ++++ completions/git-forgit.fish | 4 +++- conf.d/forgit.plugin.fish | 1 + forgit.plugin.zsh | 6 ++++++ 8 files changed, 58 insertions(+), 2 deletions(-) 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..a2372522 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ If you're having issues after updating, and commands such as `forgit::add` or al - **Interactive `git reset HEAD ` selector** (`grh`) +- **Interactive `git restore ` selector** (`grs`) + - **Interactive `git checkout ` selector** (`gcf`) - **Interactive `git checkout ` selector** (`gcb`) @@ -222,6 +224,7 @@ forgit_diff=gd forgit_show=gso forgit_add=ga forgit_reset_head=grh +forgit_restore=grs forgit_ignore=gi forgit_attributes=gat forgit_checkout_file=gcf @@ -286,6 +289,7 @@ These are passed to the according `git` calls. | `gd` | `FORGIT_DIFF_GIT_OPTS` | | `gso` | `FORGIT_SHOW_GIT_OPTS` | | `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` | +| `grs` | `FORGIT_RESTORE_GIT_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`, `FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` | | `gsw` | `FORGIT_SWITCH_BRANCH_GIT_OPTS` | @@ -346,6 +350,7 @@ Customizing fzf options for each command individually is also supported: | `gd` | `FORGIT_DIFF_FZF_OPTS` | | `gso` | `FORGIT_SHOW_FZF_OPTS` | | `grh` | `FORGIT_RESET_HEAD_FZF_OPTS` | +| `grs` | `FORGIT_RESTORE_FZF_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` | | `gsw` | `FORGIT_SWITCH_BRANCH_FZF_OPTS` | diff --git a/bin/git-forgit b/bin/git-forgit index f54845f2..071e8e77 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -522,6 +522,39 @@ _forgit_reset_head() { git status --short } +_forgit_restore_preview() { + git diff --color=always -- "$1" | _forgit_pager diff +} + +_forgit_git_restore() { + _forgit_restore_git_opts=() + _forgit_parse_array _forgit_restore_git_opts "$FORGIT_RESTORE_GIT_OPTS" + git restore "${_forgit_restore_git_opts[@]}" "$@" +} + +# git restore selector +_forgit_restore() { + _forgit_inside_work_tree || return 1 + local files opts + _forgit_contains_non_flags "$@" && { + _forgit_git_restore "$@" + return $? + } + [[ $(_forgit_list_files --modified | wc -l) -eq 0 ]] && echo 'Nothing to restore.' && return 1 + opts=" + $FORGIT_FZF_DEFAULT_OPTS + -m -0 + --preview=\"$FORGIT restore_preview {}\" + $FORGIT_RESTORE_FZF_OPTS + " + files=() + while IFS='' read -r file; do + files+=("$file") + done < <(_forgit_list_files --modified | + FZF_DEFAULT_OPTS="$opts" fzf) + [[ "${#files[@]}" -gt 0 ]] && _forgit_git_restore "$@" "${files[@]}" +} + _forgit_stash_show_preview() { local stash stash=$(echo "$1" | _forgit_extract_stash_name) @@ -1303,6 +1336,7 @@ PUBLIC_COMMANDS=( "reflog" "rebase" "reset_head" + "restore" "revert_commit" "show" "stash_show" @@ -1324,6 +1358,7 @@ PRIVATE_COMMANDS=( "path_preview" "revert_preview" "reset_head_preview" + "restore_preview" "show_enter" "show_preview" "stash_push_preview" diff --git a/completions/_git-forgit b/completions/_git-forgit index ee80c88b..00652c49 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -74,6 +74,7 @@ _git-forgit() { 'reflog:git reflog viewer' 'rebase:git rebase' 'reset_head:git reset HEAD (unstage) selector' + 'restore:git restore file selector' 'revert_commit:git revert commit selector' 'reword:git fixup=reword' 'squash:git squash' @@ -97,6 +98,7 @@ _git-forgit() { reflog) _git-forgit-reflog ;; rebase) _git-rebase ;; reset_head) _git-staged ;; + restore) _git-checkout-file ;; revert_commit) __git_recent_commits ;; reword) _git-log ;; squash) _git-log ;; @@ -126,6 +128,7 @@ compdef _git-log forgit::log compdef _git-reflog forgit::reflog compdef _git-rebase forgit::rebase compdef _git-staged forgit::reset::head +compdef _git-checkout-file forgit::restore compdef __git_recent_commits forgit::revert::commit compdef _git-log forgit::reword compdef _git-log forgit::squash diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 7a508ce7..5dcf0e55 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -74,6 +74,7 @@ _git_forgit() reflog rebase reset_head + restore revert_commit reword show @@ -103,6 +104,7 @@ _git_forgit() reflog) _git_reflog ;; rebase) _git_rebase ;; reset_head) _git_reset ;; + restore) _git_checkout_file ;; revert_commit) _git_revert ;; reword) _git_log ;; show) _git_show ;; @@ -141,6 +143,7 @@ then __git_complete forgit::reflog _git_reflog __git_complete forgit::rebase _git_rebase __git_complete forgit::reset::head _git_reset + __git_complete forgit::restore _git_checkout_file __git_complete forgit::revert::commit _git_revert __git_complete forgit::reword _git_log __git_complete forgit::show _git_show @@ -164,6 +167,7 @@ then __git_complete "${forgit_reflog}" _git_reflog __git_complete "${forgit_rebase}" _git_rebase __git_complete "${forgit_reset_head}" _git_reset + __git_complete "${forgit_restore}" _git_checkout_file __git_complete "${forgit_revert_commit}" _git_revert __git_complete "${forgit_reword}" _git_log __git_complete "${forgit_show}" _git_show diff --git a/completions/git-forgit.fish b/completions/git-forgit.fish index 48f995cf..8527d0ad 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 + restore revert_commit reword squash stash_show stash_push switch_branch if contains -- $subcmd (commandline -opc) return 1 end @@ -39,6 +39,7 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a log -d 'git commit v complete -c git-forgit -n __fish_forgit_needs_subcommand -a reflog -d 'git reflog viewer' complete -c git-forgit -n __fish_forgit_needs_subcommand -a rebase -d 'git rebase' complete -c git-forgit -n __fish_forgit_needs_subcommand -a reset_head -d 'git reset HEAD (unstage) selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a restore -d 'git restore file selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a revert_commit -d 'git revert commit selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a reword -d 'git fixup=reword' complete -c git-forgit -n __fish_forgit_needs_subcommand -a show -d 'git show viewer' @@ -61,6 +62,7 @@ complete -c git-forgit -n '__fish_seen_subcommand_from log' -a "(complete -C 'gi complete -c git-forgit -n '__fish_seen_subcommand_from reflog' -a "(complete -C 'git reflog ')" complete -c git-forgit -n '__fish_seen_subcommand_from rebase' -a "(complete -C 'git rebase ')" complete -c git-forgit -n '__fish_seen_subcommand_from reset_head' -a "(__fish_git_files all-staged)" +complete -c git-forgit -n '__fish_seen_subcommand_from restore' -a "(__fish_git_files modified)" complete -c git-forgit -n '__fish_seen_subcommand_from revert_commit' -a "(__fish_git_commits)" complete -c git-forgit -n '__fish_seen_subcommand_from reword' -a "(complete -C 'git log ')" complete -c git-forgit -n '__fish_seen_subcommand_from show' -a "(complete -C 'git show ')" diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 2ffd4299..5d5aefda 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -34,6 +34,7 @@ alias git-forgit "$FORGIT" if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_add; or string collect "ga") git-forgit add abbr -a -- (string collect $forgit_reset_head; or string collect "grh") git-forgit reset_head + abbr -a -- (string collect $forgit_restore; or string collect "grs") git-forgit restore abbr -a -- (string collect $forgit_log; or string collect "glo") git-forgit log abbr -a -- (string collect $forgit_reflog; or string collect "grl") git-forgit reflog abbr -a -- (string collect $forgit_diff; or string collect "gd") git-forgit diff diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index 012c354f..a649d92a 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -68,6 +68,10 @@ forgit::reset::head() { "$FORGIT" reset_head "$@" } +forgit::restore() { + "$FORGIT" restore "$@" +} + forgit::stash::show() { "$FORGIT" stash_show "$@" } @@ -166,6 +170,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin export forgit_add="${forgit_add:-ga}" builtin export forgit_reset_head="${forgit_reset_head:-grh}" + builtin export forgit_restore="${forgit_restore:-grs}" builtin export forgit_log="${forgit_log:-glo}" builtin export forgit_reflog="${forgit_reflog:-grl}" builtin export forgit_diff="${forgit_diff:-gd}" @@ -191,6 +196,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin alias "${forgit_add}"='forgit::add' builtin alias "${forgit_reset_head}"='forgit::reset::head' + builtin alias "${forgit_restore}"='forgit::restore' builtin alias "${forgit_log}"='forgit::log' builtin alias "${forgit_reflog}"='forgit::reflog' builtin alias "${forgit_diff}"='forgit::diff' From 2962b0c2d2159ff22f75ac4c4ecdc12893b53e7a Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 11 Feb 2026 16:57:27 -0600 Subject: [PATCH 2/4] tests: Add comprehensive test coverage for git restore functionality Currently the restore functionality lacks automated tests, making it difficult to verify correctness during refactoring and increasing the risk of regressions. Without tests, developers cannot confidently modify the restore behavior or ensure edge cases are handled properly. Add a complete test suite covering core restore scenarios including modified files, untracked files, mixed staged/unstaged changes, and renamed files. These tests validate that restore correctly reverts working tree changes while preserving staged content and ignoring untracked files, ensuring the command behaves consistently with Git's restore semantics. Signed-off-by: Javier Tia --- tests/restore.test.sh | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/restore.test.sh diff --git a/tests/restore.test.sh b/tests/restore.test.sh new file mode 100644 index 00000000..69f45284 --- /dev/null +++ b/tests/restore.test.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + source bin/git-forgit + export GIT_CONFIG_SYSTEM=/dev/null + export GIT_CONFIG_GLOBAL=/dev/null + cd "$(bashunit::temp_dir)" || return 1 + git init -q + git config user.email "test@example.com" + git config user.name "Test User" + echo "initial" >README.md + git add README.md + git commit -q -m "Initial commit" +} + +function test_restore_shows_message_when_no_modified_files() { + output=$(_forgit_restore 2>&1) + assert_general_error + assert_same "Nothing to restore." "$output" +} + +function test_restore_reverts_modified_file_to_committed_state() { + echo "original content" >tracked.txt + git add tracked.txt + git commit -q -m "Add tracked file" + echo "modified content" >tracked.txt + _forgit_git_restore tracked.txt + assert_same "original content" "$(cat tracked.txt)" +} + +function test_restore_does_not_affect_untracked_files() { + echo "untracked content" >untracked.txt + _forgit_git_restore untracked.txt 2>/dev/null || true + assert_file_exists untracked.txt + assert_same "untracked content" "$(cat untracked.txt)" +} + +function test_restore_only_reverts_unstaged_changes_when_file_has_staged_and_unstaged() { + echo "committed" >mixed.txt + git add mixed.txt + git commit -q -m "Add mixed file" + echo "staged change" >mixed.txt + git add mixed.txt + echo "unstaged change" >mixed.txt + _forgit_git_restore mixed.txt + assert_same "staged change" "$(cat mixed.txt)" +} + +function test_restore_with_renamed_file() { + echo "rename content" >before-rename.txt + git add before-rename.txt + git commit -q -m "Add file for rename test" + git mv before-rename.txt after-rename.txt + git commit -q -m "Rename file" + echo "modified after rename" >after-rename.txt + _forgit_git_restore after-rename.txt + assert_same "rename content" "$(cat after-rename.txt)" +} + +function test_restore_passes_through_arguments_when_non_flags_provided() { + echo "original" >passthrough.txt + git add passthrough.txt + git commit -q -m "Add passthrough file" + echo "modified" >passthrough.txt + _forgit_restore passthrough.txt + assert_same "original" "$(cat passthrough.txt)" +} From 7d523361f8b4c431ffb14370d308493af92a9b70 Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 11 Feb 2026 17:09:13 -0600 Subject: [PATCH 3/4] forgit: Add discard command for unstage and restore Currently, users must run two separate commands to completely revert changes: reset HEAD to unstage files, then restore to discard working tree modifications. This two-step workflow is error-prone and inefficient, especially when reverting multiple files interactively. Introduce a discard command that combines unstage and restore into a single interactive operation. This provides a safer and more streamlined way to abandon uncommitted changes through the familiar fzf-based file selector, matching the workflow of existing forgit commands like add and restore while preventing the common mistake of unstaging without actually reverting the changes. Signed-off-by: Javier Tia --- README.md | 10 ++++++++++ bin/git-forgit | 37 +++++++++++++++++++++++++++++++++++++ completions/_git-forgit | 1 + completions/git-forgit.bash | 2 ++ completions/git-forgit.fish | 2 ++ conf.d/forgit.plugin.fish | 1 + forgit.plugin.zsh | 6 ++++++ 7 files changed, 59 insertions(+) diff --git a/README.md b/README.md index a2372522..5158b004 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,13 @@ If you're having issues after updating, and commands such as `forgit::add` or al - **Interactive `git restore ` selector** (`grs`) +- **Interactive `git discard ` selector** (`gdc`) + + Combines `git reset HEAD` (unstage) and `git restore` (revert) into a single operation, + fully discarding all changes for selected files back to their committed state. + + > **Warning:** This permanently discards uncommitted changes with no way to recover them. + - **Interactive `git checkout ` selector** (`gcf`) - **Interactive `git checkout ` selector** (`gcb`) @@ -225,6 +232,7 @@ forgit_show=gso forgit_add=ga forgit_reset_head=grh forgit_restore=grs +forgit_discard=gdc forgit_ignore=gi forgit_attributes=gat forgit_checkout_file=gcf @@ -290,6 +298,7 @@ These are passed to the according `git` calls. | `gso` | `FORGIT_SHOW_GIT_OPTS` | | `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` | | `grs` | `FORGIT_RESTORE_GIT_OPTS` | +| `gdc` | `FORGIT_DISCARD_GIT_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`, `FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` | | `gsw` | `FORGIT_SWITCH_BRANCH_GIT_OPTS` | @@ -351,6 +360,7 @@ Customizing fzf options for each command individually is also supported: | `gso` | `FORGIT_SHOW_FZF_OPTS` | | `grh` | `FORGIT_RESET_HEAD_FZF_OPTS` | | `grs` | `FORGIT_RESTORE_FZF_OPTS` | +| `gdc` | `FORGIT_DISCARD_FZF_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` | | `gsw` | `FORGIT_SWITCH_BRANCH_FZF_OPTS` | diff --git a/bin/git-forgit b/bin/git-forgit index 071e8e77..11fb5cf0 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -555,6 +555,41 @@ _forgit_restore() { [[ "${#files[@]}" -gt 0 ]] && _forgit_git_restore "$@" "${files[@]}" } +_forgit_discard_preview() { + git diff --color=always HEAD -- "$1" | _forgit_pager diff +} + +_forgit_git_discard() { + _forgit_discard_git_opts=() + _forgit_parse_array _forgit_discard_git_opts "$FORGIT_DISCARD_GIT_OPTS" + git reset -q HEAD "${_forgit_discard_git_opts[@]}" -- "$@" && git restore "${_forgit_discard_git_opts[@]}" "$@" +} + +# git discard (unstage + restore) selector +_forgit_discard() { + _forgit_inside_work_tree || return 1 + local files opts + _forgit_contains_non_flags "$@" && { + _forgit_git_discard "$@" + return $? + } + local changed + changed=$(git diff --name-only HEAD 2>/dev/null) + [[ -z "$changed" ]] && echo 'Nothing to discard.' && return 1 + opts=" + $FORGIT_FZF_DEFAULT_OPTS + -m -0 + --preview=\"$FORGIT discard_preview {}\" + $FORGIT_DISCARD_FZF_OPTS + " + files=() + while IFS='' read -r file; do + files+=("$file") + done < <(echo "$changed" | + FZF_DEFAULT_OPTS="$opts" fzf) + [[ "${#files[@]}" -gt 0 ]] && _forgit_git_discard "${files[@]}" +} + _forgit_stash_show_preview() { local stash stash=$(echo "$1" | _forgit_extract_stash_name) @@ -1337,6 +1372,7 @@ PUBLIC_COMMANDS=( "rebase" "reset_head" "restore" + "discard" "revert_commit" "show" "stash_show" @@ -1359,6 +1395,7 @@ PRIVATE_COMMANDS=( "revert_preview" "reset_head_preview" "restore_preview" + "discard_preview" "show_enter" "show_preview" "stash_push_preview" diff --git a/completions/_git-forgit b/completions/_git-forgit index 00652c49..38c23426 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -129,6 +129,7 @@ compdef _git-reflog forgit::reflog compdef _git-rebase forgit::rebase compdef _git-staged forgit::reset::head compdef _git-checkout-file forgit::restore +compdef _git-checkout-file forgit::discard compdef __git_recent_commits forgit::revert::commit compdef _git-log forgit::reword compdef _git-log forgit::squash diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 5dcf0e55..797cacbc 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -144,6 +144,7 @@ then __git_complete forgit::rebase _git_rebase __git_complete forgit::reset::head _git_reset __git_complete forgit::restore _git_checkout_file +__git_complete forgit::discard _git_checkout_file __git_complete forgit::revert::commit _git_revert __git_complete forgit::reword _git_log __git_complete forgit::show _git_show @@ -168,6 +169,7 @@ then __git_complete "${forgit_rebase}" _git_rebase __git_complete "${forgit_reset_head}" _git_reset __git_complete "${forgit_restore}" _git_checkout_file +__git_complete "${forgit_discard}" _git_checkout_file __git_complete "${forgit_revert_commit}" _git_revert __git_complete "${forgit_reword}" _git_log __git_complete "${forgit_show}" _git_show diff --git a/completions/git-forgit.fish b/completions/git-forgit.fish index 8527d0ad..d6e0b052 100644 --- a/completions/git-forgit.fish +++ b/completions/git-forgit.fish @@ -40,6 +40,7 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a reflog -d 'git reflo complete -c git-forgit -n __fish_forgit_needs_subcommand -a rebase -d 'git rebase' complete -c git-forgit -n __fish_forgit_needs_subcommand -a reset_head -d 'git reset HEAD (unstage) selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a restore -d 'git restore file selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a discard -d 'git discard (unstage + restore) selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a revert_commit -d 'git revert commit selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a reword -d 'git fixup=reword' complete -c git-forgit -n __fish_forgit_needs_subcommand -a show -d 'git show viewer' @@ -63,6 +64,7 @@ complete -c git-forgit -n '__fish_seen_subcommand_from reflog' -a "(complete -C complete -c git-forgit -n '__fish_seen_subcommand_from rebase' -a "(complete -C 'git rebase ')" complete -c git-forgit -n '__fish_seen_subcommand_from reset_head' -a "(__fish_git_files all-staged)" complete -c git-forgit -n '__fish_seen_subcommand_from restore' -a "(__fish_git_files modified)" +complete -c git-forgit -n '__fish_seen_subcommand_from discard' -a "(__fish_git_files modified)" complete -c git-forgit -n '__fish_seen_subcommand_from revert_commit' -a "(__fish_git_commits)" complete -c git-forgit -n '__fish_seen_subcommand_from reword' -a "(complete -C 'git log ')" complete -c git-forgit -n '__fish_seen_subcommand_from show' -a "(complete -C 'git show ')" diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 5d5aefda..5ac3ded9 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -35,6 +35,7 @@ if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_add; or string collect "ga") git-forgit add abbr -a -- (string collect $forgit_reset_head; or string collect "grh") git-forgit reset_head abbr -a -- (string collect $forgit_restore; or string collect "grs") git-forgit restore + abbr -a -- (string collect $forgit_discard; or string collect "gdc") git-forgit discard abbr -a -- (string collect $forgit_log; or string collect "glo") git-forgit log abbr -a -- (string collect $forgit_reflog; or string collect "grl") git-forgit reflog abbr -a -- (string collect $forgit_diff; or string collect "gd") git-forgit diff diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index a649d92a..68675e1c 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -72,6 +72,10 @@ forgit::restore() { "$FORGIT" restore "$@" } +forgit::discard() { + "$FORGIT" discard "$@" +} + forgit::stash::show() { "$FORGIT" stash_show "$@" } @@ -171,6 +175,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin export forgit_add="${forgit_add:-ga}" builtin export forgit_reset_head="${forgit_reset_head:-grh}" builtin export forgit_restore="${forgit_restore:-grs}" + builtin export forgit_discard="${forgit_discard:-gdc}" builtin export forgit_log="${forgit_log:-glo}" builtin export forgit_reflog="${forgit_reflog:-grl}" builtin export forgit_diff="${forgit_diff:-gd}" @@ -197,6 +202,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin alias "${forgit_add}"='forgit::add' builtin alias "${forgit_reset_head}"='forgit::reset::head' builtin alias "${forgit_restore}"='forgit::restore' + builtin alias "${forgit_discard}"='forgit::discard' builtin alias "${forgit_log}"='forgit::log' builtin alias "${forgit_reflog}"='forgit::reflog' builtin alias "${forgit_diff}"='forgit::diff' From 6cfc362d2246b02a682e61518ffe67b15336f60c Mon Sep 17 00:00:00 2001 From: Javier Tia Date: Wed, 11 Feb 2026 17:09:34 -0600 Subject: [PATCH 4/4] tests: Add tests for git-forgit discard functionality The git-forgit discard feature previously lacked automated test coverage, making it difficult to verify correct behavior across different scenarios and prevent regressions during refactoring or enhancements. Add a full test suite covering critical discard workflows: empty repository state, unstaged modifications, staged changes, mixed staging states, committed renames, and argument passthrough. These tests validate that discard correctly restores files to their committed state regardless of staging status or file history. Signed-off-by: Javier Tia --- tests/discard.test.sh | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/discard.test.sh diff --git a/tests/discard.test.sh b/tests/discard.test.sh new file mode 100644 index 00000000..8931332e --- /dev/null +++ b/tests/discard.test.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + source bin/git-forgit + export GIT_CONFIG_SYSTEM=/dev/null + export GIT_CONFIG_GLOBAL=/dev/null + cd "$(bashunit::temp_dir)" || return 1 + git init -q + git config user.email "test@example.com" + git config user.name "Test User" + echo "initial" >README.md + git add README.md + git commit -q -m "Initial commit" +} + +function test_discard_shows_message_when_no_changes() { + output=$(_forgit_discard 2>&1) + assert_general_error + assert_same "Nothing to discard." "$output" +} + +function test_discard_reverts_unstaged_changes() { + echo "original" >tracked.txt + git add tracked.txt + git commit -q -m "Add tracked file" + echo "modified" >tracked.txt + _forgit_git_discard tracked.txt + assert_same "original" "$(cat tracked.txt)" +} + +function test_discard_reverts_staged_changes() { + echo "original" >staged.txt + git add staged.txt + git commit -q -m "Add staged file" + echo "staged change" >staged.txt + git add staged.txt + _forgit_git_discard staged.txt + assert_same "original" "$(cat staged.txt)" + # Verify file is no longer staged + assert_same "" "$(git diff --staged --name-only)" +} + +function test_discard_reverts_mixed_staged_and_unstaged_changes() { + echo "committed" >mixed.txt + git add mixed.txt + git commit -q -m "Add mixed file" + echo "staged change" >mixed.txt + git add mixed.txt + echo "unstaged change" >mixed.txt + _forgit_git_discard mixed.txt + assert_same "committed" "$(cat mixed.txt)" +} + +function test_discard_with_committed_rename() { + echo "rename content" >before-rename.txt + git add before-rename.txt + git commit -q -m "Add file for rename test" + git mv before-rename.txt after-rename.txt + git commit -q -m "Rename file" + echo "modified after rename" >after-rename.txt + _forgit_git_discard after-rename.txt + assert_same "rename content" "$(cat after-rename.txt)" +} + +function test_discard_passes_through_arguments_when_non_flags_provided() { + echo "original" >passthrough.txt + git add passthrough.txt + git commit -q -m "Add passthrough file" + echo "modified" >passthrough.txt + git add passthrough.txt + _forgit_discard passthrough.txt + assert_same "original" "$(cat passthrough.txt)" +}