diff --git a/whatsdirty/test.sh b/whatsdirty/test.sh index 26dd6a1..78bc06c 100755 --- a/whatsdirty/test.sh +++ b/whatsdirty/test.sh @@ -66,13 +66,17 @@ fi # Test 2: Running with a valid directory should save config echo -e "\nTest 2: First run with directory argument" -TEST_DIR="/home/j/code" +# Use a directory that should exist in most environments +TEST_DIR="/tmp" if [ -d "$TEST_DIR" ]; then "$WHATSDIRTY_SCRIPT" "$TEST_DIR" > /dev/null 2>&1 if [ -f "$CONFIG_FILE" ] && [ "$(cat "$CONFIG_FILE")" = "$TEST_DIR" ]; then print_test "Directory saved to config" "PASS" else print_test "Directory saved to config" "FAIL" + echo " Expected config to contain: $TEST_DIR" + echo " Config file exists: $([ -f "$CONFIG_FILE" ] && echo "yes" || echo "no")" + [ -f "$CONFIG_FILE" ] && echo " Config contains: $(cat "$CONFIG_FILE")" fi else print_test "Directory saved to config" "SKIP (test directory not found)" @@ -80,10 +84,13 @@ fi # Test 3: Running without arguments should use saved directory echo -e "\nTest 3: Subsequent run without arguments" -if "$WHATSDIRTY_SCRIPT" > /dev/null 2>&1; then +OUTPUT=$("$WHATSDIRTY_SCRIPT" 2>&1 || true) +# Should not contain error message about no directory +if ! echo "$OUTPUT" | grep -q "Error:" && [ -f "$CONFIG_FILE" ]; then print_test "Uses saved directory from config" "PASS" else print_test "Uses saved directory from config" "FAIL" + echo " Output: $OUTPUT" fi # Test 4: Running with new directory should update config diff --git a/whatsdirty/whatsdirty.sh b/whatsdirty/whatsdirty.sh index 8d59dcd..0af6ecb 100755 --- a/whatsdirty/whatsdirty.sh +++ b/whatsdirty/whatsdirty.sh @@ -1,178 +1,231 @@ #!/bin/bash -# iterate through all subfolders in the current directory (or $1 if provided), -# and check: -# is it a git rep? -# if so is it clean (nothing to commit)? -# -# for each git repo print the name of the repo, the datestamp of the last commit, -# the nubmer of changes since then, and whether it's clean or not. +# whatsdirty.sh - Check git repository status in subdirectories +# Shows which repositories have uncommitted changes and when they were last committed -# Configuration file path +# Configuration CONFIG_FILE="$HOME/.config/whatsdirty.conf" +RED='\033[0;31m' +NC='\033[0m' # No Color + +# === Configuration Management Functions === -# Function to save directory to config save_directory() { local dir="$1" mkdir -p "$(dirname "$CONFIG_FILE")" echo "$dir" > "$CONFIG_FILE" } -# Function to load directory from config load_directory() { if [ -f "$CONFIG_FILE" ]; then cat "$CONFIG_FILE" fi } -# Determine which directory to use -if [ $# -eq 0 ]; then - # No argument provided - try to load from config - SAVED_DIR=$(load_directory) - if [ -z "$SAVED_DIR" ]; then - echo "Error: No directory specified and no saved directory found." - echo "Usage: $0 " - echo "The specified directory will be saved for future use." - exit 1 - fi - DIR="$SAVED_DIR" -else - # Argument provided - use it and save to config - DIR="$1" - # Validate and canonicalize the directory first - DIR=$(cd "$DIR" 2>/dev/null && pwd) || { echo "Error: Directory '$1' not found"; exit 1; } - save_directory "$DIR" -fi - -# Make sure directory is canonical (in case loaded from config) -DIR=$(cd "$DIR" 2>/dev/null && pwd) || { - echo "Error: Saved directory '$DIR' no longer exists"; - rm -f "$CONFIG_FILE" - exit 1; +validate_directory() { + local dir="$1" + # Use subshell to avoid changing current directory + (cd "$dir" 2>/dev/null && pwd) } -# Color codes -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Temporary file to store repository data -TEMP_FILE=$(mktemp) -# shellcheck disable=SC2064 -trap "rm -f $TEMP_FILE" EXIT - -# iterate through all subfolders in DIR -for folder in "$DIR"/*; do - # Skip if not a directory - [ ! -d "$folder" ] && continue +handle_directory_argument() { + local arg="$1" - # Check if it's a git repository - if [ -d "$folder/.git" ]; then - # Get the folder name - repo_name=$(basename "$folder") - - # Change to the repository directory - cd "$folder" || continue - - # Get the last commit timestamp in seconds since epoch - last_commit_timestamp=$(git log -1 --format="%ct" 2>/dev/null) - - if [ -n "$last_commit_timestamp" ]; then - # Get current timestamp - current_timestamp=$(date +%s) - - # Calculate difference in seconds - diff_seconds=$((current_timestamp - last_commit_timestamp)) - - # Convert to hours with decimal precision - # Using bc for floating point arithmetic - hours_ago=$(echo "scale=1; $diff_seconds / 3600" | bc) - - # Ensure leading zero for values less than 1 - if [[ $hours_ago =~ ^\. ]]; then - hours_ago="0$hours_ago" - fi - - # Format the hours number and "hours" label separately - hours_num="$hours_ago" - hours_label="hours" - # Combine with right-justified formatting - hours_formatted=$(printf "%8s %-6s" "$hours_num" "$hours_label") - else - hours_num="999999" # Large number for sorting - hours_label="commits" - hours_formatted=$(printf "%8s %-7s" "No" "$hours_label") + if [ -z "$arg" ]; then + # No argument - try to load from config + local saved_dir=$(load_directory) + if [ -z "$saved_dir" ]; then + echo "Error: No directory specified and no saved directory found." >&2 + echo "Usage: $0 " >&2 + echo "The specified directory will be saved for future use." >&2 + exit 1 fi - - # Count uncommitted changes (staged and unstaged) - # Count modified, deleted, and untracked files - staged_changes=$(git diff --cached --numstat 2>/dev/null | wc -l) - unstaged_changes=$(git diff --numstat 2>/dev/null | wc -l) - untracked_files=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l) - total_changes=$((staged_changes + unstaged_changes + untracked_files)) - - # Store repo data with status prefix (D for dirty, C for clean) - if [ $total_changes -eq 0 ]; then - echo "C|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}" >> "$TEMP_FILE" - else - # For dirty repos, collect the list of changed files - changed_files="" - - # Get staged files - staged_files=$(git diff --cached --name-only 2>/dev/null) - if [ -n "$staged_files" ]; then - while IFS= read -r file; do - changed_files="${changed_files} [staged] $file\n" - done <<< "$staged_files" - fi - - # Get unstaged modified files - unstaged_files=$(git diff --name-only 2>/dev/null) - if [ -n "$unstaged_files" ]; then - while IFS= read -r file; do - changed_files="${changed_files} [modified] $file\n" - done <<< "$unstaged_files" - fi - - # Get untracked files - untracked=$(git ls-files --others --exclude-standard 2>/dev/null) - if [ -n "$untracked" ]; then - while IFS= read -r file; do - changed_files="${changed_files} [untracked] $file\n" - done <<< "$untracked" - fi - - # Remove trailing newline - changed_files=$(echo -e "$changed_files" | sed '$ d') - - # Store with a special delimiter (|||) to separate file list - echo "D|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}|||${changed_files}" >> "$TEMP_FILE" + echo "$saved_dir" + else + # Argument provided - validate and save + local canonical_dir=$(validate_directory "$arg") + if [ -z "$canonical_dir" ]; then + echo "Error: Directory '$arg' not found" >&2 + exit 1 fi - - # Return to the original directory - cd "$DIR" || exit 1 + save_directory "$canonical_dir" + echo "$canonical_dir" fi -done +} -# Print header -printf "%-40s %15s %-10s %-10s\n" "Repository" "Last Commit" "Changes" "Status" -printf "%-40s %15s %-10s %-10s\n" "----------" "(hours)" "-------" "------" +# === Git Repository Functions === -# Print dirty repositories first (sorted by changes, descending) -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 status hours changes repo hours_fmt separator file_list; do - printf "%-40s %15s ${RED}%-10s${NC} ${RED}%-10s${NC}\n" "$repo" "$hours_fmt" "$changes" "Dirty" - # Print the changed files if they exist - if [ -n "$file_list" ]; then - echo -e "$file_list" +get_last_commit_hours() { + local last_commit_timestamp=$(git log -1 --format="%ct" 2>/dev/null) + + if [ -n "$last_commit_timestamp" ]; then + local current_timestamp=$(date +%s) + local diff_seconds=$((current_timestamp - last_commit_timestamp)) + + # Convert to hours with decimal precision + local hours_ago=$(echo "scale=1; $diff_seconds / 3600" | bc) + + # Ensure leading zero for values less than 1 + if [[ $hours_ago =~ ^\. ]]; then + hours_ago="0$hours_ago" + fi + + echo "$hours_ago" + else + echo "" + fi +} + +count_git_changes() { + local staged=$(git diff --cached --numstat 2>/dev/null | wc -l) + local unstaged=$(git diff --numstat 2>/dev/null | wc -l) + local untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l) + echo $((staged + unstaged + untracked)) +} + +get_changed_files_list() { + local changed_files="" + + # Get staged files + local staged_files=$(git diff --cached --name-only 2>/dev/null) + if [ -n "$staged_files" ]; then + while IFS= read -r file; do + changed_files="${changed_files} [staged] $file\n" + done <<< "$staged_files" + fi + + # Get unstaged modified files + local unstaged_files=$(git diff --name-only 2>/dev/null) + if [ -n "$unstaged_files" ]; then + while IFS= read -r file; do + changed_files="${changed_files} [modified] $file\n" + done <<< "$unstaged_files" + fi + + # Get untracked files + local untracked=$(git ls-files --others --exclude-standard 2>/dev/null) + if [ -n "$untracked" ]; then + while IFS= read -r file; do + changed_files="${changed_files} [untracked] $file\n" + done <<< "$untracked" + fi + + # Remove trailing newline + if [ -n "$changed_files" ]; then + echo -e "$changed_files" | sed '$ d' + fi +} + +analyze_repository() { + local repo_path="$1" + local repo_name=$(basename "$repo_path") + + cd "$repo_path" || return 1 + + # Get commit info + local 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" + hours_formatted=$(printf "%8s %-6s" "$hours_num" "$hours_label") + else + hours_num="999999" # Large number for sorting + hours_label="commits" + hours_formatted=$(printf "%8s %-7s" "No" "$hours_label") + fi + + # Count changes + local 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}" + else + local changed_files=$(get_changed_files_list) + echo "D|${hours_num}|${total_changes}|${repo_name}|${hours_formatted}|||${changed_files}" + fi +} + +# === Output Formatting Functions === + +print_header() { + printf "%-40s %15s %-10s %-10s\n" "Repository" "Last Commit" "Changes" "Status" + printf "%-40s %15s %-10s %-10s\n" "----------" "(hours)" "-------" "------" +} + +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 status hours changes repo hours_fmt separator file_list; do + printf "%-40s %15s ${RED}%-10s${NC} ${RED}%-10s${NC}\n" "$repo" "$hours_fmt" "$changes" "Dirty" + if [ -n "$file_list" ]; then + echo -e "$file_list" + fi + done + fi +} + +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 status hours changes repo hours_fmt; do + printf "%-40s %15s %-10s %-10s\n" "$repo" "$hours_fmt" "$changes" "Clean" + done + fi +} + +# === Main Function === + +scan_repositories() { + local scan_dir="$1" + local temp_file="$2" + + for folder in "$scan_dir"/*; do + [ ! -d "$folder" ] && continue + + if [ -d "$folder/.git" ]; then + local repo_data=$(analyze_repository "$folder") + if [ -n "$repo_data" ]; then + echo "$repo_data" >> "$temp_file" + fi + cd "$scan_dir" || exit 1 fi done -fi +} -# Print clean repositories (sorted by hours, ascending) -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 status hours changes repo hours_fmt; do - printf "%-40s %15s %-10s %-10s\n" "$repo" "$hours_fmt" "$changes" "Clean" - done -fi \ No newline at end of file +main() { + # Handle directory argument + local DIR=$(handle_directory_argument "$1") + + # Validate directory still exists (for loaded configs) + local validated_dir=$(validate_directory "$DIR") + if [ -z "$validated_dir" ]; then + echo "Error: Saved directory '$DIR' no longer exists" >&2 + rm -f "$CONFIG_FILE" + exit 1 + fi + DIR="$validated_dir" + + # Create temp file for results + local TEMP_FILE=$(mktemp) + trap "rm -f $TEMP_FILE" EXIT + + # Scan repositories + scan_repositories "$DIR" "$TEMP_FILE" + + # Display results + print_header + print_dirty_repositories "$TEMP_FILE" + print_clean_repositories "$TEMP_FILE" +} + +# Execute main function with all arguments +main "$@" \ No newline at end of file