diff --git a/whatsdirty b/whatsdirty index fd9f8f3..bb2b97e 100755 --- a/whatsdirty +++ b/whatsdirty @@ -6,6 +6,8 @@ # Configuration CONFIG_FILE="$HOME/.config/whatsdirty.conf" RED='\033[0;31m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' NC='\033[0m' # No Color # === Configuration Management Functions === @@ -57,6 +59,46 @@ handle_directory_argument() { # === Git Repository Functions === +fetch_remote_status() { + # Fetch remote updates without merging + # Using -q for quiet mode, timeout for hung connections + timeout 5 git fetch --all --quiet 2>/dev/null || true +} + +check_upstream_status() { + local current_branch + current_branch=$(git branch --show-current 2>/dev/null) + + if [ -z "$current_branch" ]; then + echo "DETACHED|0|0" + return + fi + + # Check if branch has upstream configured + local upstream + upstream=$(git rev-parse --abbrev-ref "@{upstream}" 2>/dev/null) + + if [ -z "$upstream" ]; then + echo "NO_UPSTREAM|0|0" + return + fi + + # Count commits ahead and behind + local ahead behind + ahead=$(git rev-list --count "@{upstream}..HEAD" 2>/dev/null || echo "0") + behind=$(git rev-list --count "HEAD..@{upstream}" 2>/dev/null || echo "0") + + if [ "$behind" -gt 0 ] && [ "$ahead" -gt 0 ]; then + echo "DIVERGED|$ahead|$behind" + elif [ "$behind" -gt 0 ]; then + echo "BEHIND|$ahead|$behind" + elif [ "$ahead" -gt 0 ]; then + echo "AHEAD|$ahead|$behind" + else + echo "UP_TO_DATE|0|0" + fi +} + get_last_commit_hours() { local last_commit_timestamp last_commit_timestamp=$(git log -1 --format="%ct" 2>/dev/null) @@ -131,7 +173,7 @@ analyze_repository() { local repo_path="$1" local base_dir="$2" local repo_name - + # Calculate relative path from base directory if [ -n "$base_dir" ]; then repo_name=$(realpath --relative-to="$base_dir" "$repo_path" 2>/dev/null) @@ -142,16 +184,25 @@ analyze_repository() { else repo_name=$(basename "$repo_path") fi - + cd "$repo_path" || return 1 - + + # Fetch remote updates first + fetch_remote_status + + # Check upstream status + local upstream_info + upstream_info=$(check_upstream_status) + local upstream_status upstream_ahead upstream_behind + IFS='|' read -r upstream_status upstream_ahead upstream_behind <<< "$upstream_info" + # Get commit info local hours_ago hours_ago=$(get_last_commit_hours) local hours_num local hours_label local hours_formatted - + if [ -n "$hours_ago" ]; then hours_num="$hours_ago" hours_label="hours" @@ -161,35 +212,152 @@ analyze_repository() { hours_label="commits" hours_formatted=$(printf "%8s %-7s" "No" "$hours_label") fi - + # Count changes local total_changes total_changes=$(count_git_changes) - + # Determine status and collect data if [ "$total_changes" -eq 0 ]; then - echo "C|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}" + echo "C|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}|${upstream_status}|${upstream_ahead}|${upstream_behind}" else local changed_files changed_files=$(get_changed_files_list) - echo "D|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}|||${changed_files}" + echo "D|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}|${upstream_status}|${upstream_ahead}|${upstream_behind}|||${changed_files}" fi } # === Output Formatting Functions === +format_upstream_status() { + local upstream_status="$1" + local ahead="$2" + local behind="$3" + + case "$upstream_status" in + "BEHIND") + echo "${YELLOW}↓${behind}${NC}" + ;; + "AHEAD") + echo "${BLUE}↑${ahead}${NC}" + ;; + "DIVERGED") + echo "${RED}↑${ahead}↓${behind}${NC}" + ;; + "UP_TO_DATE") + echo "✓" + ;; + "NO_UPSTREAM") + echo "--" + ;; + "DETACHED") + echo "DET" + ;; + *) + echo "?" + ;; + esac +} + +print_summary() { + local temp_file="$1" + local total_dirty total_clean total_behind total_ahead total_diverged + + total_dirty=$(grep -c "^D|" "$temp_file" 2>/dev/null || echo 0) + total_clean=$(grep -c "^C|" "$temp_file" 2>/dev/null || echo 0) + + # Count upstream statuses + total_behind=0 + total_ahead=0 + total_diverged=0 + + while IFS='|' read -r type _ _ _ _ upstream_status _ _; do + case "$upstream_status" in + "BEHIND") ((total_behind++)) ;; + "AHEAD") ((total_ahead++)) ;; + "DIVERGED") ((total_diverged++)) ;; + esac + done < "$temp_file" + + # Print summary + echo "=== SUMMARY ===" + echo "Total repositories: $((total_dirty + total_clean))" + + if [ "$total_dirty" -gt 0 ]; then + printf "${RED}Dirty: $total_dirty${NC}" + else + printf "Dirty: 0" + fi + + echo " | Clean: $total_clean" + + if [ "$total_behind" -gt 0 ] || [ "$total_diverged" -gt 0 ]; then + printf "${YELLOW}Needs pull: $((total_behind + total_diverged))${NC}" + if [ "$total_behind" -gt 0 ]; then + printf " (${YELLOW}Behind: $total_behind${NC}" + if [ "$total_diverged" -gt 0 ]; then + printf ", ${RED}Diverged: $total_diverged${NC}" + fi + printf ")" + elif [ "$total_diverged" -gt 0 ]; then + printf " (${RED}Diverged: $total_diverged${NC})" + fi + echo "" + fi + + if [ "$total_ahead" -gt 0 ]; then + printf "${BLUE}Ahead: $total_ahead${NC} (can push)\n" + fi + + echo "" +} + print_header() { - printf "%-40s %15s %-10s %-10s\n" "Repository" "Last Commit" "Changes" "Status" - printf "%-40s %15s %-10s %-10s\n" "----------" "(hours)" "-------" "------" + printf "%-40s %15s %-10s %-10s %-12s\n" "Repository" "Last Commit" "Changes" "Status" "Upstream" + printf "%-40s %15s %-10s %-10s %-12s\n" "----------" "(hours)" "-------" "------" "--------" +} + +print_repos_needing_pull() { + local temp_file="$1" + local needs_pull=false + + # Check if any repos need pulling + while IFS='|' read -r type _ _ repo _ upstream_status upstream_ahead upstream_behind rest; do + if [ "$upstream_status" = "BEHIND" ] || [ "$upstream_status" = "DIVERGED" ]; then + needs_pull=true + break + fi + done < "$temp_file" + + if [ "$needs_pull" = true ]; then + printf "\n${YELLOW}=== REPOSITORIES NEEDING PULL ===${NC}\n\n" + + # Sort by number of commits behind + while IFS='|' read -r type _ changes repo hours_fmt upstream_status upstream_ahead upstream_behind rest; do + if [ "$upstream_status" = "BEHIND" ] || [ "$upstream_status" = "DIVERGED" ]; then + local upstream_fmt + upstream_fmt=$(format_upstream_status "$upstream_status" "$upstream_ahead" "$upstream_behind") + local status_color="" + local status_text="Clean" + if [ "$type" = "D" ]; then + status_color="${RED}" + status_text="Dirty" + fi + printf "%-40s %15s %-10s ${status_color}%-10s${NC} %-12b\n" "$repo" "$hours_fmt" "$changes" "$status_text" "$upstream_fmt" + fi + done < "$temp_file" | sort -t'|' -k8,8nr + fi } print_dirty_repositories() { local temp_file="$1" - + if grep -q "^D|" "$temp_file" 2>/dev/null; then printf "\n=== DIRTY REPOSITORIES ===\n\n" - grep "^D|" "$temp_file" | sort -t'|' -k3,3nr | while IFS='|' read -r _ _ changes repo hours_fmt _ file_list; do - printf "%-40s %15s ${RED}%-10s${NC} ${RED}%-10s${NC}\n" "$repo" "$hours_fmt" "$changes" "Dirty" + grep "^D|" "$temp_file" | sort -t'|' -k3,3nr | while IFS='|' read -r _ _ changes repo hours_fmt upstream_status upstream_ahead upstream_behind _ file_list; do + local upstream_fmt + upstream_fmt=$(format_upstream_status "$upstream_status" "$upstream_ahead" "$upstream_behind") + printf "%-40s %15s ${RED}%-10s${NC} ${RED}%-10s${NC} %-12b\n" "$repo" "$hours_fmt" "$changes" "Dirty" "$upstream_fmt" if [ -n "$file_list" ]; then echo -e "$file_list" fi @@ -199,11 +367,13 @@ print_dirty_repositories() { print_clean_repositories() { local temp_file="$1" - + if grep -q "^C|" "$temp_file" 2>/dev/null; then printf "\n=== CLEAN REPOSITORIES ===\n\n" - grep "^C|" "$temp_file" | sort -t'|' -k2,2n | while IFS='|' read -r _ _ changes repo hours_fmt; do - printf "%-40s %15s %-10s %-10s\n" "$repo" "$hours_fmt" "$changes" "Clean" + grep "^C|" "$temp_file" | sort -t'|' -k2,2n | while IFS='|' read -r _ _ changes repo hours_fmt upstream_status upstream_ahead upstream_behind; do + local upstream_fmt + upstream_fmt=$(format_upstream_status "$upstream_status" "$upstream_ahead" "$upstream_behind") + printf "%-40s %15s %-10s %-10s %-12b\n" "$repo" "$hours_fmt" "$changes" "Clean" "$upstream_fmt" done fi } @@ -253,7 +423,9 @@ main() { scan_repositories "$DIR" "$TEMP_FILE" # Display results + print_summary "$TEMP_FILE" print_header + print_repos_needing_pull "$TEMP_FILE" print_dirty_repositories "$TEMP_FILE" print_clean_repositories "$TEMP_FILE" }