diff --git a/README.md b/README.md index ca0cf68f..78c2f100 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ It's **lightweight** and **easy to use**. | `gso` | Interactive `git show` viewer | | `grh` | Interactive `git reset HEAD ` selector | | `gcf` | Interactive `git checkout ` selector | +| `gcff` | Interactive `git checkout from ` selector | | `gcb` | Interactive `git checkout ` selector | | `gsw` | Interactive `git switch ` selector | | `gbd` | Interactive `git branch -D ` selector | @@ -188,6 +189,7 @@ forgit_reset_head=grh forgit_ignore=gi forgit_attributes=gat forgit_checkout_file=gcf +forgit_checkout_file_from_commit=gcff forgit_checkout_branch=gcb forgit_switch_branch=gsw forgit_branch_delete=gbd @@ -266,6 +268,7 @@ Each forgit command can be customized with dedicated environment variables for g | `gso` | `FORGIT_SHOW_GIT_OPTS` | `FORGIT_SHOW_FZF_OPTS` | | `grh` | `FORGIT_RESET_HEAD_GIT_OPTS` | `FORGIT_RESET_HEAD_FZF_OPTS` | | `gcf` | `FORGIT_CHECKOUT_FILE_GIT_OPTS` | `FORGIT_CHECKOUT_FILE_FZF_OPTS` | +| `gcff` | `FORGIT_SHOW_GIT_OPTS`
`FORGIT_CHECKOUT_FILE_GIT_OPTS` | `FORGIT_CHECKOUT_FILE_FROM_COMMIT_LOG_FZF_OPTS`
`FORGIT_CHECKOUT_FILE_FROM_COMMIT_SHOW_FZF_OPTS` | | `gcb` | `FORGIT_CHECKOUT_BRANCH_GIT_OPTS`
`FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS` | `FORGIT_CHECKOUT_BRANCH_FZF_OPTS` | | `gsw` | `FORGIT_SWITCH_BRANCH_GIT_OPTS` | `FORGIT_SWITCH_BRANCH_FZF_OPTS` | | `gbd` | `FORGIT_BRANCH_DELETE_GIT_OPTS` | `FORGIT_BRANCH_DELETE_FZF_OPTS` | diff --git a/bin/git-forgit b/bin/git-forgit index c50994a6..948e7d6a 100755 --- a/bin/git-forgit +++ b/bin/git-forgit @@ -231,10 +231,22 @@ _forgit_log_enter() { "${FORGIT}" show "${sha}" "$@" } +_forgit_git_log() { + local graph log_format + log_format=$1 + shift + graph=() + [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) + _forgit_log_git_opts=() + _forgit_parse_array _forgit_log_git_opts "$FORGIT_LOG_GIT_OPTS" + git log "${graph[@]}" --color=always --format="$log_format" "${_forgit_log_git_opts[@]}" "$@" | + _forgit_emojify +} + # git commit viewer _forgit_log() { _forgit_inside_work_tree || return 1 - local opts graph quoted_files log_format + local opts quoted_files log_format quoted_files=$(_forgit_quote_files "$@") opts=" $FORGIT_FZF_DEFAULT_OPTS @@ -244,14 +256,8 @@ _forgit_log() { --preview=\"$FORGIT preview log_preview {} $quoted_files\" $FORGIT_LOG_FZF_OPTS " - graph=() - [[ $_forgit_log_graph_enable == true ]] && graph=(--graph) log_format=${FORGIT_GLO_FORMAT:-$_forgit_log_format} - _forgit_log_git_opts=() - _forgit_parse_array _forgit_log_git_opts "$FORGIT_LOG_GIT_OPTS" - git log "${graph[@]}" --color=always --format="$log_format" "${_forgit_log_git_opts[@]}" "$@" | - _forgit_emojify | - FZF_DEFAULT_OPTS="$opts" fzf + _forgit_git_log "$log_format" "$@" | FZF_DEFAULT_OPTS="$opts" fzf fzf_exit_code=$? # exit successfully on 130 (ctrl-c/esc) [[ $fzf_exit_code == 130 ]] && return 0 @@ -424,6 +430,19 @@ _forgit_show_enter() { _forgit_show_view "$file" "$_forgit_fullscreen_context" "${commit}" } +_forgit_git_show() { + local commit=$1 + shift + + _forgit_show_git_opts=() + _forgit_parse_array _forgit_show_git_opts "$FORGIT_SHOW_GIT_OPTS" + # Add "^{commit}" suffix after the actual commit. This suppresses the tag information in case it is a tag. + # See: https://git-scm.com/docs/git-show#Documentation/git-show.txt-codegitshow-s--formatsv100commitcode + git show --pretty="" --name-status --diff-merges=first-parent "${_forgit_show_git_opts[@]}" "${commit}^{commit}" -- "$@" | + sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1] \2/' | + sed 's/ / -> /2' | expand -t 8 +} + # git show viewer _forgit_show() { _forgit_inside_work_tree || return 1 @@ -452,15 +471,7 @@ _forgit_show() { $FORGIT_SHOW_FZF_OPTS --prompt=\"${commit} > \" " - _forgit_show_git_opts=() - _forgit_parse_array _forgit_show_git_opts "$FORGIT_SHOW_GIT_OPTS" - # Add "^{commit}" suffix after the actual commit. This suppresses the tag information in case it is a tag. - # See: https://git-scm.com/docs/git-show#Documentation/git-show.txt-codegitshow-s--formatsv100commitcode - git show --pretty="" --name-status --diff-merges=first-parent "${_forgit_show_git_opts[@]}" "${commit}^{commit}" \ - -- "${files[@]}" | - sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1] \2/' | - sed 's/ / -> /2' | expand -t 8 | - FZF_DEFAULT_OPTS="$opts" fzf + _forgit_git_show "$commit" "${files[@]}" | FZF_DEFAULT_OPTS="$opts" fzf fzf_exit_code=$? # exit successfully on 130 (ctrl-c/esc) [[ $fzf_exit_code == 130 ]] && return 0 @@ -909,6 +920,54 @@ _forgit_checkout_file() { [[ "${#files[@]}" -gt 0 ]] && _forgit_git_checkout_file "$@" "${files[@]}" } +# git checkout-file from commit selector +_forgit_checkout_file_from_commit() { + _forgit_inside_work_tree || return 1 + local opts commit file branch + + if [[ $# -gt 0 ]]; then + branch=$1 + shift + else + # default to the current branch if none was passed + branch=$(git rev-parse --abbrev-ref HEAD) + fi + + # select the commit interactively + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +s +m --tiebreak=index + --preview=\"$FORGIT log_preview {}\" + $FORGIT_CHECKOUT_FILE_FROM_COMMIT_LOG_FZF_OPTS + " + commit=$(_forgit_git_log "$_forgit_log_format" "$branch" "$@" | + FZF_DEFAULT_OPTS="$opts" fzf | + _forgit_extract_sha) + [[ -n "$commit" ]] || return 0 + + # select the file interactively + opts=" + $FORGIT_FZF_DEFAULT_OPTS + +m -0 + --preview=\"$FORGIT preview show_preview {} '$_forgit_preview_context' $commit\" + --preview-label=\" Diff \" + --bind=\"alt-t:transform:[[ ! \\\"\$FZF_PREVIEW_LABEL\\\" =~ 'Diff' ]] && + echo 'change-preview-label( Diff )+refresh-preview' || + echo 'change-preview-label( Commit Message )+refresh-preview'\" + $FORGIT_CHECKOUT_FILE_FROM_COMMIT_SHOW_FZF_OPTS + --prompt=\"${commit} > \" + " + file=$(_forgit_git_show "$commit" | FZF_DEFAULT_OPTS="$opts" fzf) + [[ -n "$file" ]] || return 0 + + # special case: when the file was deleted in the commit + # check out the file from the previous commit. + [[ "$file" =~ ^\[D\] ]] && commit="$commit~" + file=$(echo "$file" | _forgit_get_single_file_from_diff_line) + + _forgit_git_checkout_file "$commit" -- "$file" +} + _forgit_git_checkout_branch() { _forgit_checkout_branch_git_opts=() _forgit_parse_array _forgit_checkout_branch_git_opts "$FORGIT_CHECKOUT_BRANCH_GIT_OPTS" @@ -1532,6 +1591,7 @@ PUBLIC_COMMANDS=( "switch_branch" "checkout_commit" "checkout_file" + "checkout_file_from_commit" "checkout_tag" "cherry_pick" "cherry_pick_from_branch" diff --git a/completions/_git-forgit b/completions/_git-forgit index ac5d6d0d..08bb79b7 100644 --- a/completions/_git-forgit +++ b/completions/_git-forgit @@ -73,6 +73,7 @@ _git-forgit() { 'checkout_branch:git checkout branch selector' 'checkout_commit:git checkout commit selector' 'checkout_file:git checkout-file selector' + 'checkout_file_from_commit:git checkout-file from commit selector' 'checkout_tag:git checkout tag selector' 'cherry_pick:git cherry-picking' 'cherry_pick_from_branch:git cherry-picking with interactive branch selection' @@ -104,6 +105,7 @@ _git-forgit() { checkout_branch) _git-branches ;; checkout_commit) __git_recent_commits ;; checkout_file) _git-checkout-file ;; + checkout_file_from_commit) _git-branches ;; checkout_tag) __git_tags ;; cherry_pick) _git-cherry-pick ;; cherry_pick_from_branch) _git-branches ;; @@ -138,6 +140,7 @@ compdef _git-branches forgit::checkout::branch compdef _git-switch forgit::switch::branch compdef __git_recent_commits forgit::checkout::commit compdef _git-checkout-file forgit::checkout::file +compdef _git-branches forgit::checkout::file::from::commit compdef __git_tags forgit::checkout::tag compdef _git-cherry-pick forgit::cherry::pick compdef _git-branches forgit::cherry::pick::from::branch diff --git a/completions/git-forgit.bash b/completions/git-forgit.bash index 16573b02..ef467bc0 100755 --- a/completions/git-forgit.bash +++ b/completions/git-forgit.bash @@ -73,6 +73,7 @@ _git_forgit() checkout_branch checkout_commit checkout_file + checkout_file_from_commit checkout_tag cherry_pick cherry_pick_from_branch @@ -109,6 +110,7 @@ _git_forgit() checkout_branch) _git_checkout_branch ;; checkout_commit) _git_checkout ;; checkout_file) _git_checkout_file ;; + checkout_file_from_commit) _git_checkout_branch ;; checkout_tag) _git_checkout_tag ;; cherry_pick) _git_cherry_pick ;; cherry_pick_from_branch) _git_checkout_branch ;; @@ -152,6 +154,7 @@ then __git_complete forgit::switch::branch _git_switch __git_complete forgit::checkout::commit _git_checkout __git_complete forgit::checkout::file _git_checkout_file + __git_complete forgit::checkout::file::from::commit _git_checkout_branch __git_complete forgit::checkout::tag _git_checkout_tag __git_complete forgit::cherry::pick _git_cherry_pick __git_complete forgit::cherry::pick::from::branch _git_checkout_branch @@ -179,6 +182,7 @@ then __git_complete "${forgit_switch_branch}" _git_switch __git_complete "${forgit_checkout_commit}" _git_checkout __git_complete "${forgit_checkout_file}" _git_checkout_file + __git_complete "${forgit_checkout_file_from_commit}" _git_checkout_branch __git_complete "${forgit_checkout_tag}" _git_checkout_tag __git_complete "${forgit_cherry_pick}" _git_checkout_branch __git_complete "${forgit_clean}" _git_clean diff --git a/completions/git-forgit.fish b/completions/git-forgit.fish index 350d1de4..0aae12f9 100644 --- a/completions/git-forgit.fish +++ b/completions/git-forgit.fish @@ -6,8 +6,8 @@ # sourced when git-forgit command or forgit subcommand of git is invoked. 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 \ + for subcmd in add blame branch_delete checkout_branch checkout_commit checkout_file checkout_file_from_commit \ + 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 worktree worktree_add worktree_delete if contains -- $subcmd (commandline -opc) return 1 @@ -32,6 +32,7 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a branch_delete -d 'gi complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_branch -d 'git checkout branch selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_commit -d 'git checkout commit selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_file -d 'git checkout-file selector' +complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_file_from_commit -d 'git checkout-file from commit selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a checkout_tag -d 'git checkout tag selector' complete -c git-forgit -n __fish_forgit_needs_subcommand -a cherry_pick -d 'git cherry-picking' complete -c git-forgit -n __fish_forgit_needs_subcommand -a cherry_pick_from_branch -d 'git cherry-picking with interactive branch selection' @@ -59,6 +60,7 @@ complete -c git-forgit -n '__fish_seen_subcommand_from branch_delete' -a "(__fis complete -c git-forgit -n '__fish_seen_subcommand_from checkout_branch' -a "(complete -C 'git switch ')" complete -c git-forgit -n '__fish_seen_subcommand_from checkout_commit' -a "(__fish_git_commits)" complete -c git-forgit -n '__fish_seen_subcommand_from checkout_file' -a "(__fish_git_files modified)" +complete -c git-forgit -n '__fish_seen_subcommand_from checkout_file_from_commit' -a "(complete -C 'git switch ')" complete -c git-forgit -n '__fish_seen_subcommand_from checkout_tag' -a "(__fish_git_tags)" -d Tag complete -c git-forgit -n '__fish_seen_subcommand_from cherry_pick' -a "(complete -C 'git cherry-pick ')" complete -c git-forgit -n '__fish_seen_subcommand_from clean' -a "(__fish_git_files untracked ignored)" diff --git a/conf.d/forgit.plugin.fish b/conf.d/forgit.plugin.fish index 96bf7dc4..2fe5b09e 100644 --- a/conf.d/forgit.plugin.fish +++ b/conf.d/forgit.plugin.fish @@ -57,6 +57,7 @@ if test -z "$FORGIT_NO_ALIASES" abbr -a -- (string collect $forgit_ignore; or string collect "gi") git-forgit ignore abbr -a -- (string collect $forgit_attributes; or string collect "gat") git-forgit attributes abbr -a -- (string collect $forgit_checkout_file; or string collect "gcf") git-forgit checkout_file + abbr -a -- (string collect $forgit_checkout_file_from_commit; or string collect "gcff") git-forgit checkout_file_from_commit abbr -a -- (string collect $forgit_checkout_branch; or string collect "gcb") git-forgit checkout_branch abbr -a -- (string collect $forgit_switch_branch; or string collect "gsw") git-forgit switch_branch abbr -a -- (string collect $forgit_branch_delete; or string collect "gbd") git-forgit branch_delete diff --git a/forgit.plugin.zsh b/forgit.plugin.zsh index b312837e..962efa91 100755 --- a/forgit.plugin.zsh +++ b/forgit.plugin.zsh @@ -104,6 +104,10 @@ forgit::reword() { "$FORGIT" reword "$@" } +forgit::checkout::file::from::commit() { + "$FORGIT" checkout_file_from_commit "$@" +} + forgit::checkout::file() { "$FORGIT" checkout_file "$@" } @@ -191,6 +195,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin export forgit_ignore="${forgit_ignore:-gi}" builtin export forgit_attributes="${forgit_attributes:-gat}" builtin export forgit_checkout_file="${forgit_checkout_file:-gcf}" + builtin export forgit_checkout_file_from_commit="${forgit_checkout_file_from_commit:-gcff}" builtin export forgit_checkout_branch="${forgit_checkout_branch:-gcb}" builtin export forgit_switch_branch="${forgit_switch_branch:-gsw}" builtin export forgit_checkout_commit="${forgit_checkout_commit:-gco}" @@ -219,6 +224,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then builtin alias "${forgit_ignore}"='forgit::ignore' builtin alias "${forgit_attributes}"='forgit::attributes' builtin alias "${forgit_checkout_file}"='forgit::checkout::file' + builtin alias "${forgit_checkout_file_from_commit}"='forgit::checkout::file::from::commit' builtin alias "${forgit_checkout_branch}"='forgit::checkout::branch' builtin alias "${forgit_switch_branch}"='forgit::switch::branch' builtin alias "${forgit_checkout_commit}"='forgit::checkout::commit' diff --git a/tests/log.test.sh b/tests/log.test.sh new file mode 100644 index 00000000..39f114a2 --- /dev/null +++ b/tests/log.test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + # disable log graph to remove the asterisk from the output + export FORGIT_LOG_GRAPH_ENABLE='false' + + source bin/git-forgit + + # ignore global git config files + export GIT_CONFIG_SYSTEM=/dev/null + export GIT_CONFIG_GLOBAL=/dev/null + + # create a new git repository in a temp directory + cd "$(bashunit::temp_dir)" || return 1 + git init --quiet + git config user.email "test@example.com" + git config user.name "Test User" + # create an initial commit so we have a valid repo + git commit --allow-empty -qm "Initial commit" +} + +function test_forgit_git_log() { + local output + # set log format to '%s' so we don't have to match the commit hash + output=$(_forgit_git_log '%s' 2>&1 | sed 's/^[0-9a-f] //') + assert_same "Initial commit" "$output" +} diff --git a/tests/show.test.sh b/tests/show.test.sh new file mode 100644 index 00000000..e7a9f5fb --- /dev/null +++ b/tests/show.test.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +function set_up_before_script() { + source bin/git-forgit + + # ignore global git config files + export GIT_CONFIG_SYSTEM=/dev/null + export GIT_CONFIG_GLOBAL=/dev/null + + # create a new git repository in a temp directory + cd "$(bashunit::temp_dir)" || return 1 + git init --quiet + git config user.email "test@example.com" + git config user.name "Test User" + # create an initial commit so we have a valid repo + touch file.txt + touch file1.txt + git add . + git commit -qm "Initial commit" +} + +function test_forgit_git_show() { + local output + output=$(_forgit_git_show HEAD 2>&1) + assert_same "[A] file.txt +[A] file1.txt" "$output" +} + +function test_forgit_git_show_filter() { + local output + output=$(_forgit_git_show HEAD -- file.txt 2>&1) + assert_same "[A] file.txt" "$output" +}