diff --git a/project.sh b/project.sh index 937efa1..6fc9feb 100755 --- a/project.sh +++ b/project.sh @@ -6,40 +6,46 @@ projects_dir=$HOME/Projects # Print the sorted, de-duplicated list of selectable projects. list_projects() { - local projects=() dir relative git_dirs kept_roots=() root kept + local projects=() dir relative clone worktree projects_real + + # git reports symlink-resolved paths, which need not spell $projects_dir the + # way we do, so keep the resolved form around to match them against too. + projects_real=$(cd "$projects_dir" 2>/dev/null && pwd -P) || + projects_real=$projects_dir - # All depth-2 directories without @ are included unconditionally for dir in "$projects_dir"/*/*/; do [ -d "$dir" ] || continue relative="${dir#$projects_dir/}" relative="${relative%/}" - [[ "$relative" != *@* ]] && projects+=("$relative") + + # All depth-2 directories without @ are included unconditionally + if [[ "$relative" != *@* ]]; then + projects+=("$relative") + continue + fi + + # A `repo@branch` directory is the parent of the worktrees hanging off the + # `repo` clone, nested one level deeper per `/` in the branch name. Ask + # that clone which worktrees it has instead of scanning: git already knows, + # whereas a scan deep enough to reach them has to walk every working tree + # under $projects_dir to find a handful of entries. Submodules aren't + # worktrees, so they never turn up and need no filtering out. `git worktree + # list` also names the clone itself, which the branch above already added — + # the final sort -u collapses the pair. + clone="$projects_dir/${relative%%@*}" + while IFS= read -r worktree; do + case $worktree in + "$projects_dir"/*) worktree=${worktree#"$projects_dir"/} ;; + "$projects_real"/*) worktree=${worktree#"$projects_real"/} ;; + *) continue ;; + esac + # Registered but deleted worktrees linger until `git worktree prune`. + [ -d "$projects_dir/$worktree" ] || continue + projects+=("$worktree") + done < <(git -C "$clone" worktree list --porcelain 2>/dev/null | + sed -n 's|^worktree ||p') done - # For @ directories, only show the top-most directory that holds a .git - # entry. Sorting makes every parent precede its descendants, so once a root - # is kept we drop anything nested beneath it — typically a git submodule — - # while separate clones and worktrees, each in their own directory, survive. - if command -v fd &>/dev/null; then - git_dirs=$(fd -H '^\.git$' "$projects_dir" || true) - else - git_dirs=$(find "$projects_dir" \( -name 'node_modules' -o -name '.git' \) \ - -prune -name '.git' -print || true) - fi - - while IFS= read -r root; do - for kept in "${kept_roots[@]}"; do - if [[ "$root" == "$kept/"* ]]; then - continue 2 - fi - done - kept_roots+=("$root") - projects+=("$root") - done < <(printf '%s\n' "$git_dirs" | - grep '@' | - sed -E -e "s|^$projects_dir/||" -e 's|/\.git/?$||' | - sort -u) - printf '%s\n' "${projects[@]}" | sort -u }