Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright © 2017-2021 Wenxuan Zhang <wenxuangm@gmail.com>
Copyright © 2017-2026 Wenxuan Zhang <wenxuangm@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ If you're having issues after updating, and commands such as `forgit::add` or al

- **Interactive `git reset HEAD <file>` selector** (`grh`)

- **Interactive `git restore <file>` selector** (`grs`)

- **Interactive `git discard <file>` 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 <file>` selector** (`gcf`)

- **Interactive `git checkout <branch>` selector** (`gcb`)
Expand Down Expand Up @@ -222,6 +231,8 @@ forgit_diff=gd
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
Expand Down Expand Up @@ -286,6 +297,8 @@ 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` |
| `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` |
Expand Down Expand Up @@ -346,6 +359,8 @@ 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` |
| `gdc` | `FORGIT_DISCARD_FZF_OPTS` |
| `gcf` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` |
| `gcb` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` |
| `gsw` | `FORGIT_SWITCH_BRANCH_FZF_OPTS` |
Expand Down
72 changes: 72 additions & 0 deletions bin/git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,74 @@ _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_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)
Expand Down Expand Up @@ -1303,6 +1371,8 @@ PUBLIC_COMMANDS=(
"reflog"
"rebase"
"reset_head"
"restore"
"discard"
"revert_commit"
"show"
"stash_show"
Expand All @@ -1324,6 +1394,8 @@ PRIVATE_COMMANDS=(
"path_preview"
"revert_preview"
"reset_head_preview"
"restore_preview"
"discard_preview"
"show_enter"
"show_preview"
"stash_push_preview"
Expand Down
4 changes: 4 additions & 0 deletions completions/_git-forgit
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 ;;
Expand Down Expand Up @@ -126,6 +128,8 @@ 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-checkout-file forgit::discard
compdef __git_recent_commits forgit::revert::commit
compdef _git-log forgit::reword
compdef _git-log forgit::squash
Expand Down
6 changes: 6 additions & 0 deletions completions/git-forgit.bash
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ _git_forgit()
reflog
rebase
reset_head
restore
revert_commit
reword
show
Expand Down Expand Up @@ -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 ;;
Expand Down Expand Up @@ -141,6 +143,8 @@ 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::discard _git_checkout_file
__git_complete forgit::revert::commit _git_revert
__git_complete forgit::reword _git_log
__git_complete forgit::show _git_show
Expand All @@ -164,6 +168,8 @@ 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_discard}" _git_checkout_file
__git_complete "${forgit_revert_commit}" _git_revert
__git_complete "${forgit_reword}" _git_log
__git_complete "${forgit_show}" _git_show
Expand Down
6 changes: 5 additions & 1 deletion completions/git-forgit.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +39,8 @@ 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 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'
Expand All @@ -61,6 +63,8 @@ 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 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 ')"
Expand Down
2 changes: 2 additions & 0 deletions conf.d/forgit.plugin.fish
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ 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_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
Expand Down
12 changes: 12 additions & 0 deletions forgit.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ forgit::reset::head() {
"$FORGIT" reset_head "$@"
}

forgit::restore() {
"$FORGIT" restore "$@"
}

forgit::discard() {
"$FORGIT" discard "$@"
}

forgit::stash::show() {
"$FORGIT" stash_show "$@"
}
Expand Down Expand Up @@ -166,6 +174,8 @@ 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}"
Expand All @@ -191,6 +201,8 @@ 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'
Expand Down
73 changes: 73 additions & 0 deletions tests/discard.test.sh
Original file line number Diff line number Diff line change
@@ -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)"
}
Loading