perf: speed up modified file lookup in large repositories (#544)

Use a modified-file-only path for `git-forgit restore` (`grs`) and
`git-forgit checkout_file` (`gcf`) so these interactive selectors do not scan
large sets of untracked files. This keeps both commands responsive in
repositories with many untracked files.
This commit is contained in:
Chris Apple 2026-07-13 10:05:53 -07:00 committed by GitHub
parent e0c7b844b5
commit a8e9d8bf57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 123 additions and 25 deletions

View file

@ -222,6 +222,61 @@ _forgit_list_files() {
git ls-files -z "$@" "$rootdir" | tr '\0' '\n' | uniq
}
_forgit_rewrite_repo_paths_for_cwd() {
local rootdir mode separator cwd
rootdir=$1
mode=${2:-paths}
separator=${3:-}
cwd=$(pwd -P)
perl -MCwd=realpath -MFile::Spec -e '
use strict;
use warnings;
my ($rootdir, $cwd, $mode, $separator) = @ARGV;
my $normalized_rootdir = realpath($rootdir);
my $normalized_cwd = realpath($cwd);
while (my $line = <STDIN>) {
chomp $line;
my ($status, $repo_path);
if ($mode eq q{status_entries}) {
next unless $line =~ /^(..[^[:space:]]*)( )(.*)$/;
($status, $repo_path) = ($1, $3);
} else {
next if $line eq q{};
$repo_path = $line;
}
my $absolute_path = "$normalized_rootdir/$repo_path";
my $display_path = File::Spec->abs2rel(realpath($absolute_path) // $absolute_path, $normalized_cwd);
$display_path = q{.} if $display_path eq q{};
if ($mode eq q{status_entries}) {
print "[$status] ${display_path}${separator}${normalized_rootdir}/${repo_path}\n";
} else {
print "$display_path\n";
}
}
' "$rootdir" "$cwd" "$mode" "$separator"
}
_forgit_list_modified_files() {
# on large repos with many untracked files, git ls-files
# is substantially slower than git diff
# Wherever possible (when we don't care about untracked files)
# use this command over _forgit_list_files
# See note on _forgit_list_files() about -z and other options
local rootdir
rootdir=$(git rev-parse --show-toplevel)
git -C "$rootdir" diff --name-only -z |
tr '\0' '\n' |
uniq |
_forgit_rewrite_repo_paths_for_cwd "$rootdir"
}
_forgit_list_staged_files() {
local up
up="$(git rev-parse --show-cdup)"
@ -291,32 +346,12 @@ _forgit_restore_untracked_color() {
# 1. the human-readable status line shown in fzf
# 2. the absolute-path payload used by preview/edit/add actions
_forgit_build_status_entries() {
local cwd rootdir
local rootdir
rootdir=$1
cwd=$(pwd -P)
# Use a single Perl process so path normalization stays portable while the
# full add-list transformation still runs as one batch pipeline stage.
perl -MCwd=realpath -MFile::Spec -e '
use strict;
use warnings;
my ($rootdir, $cwd, $separator) = @ARGV;
my $normalized_rootdir = realpath($rootdir);
my $normalized_cwd = realpath($cwd);
while (my $line = <STDIN>) {
chomp $line;
next unless $line =~ /^(..[^[:space:]]*)( )(.*)$/;
my ($status, $repo_path) = ($1, $3);
my $absolute_path = "$normalized_rootdir/$repo_path";
my $display_path = File::Spec->abs2rel(realpath($absolute_path) // $absolute_path, $normalized_cwd);
$display_path = "." if $display_path eq q{};
print "[$status] ${display_path}${separator}${normalized_rootdir}/${repo_path}\n";
}
' "$rootdir" "$cwd" "$_ffsep"
_forgit_rewrite_repo_paths_for_cwd "$rootdir" status_entries "$_ffsep"
}
_forgit_is_submodule() {
@ -739,7 +774,7 @@ _forgit_restore() {
if [[ $worktree == true || $staged != true ]]; then
while IFS='' read -r file; do
candidates+=("$file")
done < <(_forgit_list_files --modified)
done < <(_forgit_list_modified_files)
fi
[[ ${#candidates[@]} -eq 0 ]] && echo "Nothing to restore." && return 1
@ -1093,7 +1128,7 @@ _forgit_checkout_file() {
_forgit_git_checkout_file "$@"
return $?
}
[[ $(_forgit_list_files --modified | wc -l) -eq 0 ]] && echo 'Nothing to checkout.' && return 1
[[ $(_forgit_list_modified_files | wc -l) -eq 0 ]] && echo 'Nothing to checkout.' && return 1
opts="
$FORGIT_FZF_DEFAULT_OPTS
-m -0
@ -1103,7 +1138,7 @@ _forgit_checkout_file() {
files=()
while IFS='' read -r file; do
files+=("$file")
done < <(_forgit_list_files --modified |
done < <(_forgit_list_modified_files |
FZF_DEFAULT_OPTS="$opts" fzf)
[[ ${#files[@]} -gt 0 ]] && _forgit_git_checkout_file "$@" "${files[@]}"
}

View file

@ -44,6 +44,28 @@ function test_forgit_worktree_changes_contains_untracked() {
assert_contains "untracked_file.txt" "$output"
}
function test_forgit_list_files_matches_modified_files_until_untracked_files_exist() {
local list_files_output modified_files_output
list_files_output=$(_forgit_list_files --modified)
modified_files_output=$(_forgit_list_modified_files)
assert_same "$modified_files_output" "$list_files_output"
assert_contains "modified file.txt" "$list_files_output"
touch new_untracked_file.txt
list_files_output=$(_forgit_list_files --exclude-standard --modified --others)
modified_files_output=$(_forgit_list_modified_files)
assert_contains "modified file.txt" "$list_files_output"
assert_contains "new_untracked_file.txt" "$list_files_output"
assert_contains "modified file.txt" "$modified_files_output"
assert_not_contains "new_untracked_file.txt" "$modified_files_output"
rm new_untracked_file.txt
}
function test_forgit_worktree_changes_excludes_staged() {
local output
@ -114,6 +136,47 @@ function test_forgit_build_status_entries_uses_cwd_relative_display_paths_for_si
assert_contains "../dir1/file.txt" "$output"
}
function test_forgit_list_modified_files_uses_cwd_relative_paths_for_sibling_entries() {
local output
mkdir -p dir1 dir2
touch dir1/file.txt
git add .
git commit -m "add dirs" --quiet
echo modified >dir1/file.txt
cd dir2 || return 1
output=$(_forgit_list_modified_files)
assert_same "../dir1/file.txt" "$output"
}
function test_forgit_rewrite_repo_paths_for_cwd_rewrites_repo_relative_paths() {
local output rootdir
mkdir -p dir1 dir2
touch dir1/file.txt
rootdir=$(git rev-parse --show-toplevel)
cd dir2 || return 1
output=$(printf 'dir1/file.txt\n' | _forgit_rewrite_repo_paths_for_cwd "$rootdir")
assert_same "../dir1/file.txt" "$output"
}
function test_forgit_rewrite_repo_paths_for_cwd_formats_status_entries() {
local output rootdir
mkdir -p dir1 dir2
touch dir1/file.txt
rootdir=$(git rev-parse --show-toplevel)
cd dir2 || return 1
output=$(printf ' M dir1/file.txt\n' | _forgit_rewrite_repo_paths_for_cwd "$rootdir" status_entries "$_ffsep")
assert_same "[ M] ../dir1/file.txt${_ffsep}${rootdir}/dir1/file.txt" "$output"
}
function test_forgit_build_status_entries_uses_cwd_relative_display_paths_from_logical_symlink_paths() {
local output sandbox