From 23b3752322dbb948d7f5dbb053900e8aaa0fb274 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 2 Sep 2025 20:09:12 +1200 Subject: [PATCH] test: Add 8 files --- install.sh | 21 +++ setup_script.sh | 5 + test.sh | 167 ++++++++++++++++++++++++ whatsdirty | 335 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 528 insertions(+) create mode 100755 install.sh create mode 100755 setup_script.sh create mode 100755 test.sh create mode 100755 whatsdirty diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b792081 --- /dev/null +++ b/install.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +#SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +echo "Installing whatsdirty" + +if ! command -v getpkg ; then + + ARCH=$(uname -m) + wget "https://getbin.xyz/getpkg:latest-${ARCH}" -O bootstrap && chmod a+x bootstrap + ./bootstrap install getpkg + rm ./bootstrap + VERSION=$(getpkg version) + echo "Dropshell tool $VERSION installed" + +fi + +getpkg install whatsdirty + diff --git a/setup_script.sh b/setup_script.sh new file mode 100755 index 0000000..6b8822c --- /dev/null +++ b/setup_script.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "whatsdirty tool setup completed successfully!" +echo "Use 'whatsdirty ' to scan git repositories for uncommitted changes." +echo "The specified directory will be saved for future use." diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..bb119ab --- /dev/null +++ b/test.sh @@ -0,0 +1,167 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +WHATSDIRTY_SCRIPT="$SCRIPT_DIR/whatsdirty" +CONFIG_FILE="$HOME/.config/whatsdirty.conf" +BACKUP_FILE="" +TEST_PASSED=true + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Function to print test results +print_test() { + local test_name="$1" + local result="$2" + if [ "$result" = "PASS" ]; then + echo -e "${GREEN}✓${NC} $test_name" + else + echo -e "${RED}✗${NC} $test_name" + TEST_PASSED=false + fi +} + +# Function to cleanup and restore +CLEANED=false +cleanup() { + if [ "$CLEANED" = true ]; then + return + fi + CLEANED=true + if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then + cp "$BACKUP_FILE" "$CONFIG_FILE" + rm -f "$BACKUP_FILE" + elif [ -n "$BACKUP_FILE" ]; then + # Original didn't exist, so remove the created one + rm -f "$CONFIG_FILE" + fi +} + +# Set up trap to restore config on exit +trap cleanup EXIT + +echo "Testing whatsdirty configuration behavior..." +echo "Working directory: $(pwd)" +echo "Script path: $WHATSDIRTY_SCRIPT" +echo "Config file: $CONFIG_FILE" +echo + +# Backup existing config if it exists +if [ -f "$CONFIG_FILE" ]; then + BACKUP_FILE=$(mktemp) + cp "$CONFIG_FILE" "$BACKUP_FILE" +else + # Mark that we need to remove the config file on cleanup + BACKUP_FILE="NONE_EXISTED" +fi + +# Remove config to start fresh +rm -f "$CONFIG_FILE" + +# Test 1: Running without arguments and no config should fail +echo "Test 1: First run without arguments (should fail)" +OUTPUT=$("$WHATSDIRTY_SCRIPT" 2>&1 || true) +if echo "$OUTPUT" | grep -q "No directory specified and no saved directory found"; then + print_test "First run without args shows error" "PASS" +else + print_test "First run without args shows error" "FAIL" + echo " Expected: 'No directory specified and no saved directory found'" + echo " Got: $OUTPUT" +fi + +# Test 2: Running with a valid directory should save config +echo -e "\nTest 2: First run with directory argument" +# Use a directory that should exist in most environments +TEST_DIR="/tmp" +echo " Testing with directory: $TEST_DIR" +if [ -d "$TEST_DIR" ]; then + echo " Directory exists, running script..." + SCRIPT_OUTPUT=$("$WHATSDIRTY_SCRIPT" "$TEST_DIR" 2>&1) + SCRIPT_EXIT_CODE=$? + echo " Script exit code: $SCRIPT_EXIT_CODE" + if [ $SCRIPT_EXIT_CODE -eq 0 ] && [ -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")" + echo " Script output: $SCRIPT_OUTPUT" + fi +else + print_test "Directory saved to config" "SKIP (test directory not found)" +fi + +# Test 3: Running without arguments should use saved directory +echo -e "\nTest 3: Subsequent run without arguments" +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 +echo -e "\nTest 4: Run with different directory" +# Use the parent directory (should exist) +NEW_TEST_DIR="$SCRIPT_DIR/.." +if [ -d "$NEW_TEST_DIR" ]; then + CANONICAL_DIR=$(cd "$NEW_TEST_DIR" && pwd) + "$WHATSDIRTY_SCRIPT" "$NEW_TEST_DIR" > /dev/null 2>&1 + if [ -f "$CONFIG_FILE" ] && [ "$(cat "$CONFIG_FILE")" = "$CANONICAL_DIR" ]; then + print_test "Config updated with new directory" "PASS" + else + print_test "Config updated with new directory" "FAIL" + echo " Expected: $CANONICAL_DIR" + echo " Got: $(cat "$CONFIG_FILE" 2>/dev/null || echo "file not found")" + fi +else + print_test "Config updated with new directory" "SKIP (test directory not found)" +fi + +# Test 5: Invalid directory should fail +echo -e "\nTest 5: Invalid directory handling" +OUTPUT=$("$WHATSDIRTY_SCRIPT" "/nonexistent/directory" 2>&1 || true) +if echo "$OUTPUT" | grep -q "Directory '/nonexistent/directory' not found"; then + print_test "Invalid directory shows error" "PASS" +else + print_test "Invalid directory shows error" "FAIL" + echo " Expected: 'Directory '/nonexistent/directory' not found'" + echo " Got: $OUTPUT" +fi + +# Test 6: Saved directory that no longer exists +echo -e "\nTest 6: Saved directory that no longer exists" +mkdir -p "$(dirname "$CONFIG_FILE")" +echo "/nonexistent/saved/directory" > "$CONFIG_FILE" +OUTPUT=$("$WHATSDIRTY_SCRIPT" 2>&1 || true) +if echo "$OUTPUT" | grep -q "no longer exists"; then + # Check that config was removed + if [ ! -f "$CONFIG_FILE" ]; then + print_test "Handles deleted saved directory" "PASS" + else + print_test "Handles deleted saved directory" "FAIL (config not cleaned up)" + fi +else + print_test "Handles deleted saved directory" "FAIL" + echo " Expected message about directory no longer existing" + echo " Got: $OUTPUT" +fi + +cleanup + +# Summary +echo +if [ "$TEST_PASSED" = true ]; then + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +else + echo -e "${RED}Some tests failed!${NC}" + exit 1 +fi + diff --git a/whatsdirty b/whatsdirty new file mode 100755 index 0000000..87eb773 --- /dev/null +++ b/whatsdirty @@ -0,0 +1,335 @@ +#!/bin/bash + +# whatsdirty.sh - Check git repository status in subdirectories +# Shows which repositories have uncommitted changes and when they were last committed + +# Configuration +CONFIG_FILE="$HOME/.config/whatsdirty.conf" +RED='\033[0;31m' +NC='\033[0m' # No Color + +# === Configuration Management Functions === + +save_directory() { + local dir="$1" + mkdir -p "$(dirname "$CONFIG_FILE")" + echo "$dir" > "$CONFIG_FILE" +} + +load_directory() { + if [ -f "$CONFIG_FILE" ]; then + cat "$CONFIG_FILE" + fi +} + +validate_directory() { + local dir="$1" + # Use subshell to avoid changing current directory + (cd "$dir" 2>/dev/null && pwd) +} + +handle_directory_argument() { + local arg="$1" + + if [ -z "$arg" ]; then + # No argument - try to load from config + local saved_dir + 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 + echo "$saved_dir" + else + # Argument provided - validate and save + local canonical_dir + canonical_dir=$(validate_directory "$arg") + if [ -z "$canonical_dir" ]; then + echo "Error: Directory '$arg' not found" >&2 + exit 1 + fi + save_directory "$canonical_dir" + echo "$canonical_dir" + fi +} + +# === Git Repository Functions === + +get_last_commit_hours() { + local last_commit_timestamp + last_commit_timestamp=$(git log -1 --format="%ct" 2>/dev/null) + + if [ -n "$last_commit_timestamp" ]; then + local current_timestamp + current_timestamp=$(date +%s) + local diff_seconds=$((current_timestamp - last_commit_timestamp)) + + # Convert to hours with decimal precision using awk (more portable than bc) + local hours_ago + hours_ago=$(awk "BEGIN {printf \"%.1f\", $diff_seconds / 3600}") + + # 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 + staged=$(git diff --cached --numstat 2>/dev/null | wc -l) + local unstaged + unstaged=$(git diff --numstat 2>/dev/null | wc -l) + local untracked + 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 + 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 + 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 + 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 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) + # If realpath fails, fall back to basename + if [ -z "$repo_name" ] || [ "$repo_name" = "$repo_path" ]; then + repo_name=$(basename "$repo_path") + fi + else + repo_name=$(basename "$repo_path") + fi + + cd "$repo_path" || return 1 + + # 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" + 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 + 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 + 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 _ _ changes repo hours_fmt _ 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 _ _ 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" + + # Use find to recursively locate all .git directories + while IFS= read -r git_dir; do + # Get the repository path (parent of .git) + local repo_path + repo_path=$(dirname "$git_dir") + + local repo_data + repo_data=$(analyze_repository "$repo_path" "$scan_dir") + if [ -n "$repo_data" ]; then + echo "$repo_data" >> "$temp_file" + fi + cd "$scan_dir" || exit 1 + done < <(find "$scan_dir" -type d -name ".git" 2>/dev/null | sort) +} + +main() { + # Handle directory argument + local DIR + DIR=$(handle_directory_argument "$1") + + # Validate directory still exists (for loaded configs) + local validated_dir + 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 + 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" +} + +# === Autocomplete Function === + +handle_autocomplete() { + local cur_word="${COMP_WORDS[COMP_CWORD]}" + + # If we're completing the first argument after 'whatsdirty' + if [ ${COMP_CWORD} -eq 1 ]; then + # Suggest common directories and saved directory + local saved_dir + saved_dir=$(load_directory) + local suggestions="" + + # Add saved directory if it exists + if [ -n "$saved_dir" ] && [ -d "$saved_dir" ]; then + suggestions="$saved_dir" + fi + + # Add common development directories + for dir in ~/code ~/projects ~/dev ~/src ~/workspace /home/*/code /home/*/projects; do + if [ -d "$dir" ]; then + # Only add if it contains git repositories + if find "$dir" -maxdepth 2 -name ".git" -type d 2>/dev/null | head -1 | grep -q .; then + suggestions="$suggestions $dir" + fi + fi + done + + # Directory completion for current word + if [ -n "$cur_word" ]; then + # Use mapfile for directory completion + local dir_completions + mapfile -t dir_completions < <(compgen -d -- "$cur_word") + # Add our suggestions that match the current word + local matching_suggestions + mapfile -t matching_suggestions < <(echo "$suggestions" | tr ' ' '\n' | grep "^$cur_word" | sort -u) + # Combine and deduplicate + mapfile -t COMPREPLY < <(printf '%s\n' "${dir_completions[@]}" "${matching_suggestions[@]}" | sort -u) + else + # No current word, suggest all our directories plus general directory completion + local dir_completions + mapfile -t dir_completions < <(compgen -d) + local all_suggestions + mapfile -t all_suggestions < <(echo "$suggestions" | tr ' ' '\n' | sort -u) + # Combine and deduplicate, but limit to reasonable number + mapfile -t COMPREPLY < <(printf '%s\n' "${all_suggestions[@]}" "${dir_completions[@]}" | sort -u | head -20) + fi + fi +} + +# Check if we're being called for autocomplete +if [ "$1" = "autocomplete" ]; then + # This is called when the tool is being used for bash completion + # The arguments after 'autocomplete' are the completion context + shift + + # Set up COMP_WORDS and COMP_CWORD from arguments + COMP_WORDS=("$@") + COMP_CWORD=$(($# - 1)) + + # Generate completions + handle_autocomplete + + # Print completions, one per line + printf '%s\n' "${COMPREPLY[@]}" + exit 0 +fi + +# Check if we're being called to show version +if [ "$1" = "version" ] || [ "$1" = "--version" ] || [ "$1" = "-v" ] || [ "$1" = "v" ]; then + echo "2025.0803.0612" + exit 0 +fi + +# Execute main function with all arguments +main "$@" \ No newline at end of file