Something went wrong. Try again.
Dotfiles for my macos setup
Something went wrong. Try again.
16 kB · 515 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516#!/bin/bash## git-repo-manager - Find and manage git repositories recursively#set -e# Source utilitiesSOURCE_DIR=$(chezmoi source-path 2>/dev/null || echo "$HOME/.local/share/chezmoi")if [[ -f "$SOURCE_DIR/scripts/utils" ]]; then # shellcheck source=/dev/null . "$SOURCE_DIR/scripts/utils"else # Minimal fallback if utils not available print_error() { echo "❌ $1"; } print_success() { echo "✅ $1"; } print_warning() { echo "⚠️ $1"; } print_info() { echo "ℹ️ $1"; } cmd_exists() { command -v "$1" &>/dev/null; } show_header() { echo "$1"; }fi# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -print_help() { cat <<HELPEOFUsage: git-repo-manager [OPTIONS] [DIRECTORY]Find and manage git repositories recursively.Arguments: [DIRECTORY] Directory to search (default: current directory)Options: --help, -h Show this help message --depth, -d DEPTH Maximum search depth (default: 3) --interactive, -i Interactive mode with gum (default) --list, -l List only, no interactionExamples: git-repo-manager git-repo-manager ~/projects git-repo-manager --depth 5 ~/code git-repo-manager --listHELPEOF}find_git_repos() { local search_dir="$1" local max_depth="${2:-3}" find "$search_dir" -maxdepth "$max_depth" -type d -name ".git" 2>/dev/null | while read -r git_dir; do dirname "$git_dir" done}get_repo_info() { local repo_path="$1" local repo_name local last_commit_date local last_commit_msg local branch local remote_url local status local has_changes=false local default_branch="" local is_default_outdated=false repo_name=$(basename "$repo_path") cd "$repo_path" || return 1 # Get current branch branch=$(git branch --show-current 2>/dev/null || echo "detached") # Get last commit info last_commit_date=$(git log -1 --format="%ar" 2>/dev/null || echo "unknown") last_commit_msg=$(git log -1 --format="%s" 2>/dev/null | cut -c1-50) # Get remote URL remote_url=$(git config --get remote.origin.url 2>/dev/null || echo "no remote") # Check for changes if ! git diff-index --quiet HEAD -- 2>/dev/null || [[ -n $(git ls-files --others --exclude-standard 2>/dev/null) ]]; then has_changes=true status="🔴 dirty" else status="🟢 clean" fi # Check if default branch is outdated if [[ "$remote_url" != "no remote" ]]; then # Fetch quietly to get latest remote info git fetch --quiet 2>/dev/null || true # Get default branch from remote default_branch=$(git symbolic-ref "refs/remotes/origin/HEAD" 2>/dev/null | sed 's@^refs/remotes/origin/@@') if [[ -n "$default_branch" ]] && [[ "$branch" != "$default_branch" ]]; then # Check if default branch exists locally if git show-ref --verify --quiet "refs/heads/$default_branch" 2>/dev/null; then # Compare local default branch with remote local local_hash local remote_hash local_hash=$(git rev-parse "$default_branch" 2>/dev/null) remote_hash=$(git rev-parse "origin/$default_branch" 2>/dev/null) if [[ "$local_hash" != "$remote_hash" ]]; then is_default_outdated=true fi fi fi fi echo "$repo_path|$repo_name|$branch|$last_commit_date|$last_commit_msg|$remote_url|$status|$has_changes|$default_branch|$is_default_outdated"}open_repo_in_browser() { local remote_url="$1" # Convert git URL to https local https_url="$remote_url" if [[ "$remote_url" =~ ^git@github\.com:(.+)\.git$ ]]; then https_url="https://github.com/${BASH_REMATCH[1]}" elif [[ "$remote_url" =~ ^git@gitlab\.com:(.+)\.git$ ]]; then https_url="https://gitlab.com/${BASH_REMATCH[1]}" elif [[ "$remote_url" =~ ^git@bitbucket\.org:(.+)\.git$ ]]; then https_url="https://bitbucket.org/${BASH_REMATCH[1]}" elif [[ "$remote_url" =~ ^https://.*\.git$ ]]; then https_url="${remote_url%.git}" fi if [[ "$https_url" == "no remote" ]]; then print_error "No remote URL found" return 1 fi open "$https_url" print_success "Opened in browser"}delete_repo() { local repo_path="$1" local repo_name="$2" if cmd_exists "gum"; then if gum confirm "⚠️ Really delete '$repo_name'? This cannot be undone!"; then rm -rf "$repo_path" print_success "Deleted $repo_name" return 0 else print_info "Skipped deletion" return 1 fi else read -r -p "Really delete '$repo_name'? [Y/n] " confirm if [[ -z "$confirm" ]] || [[ "$confirm" =~ ^[Yy]$ ]]; then rm -rf "$repo_path" print_success "Deleted $repo_name" return 0 else print_info "Skipped deletion" return 1 fi fi}show_repo_details() { local repo_path="$1" local repo_name="$2" local branch="$3" local last_commit="$4" local commit_msg="$5" local remote_url="$6" local status="$7" local default_branch="$8" local is_default_outdated="$9" local outdated_msg="" if [[ "$is_default_outdated" == "true" ]]; then outdated_msg="⚠️ Default branch ($default_branch) is outdated" fi if cmd_exists "gum"; then if [[ -n "$outdated_msg" ]]; then gum style \ --foreground="$PURPLE" \ --border="rounded" \ --padding="1 2" \ --margin="1 0" \ "📁 $repo_name" \ "" \ "🌿 Branch: $branch" \ "⏰ Updated: $last_commit" \ "💬 $commit_msg" \ "🔗 $remote_url" \ "$status" \ "" \ "$outdated_msg" else gum style \ --foreground="$PURPLE" \ --border="rounded" \ --padding="1 2" \ --margin="1 0" \ "📁 $repo_name" \ "" \ "🌿 Branch: $branch" \ "⏰ Updated: $last_commit" \ "💬 $commit_msg" \ "🔗 $remote_url" \ "$status" fi else echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📁 $repo_name" echo "🌿 Branch: $branch" echo "⏰ Updated: $last_commit" echo "💬 $commit_msg" echo "🔗 $remote_url" echo "$status" [[ -n "$outdated_msg" ]] && echo "$outdated_msg" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" fi}checkout_default_branch() { local repo_path="$1" local default_branch="$2" cd "$repo_path" || return 1 if git checkout "$default_branch" 2>/dev/null && git pull 2>/dev/null; then print_success "Checked out and updated $default_branch" return 0 else print_error "Failed to checkout $default_branch" return 1 fi}clean_repo() { local repo_path="$1" local repo_name="$2" cd "$repo_path" || return 1 # Show what would be cleaned local untracked untracked=$(git ls-files --others --exclude-standard 2>/dev/null) local modified modified=$(git diff --name-only 2>/dev/null) local staged staged=$(git diff --cached --name-only 2>/dev/null) if [[ -z "$untracked" ]] && [[ -z "$modified" ]] && [[ -z "$staged" ]]; then print_info "Repository is already clean" return 1 fi if cmd_exists "gum"; then local msg="Changes to be removed:" [[ -n "$modified" ]] && msg="$msg\n\nModified:\n$modified" [[ -n "$staged" ]] && msg="$msg\n\nStaged:\n$staged" [[ -n "$untracked" ]] && msg="$msg\n\nUntracked:\n$untracked" echo -e "$msg" | gum style --foreground="$ORANGE" echo "" if gum confirm "🧹 Clean repository? (discard all changes)"; then git reset --hard HEAD 2>/dev/null git clean -fd 2>/dev/null print_success "Repository cleaned" return 0 else print_info "Skipped cleaning" return 1 fi else echo "Changes to be removed:" [[ -n "$modified" ]] && echo -e "\nModified:\n$modified" [[ -n "$staged" ]] && echo -e "\nStaged:\n$staged" [[ -n "$untracked" ]] && echo -e "\nUntracked:\n$untracked" read -r -p "Clean repository? (discard all changes) [Y/n] " confirm if [[ -z "$confirm" ]] || [[ "$confirm" =~ ^[Yy]$ ]]; then git reset --hard HEAD 2>/dev/null git clean -fd 2>/dev/null print_success "Repository cleaned" return 0 else print_info "Skipped cleaning" return 1 fi fi}interactive_manage() { local repo_info="$1" IFS='|' read -r repo_path repo_name branch last_commit commit_msg remote_url status has_changes default_branch is_default_outdated <<<"$repo_info" local should_continue=true while [[ "$should_continue" == "true" ]]; do show_repo_details "$repo_path" "$repo_name" "$branch" "$last_commit" "$commit_msg" "$remote_url" "$status" "$default_branch" "$is_default_outdated" if cmd_exists "gum"; then local -a actions=("⏭️ Next" "🔗 Open in browser" "🗑️ Delete" "❌ Quit") # Add checkout default option if outdated if [[ "$is_default_outdated" == "true" ]]; then actions=("⏭️ Next" "🔄 Checkout $default_branch" "🔗 Open in browser" "🗑️ Delete" "❌ Quit") fi # Add clean option if dirty if [[ "$has_changes" == "true" ]]; then if [[ "$is_default_outdated" == "true" ]]; then actions=("⏭️ Next" "🔄 Checkout $default_branch" "🧹 Clean repo" "🔗 Open in browser" "🗑️ Delete" "❌ Quit") else actions=("⏭️ Next" "🧹 Clean repo" "🔗 Open in browser" "🗑️ Delete" "❌ Quit") fi fi local action action=$(gum choose "${actions[@]}") case "$action" in "⏭️ Next") should_continue=false ;; "🔄 Checkout $default_branch") if checkout_default_branch "$repo_path" "$default_branch"; then should_continue=false fi ;; "🧹 Clean repo") if clean_repo "$repo_path" "$repo_name"; then should_continue=false fi ;; "🔗 Open in browser") open_repo_in_browser "$remote_url" ;; "🗑️ Delete") if delete_repo "$repo_path" "$repo_name"; then should_continue=false fi ;; "❌ Quit") exit 0 ;; esac else echo "" echo "Actions:" local choice_num=1 echo " $choice_num) Next" ((choice_num++)) local checkout_choice="" if [[ "$is_default_outdated" == "true" ]]; then echo " $choice_num) Checkout $default_branch" checkout_choice=$choice_num ((choice_num++)) fi local clean_choice="" if [[ "$has_changes" == "true" ]]; then echo " $choice_num) Clean repo" clean_choice=$choice_num ((choice_num++)) fi local open_choice=$choice_num echo " $choice_num) Open in browser" ((choice_num++)) local delete_choice=$choice_num echo " $choice_num) Delete" ((choice_num++)) local quit_choice=$choice_num echo " $choice_num) Quit" read -r -p "Choose [1-$choice_num]: " choice if [[ "$choice" == "1" ]]; then should_continue=false elif [[ -n "$checkout_choice" ]] && [[ "$choice" == "$checkout_choice" ]]; then if checkout_default_branch "$repo_path" "$default_branch"; then should_continue=false fi elif [[ -n "$clean_choice" ]] && [[ "$choice" == "$clean_choice" ]]; then if clean_repo "$repo_path" "$repo_name"; then should_continue=false fi elif [[ "$choice" == "$open_choice" ]]; then open_repo_in_browser "$remote_url" elif [[ "$choice" == "$delete_choice" ]]; then if delete_repo "$repo_path" "$repo_name"; then should_continue=false fi elif [[ "$choice" == "$quit_choice" ]]; then exit 0 fi fi done}list_repos() { local repos=("$@") if cmd_exists "gum"; then gum style --foreground="$CYAN" --bold "📦 Found ${#repos[@]} repositories:" echo "" else echo "Found ${#repos[@]} repositories:" fi for repo_info in "${repos[@]}"; do IFS='|' read -r repo_path repo_name branch last_commit commit_msg remote_url status has_changes default_branch is_default_outdated <<<"$repo_info" local outdated_indicator="" [[ "$is_default_outdated" == "true" ]] && outdated_indicator=" ⚠️" if cmd_exists "gum"; then gum style \ --foreground="$GREEN" \ " $status $repo_name$outdated_indicator" \ " 🌿 $branch | ⏰ $last_commit" else echo " $status $repo_name$outdated_indicator ($branch, $last_commit)" fi done}main() { local search_dir="." local max_depth=3 local mode="interactive" while [[ $# -gt 0 ]]; do case "$1" in --help | -h) print_help exit 0 ;; --depth | -d) max_depth="$2" shift 2 ;; --interactive | -i) mode="interactive" shift ;; --list | -l) mode="list" shift ;; -*) print_error "Unknown option: $1" print_help exit 1 ;; *) search_dir="$1" shift ;; esac done if [[ ! -d "$search_dir" ]]; then print_error "Directory not found: $search_dir" exit 1 fi # Find all git repos if cmd_exists "gum"; then print_info "🔍 Searching for git repositories..." else echo "Searching for git repositories..." fi local -a repo_infos=() while IFS= read -r repo_path; do local repo_info repo_info=$(get_repo_info "$repo_path") [[ -n "$repo_info" ]] && repo_infos+=("$repo_info") done < <(find_git_repos "$search_dir" "$max_depth") if [[ ${#repo_infos[@]} -eq 0 ]]; then print_warning "No git repositories found" exit 0 fi # Sort repos alphabetically by name local -a sorted_repos=() while IFS= read -r line; do sorted_repos+=("$line") done < <(printf '%s\n' "${repo_infos[@]}" | sort -t'|' -k2) repo_infos=("${sorted_repos[@]}") if [[ "$mode" == "list" ]]; then list_repos "${repo_infos[@]}" else # Interactive mode for repo_info in "${repo_infos[@]}"; do interactive_manage "$repo_info" done print_success "✨ All repositories processed" fi}main "$@"