|
#!/bin/bash |
|
|
|
# Git Maintenance Script |
|
# Purpose: Perform safe Git maintenance operations on all Git repositories in the current directory tree |
|
|
|
# set -e # Disabled to prevent premature exits |
|
|
|
# Simple color codes |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
BLUE='\033[0;34m' |
|
YELLOW='\033[1;33m' |
|
CYAN='\033[0;36m' |
|
MAGENTA='\033[0;35m' |
|
NC='\033[0m' # No Color |
|
|
|
# Global variables |
|
VERBOSE=false |
|
DRY_RUN=false |
|
BASE_DIR="." |
|
CUSTOM_COMMANDS="" |
|
|
|
# Statistics tracking |
|
TOTAL_REPOS=0 |
|
CLEAN_REPOS=0 |
|
DIRTY_REPOS=0 |
|
AHEAD_REPOS=0 |
|
BEHIND_REPOS=0 |
|
DIVERGED_REPOS=0 |
|
STASHED_REPOS=0 |
|
LARGE_REPOS=0 |
|
OLD_REPOS=0 |
|
|
|
# Failure tracking |
|
FAILED_REPOS=() |
|
|
|
# Simple logging functions |
|
log_info() { |
|
echo -e "${BLUE}βΉοΈ $1${NC}" |
|
} |
|
|
|
log_success() { |
|
echo -e "${GREEN}β
$1${NC}" |
|
} |
|
|
|
log_error() { |
|
echo -e "${RED}β $1${NC}" >&2 |
|
} |
|
|
|
log_warning() { |
|
echo -e "${YELLOW}β οΈ $1${NC}" |
|
} |
|
|
|
log_verbose() { |
|
[ "$VERBOSE" = true ] && echo -e "${BLUE}π $1${NC}" |
|
} |
|
|
|
log_insight() { |
|
echo -e "${CYAN}π‘ $1${NC}" |
|
} |
|
|
|
log_stat() { |
|
echo -e "${MAGENTA}π $1${NC}" |
|
} |
|
|
|
# Standardized logging functions for consistent formatting |
|
log_repo_processing() { |
|
local repo_name="$1" |
|
local relative_path="$2" |
|
local repo_size="$3" |
|
local repo_age="$4" |
|
local status="$5" |
|
log_info "Processing repository: $repo_name (path: $relative_path, size: $repo_size, age: ${repo_age} days, status: $status)" |
|
} |
|
|
|
log_command_result() { |
|
local cmd="$1" |
|
local repo_name="$2" |
|
local success="$3" |
|
local custom_msg="$4" |
|
|
|
if [ "$success" = true ]; then |
|
if [ -n "$custom_msg" ]; then |
|
log_success "$custom_msg: $repo_name" |
|
else |
|
log_success "Successfully executed $cmd: $repo_name" |
|
fi |
|
else |
|
if [ -n "$custom_msg" ]; then |
|
log_warning "$custom_msg: $repo_name" |
|
else |
|
log_warning "Failed to execute $cmd: $repo_name" |
|
fi |
|
fi |
|
} |
|
|
|
log_repo_skipped() { |
|
local repo_name="$1" |
|
local reason="$2" |
|
log_verbose "Skipping $repo_name: $reason" |
|
} |
|
|
|
# Help function |
|
show_help() { |
|
cat << EOF |
|
Git Maintenance Script |
|
|
|
USAGE: |
|
$0 [OPTIONS] [BASE_DIRECTORY] |
|
|
|
DESCRIPTION: |
|
Perform safe Git maintenance operations on all Git repositories in the specified directory tree. |
|
Only performs read-only operations and safe optimizations that cannot break repositories. |
|
|
|
ARGUMENTS: |
|
BASE_DIRECTORY Directory to search for Git repositories (default: current directory) |
|
|
|
OPTIONS: |
|
-v, --verbose Enable verbose output |
|
-d, --dry-run Show what would be done without executing |
|
--commands COMMANDS Comma-separated list of Git commands to execute |
|
-h, --help Show this help message |
|
|
|
EXAMPLES: |
|
$0 # Perform safe Git maintenance on all repositories |
|
$0 /path/to/projects # Perform safe Git maintenance in /path/to/projects |
|
$0 --verbose # Enable verbose output during maintenance |
|
$0 --dry-run # Show what would be done without executing |
|
$0 --commands "pull,status" # Execute custom Git commands (pull, status) |
|
$0 --commands "fetch" # Execute only fetch command |
|
$0 --dry-run --commands "pull,merge" # Show what custom commands would do |
|
|
|
SAFE OPERATIONS (Default): |
|
- git fetch --all --prune Update remote references and remove stale branches |
|
- git gc --auto Garbage collection and optimization |
|
- git prune Remove unreachable objects |
|
- git fsck --no-dangling Check repository integrity |
|
|
|
EXTRA COMMANDS (with --commands): |
|
- pull Pull latest changes (safe with clean working directory) |
|
- status Show repository status |
|
- log Show commit history |
|
- branch List branches |
|
- remote Show remote information |
|
- config Show configuration |
|
- diff Show differences |
|
- merge Merge branches (BLOCKED - too dangerous) |
|
- rebase Rebase commits (BLOCKED - too dangerous) |
|
- reset Reset repository state (BLOCKED - too dangerous) |
|
- clean Clean untracked files (BLOCKED - too dangerous) |
|
|
|
NOTE: Commands that modify repository state (merge, rebase, reset, clean) are |
|
blocked for safety reasons. Use these commands manually if needed. |
|
|
|
EOF |
|
} |
|
|
|
# Define safe operations (default commands) |
|
get_safe_operations() { |
|
echo "fetch,gc,prune,fsck" |
|
} |
|
|
|
|
|
# Check if a command requires a clean working directory |
|
requires_clean_working_dir() { |
|
local cmd="$1" |
|
[ "$cmd" = "pull" ] |
|
} |
|
|
|
# Check if a command is potentially dangerous |
|
is_dangerous_command() { |
|
local cmd="$1" |
|
case "$cmd" in |
|
merge|rebase|reset|clean) |
|
return 0 |
|
;; |
|
*) |
|
return 1 |
|
;; |
|
esac |
|
} |
|
|
|
# Validate if a command is supported |
|
is_valid_command() { |
|
local cmd="$1" |
|
case "$cmd" in |
|
fetch|gc|prune|fsck|pull|status|log|branch|remote|config|diff|merge|rebase|reset|clean) |
|
return 0 |
|
;; |
|
*) |
|
return 1 |
|
;; |
|
esac |
|
} |
|
|
|
# Parse command line arguments |
|
parse_args() { |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
-v|--verbose) |
|
VERBOSE=true |
|
shift |
|
;; |
|
-d|--dry-run) |
|
DRY_RUN=true |
|
shift |
|
;; |
|
--commands) |
|
if [ -z "$2" ]; then |
|
log_error "Error: --commands requires a value" |
|
show_help |
|
exit 1 |
|
fi |
|
CUSTOM_COMMANDS="$2" |
|
# Validate commands |
|
IFS=',' read -ra COMMANDS <<< "$CUSTOM_COMMANDS" |
|
for cmd in "${COMMANDS[@]}"; do |
|
cmd=$(echo "$cmd" | xargs) # trim whitespace |
|
if [ -z "$cmd" ]; then |
|
log_error "Error: Empty command found in --commands" |
|
exit 1 |
|
fi |
|
done |
|
shift 2 |
|
;; |
|
-h|--help) |
|
show_help |
|
exit 0 |
|
;; |
|
-*) |
|
log_error "Unknown option: $1" |
|
show_help |
|
exit 1 |
|
;; |
|
*) |
|
BASE_DIR="$1" |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
} |
|
|
|
# Check if a directory is a Git repository |
|
is_git_repo() { |
|
local dir="$1" |
|
[ -d "$dir/.git" ] |
|
} |
|
|
|
# Get repository size in human readable format |
|
get_repo_size() { |
|
local repo_dir="$1" |
|
if [ -d "$repo_dir" ]; then |
|
du -sh "$repo_dir" 2>/dev/null | cut -f1 || echo "unknown" |
|
else |
|
echo "0B" |
|
fi |
|
} |
|
|
|
# Convert size string to MB for comparison |
|
size_to_mb() { |
|
local size_str="$1" |
|
local size_mb=0 |
|
|
|
# Extract number and unit |
|
if [[ "$size_str" =~ ^([0-9]+\.?[0-9]*)([KMGTPE]?B?)$ ]]; then |
|
local number="${BASH_REMATCH[1]}" |
|
local unit="${BASH_REMATCH[2]}" |
|
|
|
# Convert number to integer (truncate decimal part) |
|
local int_number=$(echo "$number" | cut -d. -f1) |
|
|
|
# Convert to MB based on unit |
|
case "$unit" in |
|
"B"|"") |
|
size_mb=$((int_number / 1024 / 1024)) |
|
;; |
|
"K"|"KB") |
|
size_mb=$((int_number / 1024)) |
|
;; |
|
"M"|"MB") |
|
size_mb=$int_number |
|
;; |
|
"G"|"GB") |
|
size_mb=$((int_number * 1024)) |
|
;; |
|
"T"|"TB") |
|
size_mb=$((int_number * 1024 * 1024)) |
|
;; |
|
*) |
|
size_mb=0 |
|
;; |
|
esac |
|
fi |
|
|
|
echo "$size_mb" |
|
} |
|
|
|
# Get repository age (days since last commit) |
|
get_repo_age() { |
|
local repo_dir="$1" |
|
if [ -d "$repo_dir/.git" ]; then |
|
local last_commit=$(cd "$repo_dir" && git log -1 --format="%ct" 2>/dev/null || echo "0") |
|
if [ "$last_commit" != "0" ]; then |
|
local current_time=$(date +%s) |
|
local days_ago=$(( (current_time - last_commit) / 86400 )) |
|
echo "$days_ago" |
|
else |
|
echo "unknown" |
|
fi |
|
else |
|
echo "unknown" |
|
fi |
|
} |
|
|
|
# Execute a Git command and handle result |
|
execute_git_command() { |
|
local cmd="$1" |
|
local repo_name="$2" |
|
local success_msg="$3" |
|
local fail_msg="$4" |
|
local is_display_command="$5" |
|
|
|
local success=false |
|
local output="" |
|
|
|
if [ "$is_display_command" = true ]; then |
|
echo |
|
log_info "=== $success_msg for $repo_name ===" |
|
if output=$(eval "git $cmd" 2>/dev/null); then |
|
echo "$output" |
|
success=true |
|
fi |
|
echo |
|
else |
|
if eval "git $cmd" 2>/dev/null; then |
|
success=true |
|
fi |
|
fi |
|
|
|
if [ "$success" = true ]; then |
|
log_command_result "" "$repo_name" true "$success_msg" |
|
return 0 |
|
else |
|
log_command_result "" "$repo_name" false "$fail_msg" |
|
return 1 |
|
fi |
|
} |
|
|
|
# Handle command execution result |
|
handle_command_result() { |
|
local cmd="$1" |
|
local success="$2" |
|
local operations_performed_ref="$3" |
|
local operations_failed_ref="$4" |
|
|
|
if [ "$success" = true ]; then |
|
eval "$operations_performed_ref+=(\"$cmd\")" |
|
else |
|
eval "$operations_failed_ref+=(\"$cmd\")" |
|
fi |
|
} |
|
|
|
# Check if working directory is clean (reuse from analyze_repo_status) |
|
is_working_directory_clean() { |
|
local status_output=$(git status --porcelain 2>/dev/null) |
|
if [ $? -eq 0 ]; then |
|
if [ -n "$status_output" ]; then |
|
return 1 # Not clean |
|
else |
|
return 0 # Clean |
|
fi |
|
else |
|
return 1 # Error, assume not clean |
|
fi |
|
} |
|
|
|
# Analyze repository status |
|
analyze_repo_status() { |
|
local repo_dir="$1" |
|
local status_info="" |
|
|
|
if ! is_git_repo "$repo_dir"; then |
|
echo "not_git" |
|
return |
|
fi |
|
|
|
if ! cd "$repo_dir"; then |
|
log_error "Failed to change to repository directory: $repo_dir" |
|
return 1 |
|
fi |
|
|
|
# Check working directory status |
|
local working_status=0 |
|
local status_output=$(git status --porcelain 2>/dev/null) |
|
if [ $? -eq 0 ]; then |
|
# Check if status_output is empty (clean repo) or has content (dirty repo) |
|
if [ -n "$status_output" ]; then |
|
working_status=$(echo "$status_output" | wc -l) |
|
else |
|
working_status=0 |
|
fi |
|
fi |
|
if [ "$working_status" -gt 0 ]; then |
|
status_info="${status_info}dirty," |
|
else |
|
status_info="${status_info}clean," |
|
fi |
|
|
|
# Check if repository is ahead/behind |
|
local branch="detached" |
|
local branch_output=$(git branch --show-current 2>/dev/null) |
|
if [ $? -eq 0 ] && [ -n "$branch_output" ]; then |
|
branch="$branch_output" |
|
fi |
|
if [ "$branch" != "detached" ]; then |
|
local upstream="" |
|
local upstream_output=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) |
|
if [ $? -eq 0 ] && [ -n "$upstream_output" ]; then |
|
upstream="$upstream_output" |
|
fi |
|
if [ -n "$upstream" ]; then |
|
local ahead="0" |
|
local behind="0" |
|
local ahead_output=$(git rev-list --count @{u}..HEAD 2>/dev/null) |
|
if [ $? -eq 0 ] && [ -n "$ahead_output" ]; then |
|
ahead="$ahead_output" |
|
fi |
|
local behind_output=$(git rev-list --count HEAD..@{u} 2>/dev/null) |
|
if [ $? -eq 0 ] && [ -n "$behind_output" ]; then |
|
behind="$behind_output" |
|
fi |
|
|
|
if [ "$ahead" -gt 0 ] && [ "$behind" -gt 0 ]; then |
|
status_info="${status_info}diverged," |
|
elif [ "$ahead" -gt 0 ]; then |
|
status_info="${status_info}ahead," |
|
elif [ "$behind" -gt 0 ]; then |
|
status_info="${status_info}behind," |
|
else |
|
status_info="${status_info}up_to_date," |
|
fi |
|
else |
|
status_info="${status_info}no_upstream," |
|
fi |
|
else |
|
status_info="${status_info}detached," |
|
fi |
|
|
|
# Check for stashes |
|
local stash_count=0 |
|
local stash_output=$(git stash list 2>/dev/null) |
|
if [ $? -eq 0 ]; then |
|
stash_count=$(echo "$stash_output" | wc -l) |
|
fi |
|
if [ "$stash_count" -gt 0 ]; then |
|
status_info="${status_info}stashed," |
|
fi |
|
|
|
# Remove trailing comma |
|
status_info="${status_info%,}" |
|
echo "$status_info" |
|
} |
|
|
|
|
|
# Perform maintenance on a single repository |
|
maintain_repo() { |
|
local repo_dir="$1" |
|
local repo_name=$(basename "$repo_dir") |
|
local repo_size=$(get_repo_size "$repo_dir") |
|
local repo_age=$(get_repo_age "$repo_dir") |
|
|
|
if ! is_git_repo "$repo_dir"; then |
|
log_repo_skipped "$repo_name" "not a Git repository" |
|
return 0 |
|
fi |
|
|
|
# Analyze repository status |
|
local status=$(analyze_repo_status "$repo_dir") |
|
|
|
# Update statistics |
|
((TOTAL_REPOS++)) |
|
|
|
if [[ "$status" == *"clean"* ]]; then |
|
((CLEAN_REPOS++)) |
|
elif [[ "$status" == *"dirty"* ]]; then |
|
((DIRTY_REPOS++)) |
|
fi |
|
|
|
if [[ "$status" == *"ahead"* ]]; then |
|
((AHEAD_REPOS++)) |
|
elif [[ "$status" == *"behind"* ]]; then |
|
((BEHIND_REPOS++)) |
|
elif [[ "$status" == *"diverged"* ]]; then |
|
((DIVERGED_REPOS++)) |
|
fi |
|
|
|
if [[ "$status" == *"stashed"* ]]; then |
|
((STASHED_REPOS++)) |
|
fi |
|
|
|
# Check if repository is large (>100MB) |
|
local size_mb=$(size_to_mb "$repo_size") |
|
if [ "$size_mb" -gt 100 ]; then |
|
((LARGE_REPOS++)) |
|
fi |
|
|
|
# Check if repository is old (>30 days since last commit) |
|
if [[ "$repo_age" =~ ^[0-9]+$ ]] && [ "$repo_age" -gt 30 ]; then |
|
((OLD_REPOS++)) |
|
fi |
|
|
|
# Calculate relative path from BASE_DIR |
|
local relative_path="${repo_dir#$BASE_DIR/}" |
|
if [ "$relative_path" = "$repo_dir" ]; then |
|
relative_path="$repo_name" |
|
fi |
|
|
|
log_repo_processing "$repo_name" "$relative_path" "$repo_size" "$repo_age" "$status" |
|
|
|
if ! cd "$repo_dir"; then |
|
log_error "Failed to change to repository directory: $repo_dir" |
|
return 1 |
|
fi |
|
|
|
local operations_performed=() |
|
local operations_failed=() |
|
|
|
# Determine which commands to execute |
|
local commands_to_execute="" |
|
if [ -n "$CUSTOM_COMMANDS" ]; then |
|
commands_to_execute="$CUSTOM_COMMANDS" |
|
log_verbose "Using custom commands: $commands_to_execute" |
|
else |
|
commands_to_execute=$(get_safe_operations) |
|
log_verbose "Using default safe operations: $commands_to_execute" |
|
fi |
|
|
|
# Execute commands |
|
IFS=',' read -ra COMMANDS <<< "$commands_to_execute" |
|
for cmd in "${COMMANDS[@]}"; do |
|
cmd=$(echo "$cmd" | xargs) # trim whitespace |
|
|
|
# Validate command |
|
if ! is_valid_command "$cmd"; then |
|
log_warning "Skipping unknown command: $cmd on $repo_name" |
|
operations_failed+=("$cmd") |
|
continue |
|
fi |
|
|
|
# Check if command requires clean working directory |
|
if requires_clean_working_dir "$cmd"; then |
|
if ! is_working_directory_clean; then |
|
log_warning "Skipping $cmd on $repo_name: working directory is not clean" |
|
operations_failed+=("$cmd") |
|
continue |
|
fi |
|
fi |
|
|
|
# Check if command is dangerous |
|
if is_dangerous_command "$cmd"; then |
|
log_warning "Executing potentially dangerous command: $cmd on $repo_name" |
|
fi |
|
|
|
# Execute the command |
|
if [ "$DRY_RUN" = true ]; then |
|
log_success "Would perform: git $cmd on $repo_name" |
|
operations_performed+=("$cmd") |
|
else |
|
local success=false |
|
local git_cmd="" |
|
local success_msg="" |
|
local fail_msg="" |
|
local is_display=false |
|
|
|
case "$cmd" in |
|
fetch) |
|
git_cmd="fetch --all --prune" |
|
success_msg="Updated remote references" |
|
fail_msg="Failed to fetch" |
|
;; |
|
gc) |
|
git_cmd="gc --auto" |
|
success_msg="Optimized repository" |
|
fail_msg="Failed to optimize" |
|
;; |
|
prune) |
|
git_cmd="prune" |
|
success_msg="Cleaned unreachable objects" |
|
fail_msg="Failed to prune" |
|
;; |
|
fsck) |
|
git_cmd="fsck --no-dangling" |
|
success_msg="Repository integrity verified" |
|
fail_msg="Repository integrity issues found" |
|
;; |
|
pull) |
|
git_cmd="pull" |
|
success_msg="Pulled latest changes" |
|
fail_msg="Failed to pull" |
|
;; |
|
status) |
|
git_cmd="status" |
|
success_msg="Status" |
|
fail_msg="Failed to check status" |
|
is_display=true |
|
;; |
|
log) |
|
git_cmd="log --oneline -10" |
|
success_msg="Recent commits" |
|
fail_msg="Failed to check log" |
|
is_display=true |
|
;; |
|
branch) |
|
git_cmd="branch -a" |
|
success_msg="Branches" |
|
fail_msg="Failed to list branches" |
|
is_display=true |
|
;; |
|
remote) |
|
git_cmd="remote -v" |
|
success_msg="Remotes" |
|
fail_msg="Failed to list remotes" |
|
is_display=true |
|
;; |
|
config) |
|
git_cmd="config --list" |
|
success_msg="Config" |
|
fail_msg="Failed to list config" |
|
is_display=true |
|
;; |
|
diff) |
|
git_cmd="diff --stat" |
|
success_msg="Diff" |
|
fail_msg="Failed to check diff" |
|
is_display=true |
|
;; |
|
merge|rebase|reset|clean) |
|
# Capitalize first letter for better readability |
|
local cmd_capitalized="${cmd:0:1}" |
|
cmd_capitalized=$(echo "$cmd_capitalized" | tr '[:lower:]' '[:upper:]') |
|
cmd_capitalized="${cmd_capitalized}${cmd:1}" |
|
|
|
log_error "$cmd_capitalized command is too dangerous and not supported for safety reasons: $repo_name" |
|
log_insight "Use 'git $cmd' manually if you really need to $cmd" |
|
operations_failed+=("$cmd") |
|
continue |
|
;; |
|
*) |
|
# Generic command execution |
|
git_cmd="$cmd" |
|
success_msg="Executed git $cmd" |
|
fail_msg="Failed to execute git $cmd" |
|
;; |
|
esac |
|
|
|
if [ -n "$git_cmd" ]; then |
|
if execute_git_command "$git_cmd" "$repo_name" "$success_msg" "$fail_msg" "$is_display"; then |
|
success=true |
|
fi |
|
handle_command_result "$cmd" "$success" "operations_performed" "operations_failed" |
|
fi |
|
fi |
|
done |
|
|
|
# Log operations performed |
|
if [ ${#operations_performed[@]} -gt 0 ]; then |
|
log_verbose "Operations performed on $repo_name: ${operations_performed[*]}" |
|
fi |
|
|
|
# Return status based on results |
|
if [ ${#operations_failed[@]} -gt 0 ]; then |
|
# Add to failed repositories list |
|
FAILED_REPOS+=("$repo_name (failed operations: ${operations_failed[*]})") |
|
return 1 |
|
fi |
|
|
|
return 0 |
|
} |
|
|
|
# Generate detailed report |
|
generate_report() { |
|
echo |
|
log_info "=== GIT REPOSITORY MAINTENANCE REPORT ===" |
|
echo |
|
|
|
# Basic statistics |
|
log_stat "Total repositories processed: $TOTAL_REPOS" |
|
log_stat "Clean repositories: $CLEAN_REPOS" |
|
log_stat "Dirty repositories: $DIRTY_REPOS" |
|
log_stat "Repositories ahead of upstream: $AHEAD_REPOS" |
|
log_stat "Repositories behind upstream: $BEHIND_REPOS" |
|
log_stat "Diverged repositories: $DIVERGED_REPOS" |
|
log_stat "Repositories with stashes: $STASHED_REPOS" |
|
log_stat "Large repositories (>100MB): $LARGE_REPOS" |
|
log_stat "Old repositories (>30 days): $OLD_REPOS" |
|
|
|
echo |
|
|
|
# Insights and recommendations |
|
log_insight "=== INSIGHTS & RECOMMENDATIONS ===" |
|
|
|
if [ "$DIRTY_REPOS" -gt 0 ]; then |
|
log_warning "Found $DIRTY_REPOS repositories with uncommitted changes" |
|
log_insight "Consider reviewing and committing changes in these repositories" |
|
fi |
|
|
|
if [ "$AHEAD_REPOS" -gt 0 ]; then |
|
log_info "Found $AHEAD_REPOS repositories ahead of upstream" |
|
log_insight "Consider pushing changes to share your work" |
|
fi |
|
|
|
if [ "$BEHIND_REPOS" -gt 0 ]; then |
|
log_info "Found $BEHIND_REPOS repositories behind upstream" |
|
log_insight "Consider pulling latest changes to stay up to date" |
|
fi |
|
|
|
if [ "$DIVERGED_REPOS" -gt 0 ]; then |
|
log_warning "Found $DIVERGED_REPOS diverged repositories" |
|
log_insight "These repositories need manual attention to resolve conflicts" |
|
fi |
|
|
|
if [ "$STASHED_REPOS" -gt 0 ]; then |
|
log_info "Found $STASHED_REPOS repositories with stashed changes" |
|
log_insight "Consider reviewing and applying stashed changes" |
|
fi |
|
|
|
if [ "$LARGE_REPOS" -gt 0 ]; then |
|
log_warning "Found $LARGE_REPOS large repositories (>100MB)" |
|
log_insight "Consider using Git LFS for large files or cleaning up history" |
|
fi |
|
|
|
if [ "$OLD_REPOS" -gt 0 ]; then |
|
log_info "Found $OLD_REPOS repositories with no recent activity (>30 days)" |
|
log_insight "Consider archiving or cleaning up inactive repositories" |
|
fi |
|
|
|
# Health score calculation |
|
local health_score=100 |
|
if [ "$TOTAL_REPOS" -gt 0 ]; then |
|
# Calculate percentages with safe division |
|
local dirty_percent=$(( (DIRTY_REPOS * 100) / TOTAL_REPOS )) |
|
local diverged_percent=$(( (DIVERGED_REPOS * 100) / TOTAL_REPOS )) |
|
local behind_percent=$(( (BEHIND_REPOS * 100) / TOTAL_REPOS )) |
|
local stashed_percent=$(( (STASHED_REPOS * 100) / TOTAL_REPOS )) |
|
local large_percent=$(( (LARGE_REPOS * 100) / TOTAL_REPOS )) |
|
local old_percent=$(( (OLD_REPOS * 100) / TOTAL_REPOS )) |
|
|
|
# Calculate health score with weighted penalties |
|
# Dirty repos: -2 points per percent (high impact) |
|
# Diverged repos: -3 points per percent (very high impact) |
|
# Behind repos: -1 point per percent (medium impact) |
|
# Stashed repos: -0.5 points per percent (low impact) |
|
# Large repos: -0.5 points per percent (low impact) |
|
# Old repos: -0.3 points per percent (very low impact) |
|
|
|
local penalty=$(( |
|
(dirty_percent * 2) + |
|
(diverged_percent * 3) + |
|
(behind_percent * 1) + |
|
(stashed_percent / 2) + |
|
(large_percent / 2) + |
|
(old_percent * 3 / 10) |
|
)) |
|
|
|
health_score=$(( 100 - penalty )) |
|
|
|
# Ensure score is between 0 and 100 |
|
if [ "$health_score" -lt 0 ]; then |
|
health_score=0 |
|
elif [ "$health_score" -gt 100 ]; then |
|
health_score=100 |
|
fi |
|
else |
|
# No repositories found - set health score to 0 |
|
health_score=0 |
|
fi |
|
|
|
echo |
|
log_stat "Repository Health Score: $health_score/100" |
|
|
|
if [ "$health_score" -ge 90 ]; then |
|
log_success "Excellent repository health! π" |
|
elif [ "$health_score" -ge 70 ]; then |
|
log_info "Good repository health π" |
|
elif [ "$health_score" -ge 50 ]; then |
|
log_warning "Moderate repository health β οΈ" |
|
else |
|
log_error "Poor repository health - needs attention! π¨" |
|
fi |
|
} |
|
|
|
# Main execution function |
|
main() { |
|
log_info "Starting Git maintenance process..." |
|
|
|
# Parse arguments |
|
parse_args "$@" |
|
|
|
# Validate base directory |
|
if [ ! -d "$BASE_DIR" ]; then |
|
log_error "Directory '$BASE_DIR' does not exist or is not accessible" |
|
exit 1 |
|
fi |
|
|
|
if [ "$DRY_RUN" = true ]; then |
|
log_info "DRY RUN MODE - No actual maintenance will be performed" |
|
fi |
|
|
|
# Convert to absolute path |
|
if ! BASE_DIR=$(cd "$BASE_DIR" && pwd); then |
|
log_error "Failed to access directory: $BASE_DIR" |
|
exit 1 |
|
fi |
|
log_verbose "Searching for Git repositories in: $BASE_DIR" |
|
|
|
# Find all .git directories, excluding common build directories |
|
local git_dirs |
|
git_dirs=$(find "$BASE_DIR" -name '.git' -type d \ |
|
-not -path '*/target/*' \ |
|
-not -path '*/node_modules/*' \ |
|
-not -path '*/vendor/*' \ |
|
-not -path '*/.git/*' | sort) |
|
|
|
if [ -z "$git_dirs" ]; then |
|
log_info "No Git repositories found" |
|
exit 0 |
|
fi |
|
|
|
local repo_count=$(echo "$git_dirs" | wc -l) |
|
log_info "Found $repo_count Git repository(ies)" |
|
|
|
|
|
|
|
# Process repositories |
|
local processed=0 |
|
local failed=0 |
|
|
|
while IFS= read -r git_dir; do |
|
local repo_dir=$(dirname "$git_dir") |
|
|
|
if maintain_repo "$repo_dir"; then |
|
((processed++)) |
|
else |
|
((failed++)) |
|
fi |
|
done <<< "$git_dirs" |
|
|
|
# Generate final report |
|
generate_report |
|
|
|
echo |
|
log_info "=== MAINTENANCE SUMMARY ===" |
|
log_success "Repositories processed: $processed" |
|
if [ $failed -gt 0 ]; then |
|
log_error "Repositories failed: $failed" |
|
echo |
|
log_warning "Failed repositories:" |
|
for failed_repo in "${FAILED_REPOS[@]}"; do |
|
echo " β $failed_repo" |
|
done |
|
echo |
|
log_error "Some repositories had issues during maintenance" |
|
log_info "Check the verbose output above for specific error details" |
|
exit 1 |
|
fi |
|
|
|
if [ "$DRY_RUN" = true ]; then |
|
log_warning "This was a dry run. Use without --dry-run to actually perform maintenance." |
|
else |
|
log_success "Git maintenance completed successfully!" |
|
fi |
|
} |
|
|
|
# Execute main function |
|
main "$@" |