mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-01-26 11:14:08 +00:00
696 lines
19 KiB
Bash
Executable File
696 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Updates subfolders git projects.
|
|
#
|
|
# Actions taken: pull with rebase, autostashes own changes
|
|
# and prunes branches no longer in the remote.
|
|
#
|
|
# Copyright (c) 2023 Ismo Vuorinen. All Rights Reserved.
|
|
# License: MIT <https://opensource.org/license/mit/>
|
|
|
|
set -uo pipefail
|
|
|
|
# Script version
|
|
VERSION="1.0.0"
|
|
|
|
# Default settings
|
|
VERBOSE=0
|
|
QUIET=0
|
|
EXCLUDE_DIRS=""
|
|
CLEANUP=0
|
|
CONFIG_FILE=""
|
|
LOG_FILE=""
|
|
|
|
# Define color variables if terminal supports it
|
|
if [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
else
|
|
RED=''
|
|
GREEN=''
|
|
YELLOW=''
|
|
BLUE=''
|
|
CYAN=''
|
|
NC=''
|
|
fi
|
|
|
|
# Counters
|
|
TOTAL=0
|
|
SUCCESS=0
|
|
FAILED=0
|
|
CONFLICTS=0
|
|
UPDATED=0
|
|
PROCESSED=0
|
|
SKIPPED=0
|
|
UNTRACKED=0
|
|
UNMERGED=0
|
|
BRANCHES_CLEANED=0
|
|
|
|
# Function to display help message
|
|
show_help()
|
|
{
|
|
BIN=$(basename "$0")
|
|
cat << EOF
|
|
Usage: $BIN [OPTIONS]
|
|
|
|
Updates all git repositories in subdirectories.
|
|
|
|
Options:
|
|
--help, -h Display this help message and exit
|
|
--version, -v Display version information and exit
|
|
--verbose Display detailed output
|
|
--quiet, -q Suppress all output except errors
|
|
--exclude DIR Exclude directory from updates (can be used multiple times)
|
|
--cleanup Remove local branches that have been merged into current branch
|
|
--config FILE Read options from configuration file
|
|
--log FILE Log details and errors to FILE
|
|
|
|
Environment variables:
|
|
VERBOSE Set to 1 to enable verbose output
|
|
EXCLUDE_DIRS Space-separated list of directories to exclude
|
|
|
|
Examples:
|
|
$BIN Update all git repositories
|
|
$BIN --verbose Update with detailed output
|
|
$BIN --exclude node_modules --exclude vendor
|
|
Update repositories but skip node_modules
|
|
and vendor dirs
|
|
$BIN --cleanup Update and clean up merged branches
|
|
$BIN --config ~/.gitupdate.conf
|
|
Use options from config file
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# Function to display version
|
|
show_version()
|
|
{
|
|
echo "$(basename "$0") version $VERSION"
|
|
exit 0
|
|
}
|
|
|
|
# Function to log messages
|
|
# $1 - level (string: INFO, WARNING, ERROR)
|
|
# $2 - message (string)
|
|
log()
|
|
{
|
|
local level message timestamp
|
|
|
|
level="$1"
|
|
message="$2"
|
|
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
|
|
|
|
if [[ -n "$LOG_FILE" ]]; then
|
|
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
|
fi
|
|
|
|
# For errors, also log to stderr if in verbose mode
|
|
if [[ "$level" == "ERROR" && "$VERBOSE" -eq 1 && "$QUIET" -eq 0 ]]; then
|
|
echo -e "${RED}[$timestamp] [$level] $message${NC}" >&2
|
|
fi
|
|
}
|
|
|
|
# Process command-line arguments
|
|
process_args()
|
|
{
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--help | -h)
|
|
show_help
|
|
;;
|
|
--version | -v)
|
|
show_version
|
|
;;
|
|
--verbose)
|
|
VERBOSE=1
|
|
;;
|
|
--quiet | -q)
|
|
QUIET=1
|
|
;;
|
|
--exclude)
|
|
if [[ -n "$2" ]]; then
|
|
EXCLUDE_DIRS="$EXCLUDE_DIRS $2"
|
|
shift
|
|
else
|
|
echo "Error: --exclude requires a directory argument" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
--cleanup)
|
|
CLEANUP=1
|
|
;;
|
|
--config)
|
|
if [[ -n "$2" && -f "$2" ]]; then
|
|
CONFIG_FILE="$2"
|
|
shift
|
|
else
|
|
echo "Error: --config requires a valid file argument" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
--log)
|
|
if [[ -n "$2" ]]; then
|
|
LOG_FILE="$2"
|
|
shift
|
|
else
|
|
echo "Error: --log requires a file argument" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
echo "Use --help for usage information" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Process config file if specified
|
|
if [[ -n "$CONFIG_FILE" && -f "$CONFIG_FILE" ]]; then
|
|
log "INFO" "Reading configuration from $CONFIG_FILE"
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
# Skip comments and empty lines
|
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
|
[[ -z "${line// /}" ]] && continue
|
|
|
|
# Process each option from the config file
|
|
option=$(echo "$line" | awk '{print $1}')
|
|
value=$(echo "$line" | cut -d' ' -f2-)
|
|
|
|
case "$option" in
|
|
exclude) EXCLUDE_DIRS="$EXCLUDE_DIRS $value" ;;
|
|
verbose) VERBOSE=1 ;;
|
|
quiet) QUIET=1 ;;
|
|
cleanup) CLEANUP=1 ;;
|
|
log) LOG_FILE="$value" ;;
|
|
*) log "WARNING" "Unknown option in config file: $option" ;;
|
|
esac
|
|
done < "$CONFIG_FILE"
|
|
fi
|
|
|
|
# Environment variables override command-line options
|
|
[[ -n "${VERBOSE:-}" && "$VERBOSE" -eq 1 ]] && VERBOSE=1
|
|
# shellcheck disable=SC2269
|
|
[[ -n "${EXCLUDE_DIRS:-}" ]] && EXCLUDE_DIRS="${EXCLUDE_DIRS}"
|
|
|
|
# Initialize log file if specified
|
|
if [[ -n "$LOG_FILE" ]]; then
|
|
# Create log directory if it doesn't exist
|
|
mkdir -p "$(dirname "$LOG_FILE")" 2> /dev/null || true
|
|
# Initialize log file
|
|
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [INFO] Started git-update-dirs version $VERSION" > "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
# Terminal width for progress bar
|
|
TERM_WIDTH=$(tput cols 2> /dev/null || echo 120)
|
|
PROGRESS_WIDTH=$((TERM_WIDTH - 40))
|
|
MAX_DIR_LENGTH=$((TERM_WIDTH - PROGRESS_WIDTH - 25)) # Add 5 for extra padding
|
|
|
|
# Last status message, used for clearing properly
|
|
LAST_STATUS_LENGTH=0
|
|
|
|
# Function to print messages if VERBOSE is enabled
|
|
# $1 - message (string)
|
|
msg()
|
|
{
|
|
local message
|
|
message="$1"
|
|
if [[ "$VERBOSE" -eq 1 && "$QUIET" -eq 0 ]]; then
|
|
echo "$message"
|
|
[[ -n "$LOG_FILE" ]] && log "INFO" "$message"
|
|
elif [[ -n "$LOG_FILE" ]]; then
|
|
log "DEBUG" "$message"
|
|
fi
|
|
}
|
|
|
|
# Function to print normal output unless QUIET is enabled
|
|
# $1 - message (string)
|
|
print()
|
|
{
|
|
local message
|
|
message="$1"
|
|
if [[ "$QUIET" -eq 0 ]]; then
|
|
echo -e "$message"
|
|
[[ -n "$LOG_FILE" ]] && log "INFO" "$message"
|
|
elif [[ -n "$LOG_FILE" ]]; then
|
|
log "INFO" "$message"
|
|
fi
|
|
}
|
|
|
|
# Function to display progress bar
|
|
# $1 - current (int)
|
|
# $2 - total (int)
|
|
# $3 - status message (string)
|
|
show_progress()
|
|
{
|
|
[[ "$QUIET" -eq 1 ]] && return
|
|
|
|
local current total status percent filled empty
|
|
|
|
current=$1
|
|
total=$2
|
|
status=$3
|
|
|
|
# If TERM_WIDTH is less than LAST_STATUS_LENGTH set TERM_WIDTH
|
|
# to it.
|
|
if [[ $TERM_WIDTH -lt $LAST_STATUS_LENGTH ]]; then
|
|
TERM_WIDTH=$LAST_STATUS_LENGTH
|
|
fi
|
|
|
|
# Clear the entire line before updating to avoid artifacts
|
|
printf "\r%-${TERM_WIDTH}s" " "
|
|
|
|
# Avoid division by zero
|
|
if [[ "$total" -eq 0 ]]; then
|
|
percent=0
|
|
else
|
|
percent=$((current * 100 / total))
|
|
fi
|
|
|
|
filled=$((percent * PROGRESS_WIDTH / 100))
|
|
# Ensure filled doesn't exceed PROGRESS_WIDTH
|
|
[[ $filled -gt $PROGRESS_WIDTH ]] && filled=$PROGRESS_WIDTH
|
|
empty=$((PROGRESS_WIDTH - filled))
|
|
|
|
# Truncate status message if too long
|
|
if [[ ${#status} -gt $MAX_DIR_LENGTH ]]; then
|
|
status="...${status:$((${#status} - MAX_DIR_LENGTH + 4))}"
|
|
fi
|
|
|
|
# Pad the status message to ensure consistent width and add extra space
|
|
printf -v padded_status "%-${MAX_DIR_LENGTH}s" "$status"
|
|
|
|
# Create and display the progress bar with fixed width for percentage and colors
|
|
printf "\r[${BLUE}%s${NC}%s] ${GREEN}%3d%%${NC} ${CYAN}%s${NC}" \
|
|
"$(printf '#%.0s' $(seq 1 $filled))" \
|
|
"$(printf ' %.0s' $(seq 1 $empty))" \
|
|
"$percent" \
|
|
"$padded_status"
|
|
|
|
# Store the length of the current status
|
|
LAST_STATUS_LENGTH=${#status}
|
|
|
|
# Log progress if logging is enabled
|
|
[[ -n "$LOG_FILE" ]] && log "DEBUG" "Progress: $percent% - $status"
|
|
}
|
|
|
|
# Is the directory path excluded?
|
|
# $1: Directory path
|
|
# Return 0 if the directory should be skipped, 1 otherwise
|
|
excluded_path()
|
|
{
|
|
local dir home
|
|
dir="$(realpath "$1")"
|
|
home="$(realpath "$HOME")"
|
|
|
|
# Check if directory should be excluded
|
|
for exclude in $EXCLUDE_DIRS; do
|
|
# Check for parts of the directory name
|
|
if [[ "$dir" == *"$exclude"* ]] || [[ "$dir" == "$exclude" ]]; then
|
|
msg "Skipping excluded directory: $dir"
|
|
return 0
|
|
fi
|
|
|
|
# Run only if home is not empty
|
|
if [[ -n "$home" ]]; then
|
|
# Remove home directory from path
|
|
relative_dir="${dir/"$home"/}"
|
|
|
|
# Check if we should exclude based on relative paths based on the home directory
|
|
if [[ "$relative_dir" == *"$exclude"* ]] || [[ "$relative_dir" == "$exclude" ]]; then
|
|
msg "Skipping excluded relative directory: $dir"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Check if it's a git repository
|
|
if [[ ! -d "$dir/.git" ]]; then
|
|
msg "Skipping non-git directory: $dir"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Function to count git repositories
|
|
count_git_repos()
|
|
{
|
|
local count=0
|
|
for dir in */; do
|
|
if ! excluded_path "$dir"; then
|
|
((count++))
|
|
fi
|
|
done
|
|
echo $count
|
|
}
|
|
|
|
# Check for unmerged files or conflicts in a git repository
|
|
# Returns 0 if there are unmerged files, 1 otherwise
|
|
has_unmerged_files()
|
|
{
|
|
git ls-files --unmerged | grep -q "^" \
|
|
&& return 0 || return 1
|
|
}
|
|
|
|
# Check for clean working directory
|
|
# Returns 0 if working directory is clean, 1 otherwise
|
|
is_repo_clean()
|
|
{
|
|
git diff --quiet \
|
|
&& git diff --cached --quiet \
|
|
&& return 0 || return 1
|
|
}
|
|
|
|
# Function to clean up local branches that have been merged
|
|
# Returns the number of branches cleaned
|
|
cleanup_branches()
|
|
{
|
|
local cleaned=0
|
|
local current_branch output
|
|
|
|
current_branch=$(git symbolic-ref --short HEAD 2> /dev/null)
|
|
|
|
# Skip branch cleanup if we're not on a main branch
|
|
if [[ ! "$current_branch" =~ ^(master|main|develop)$ ]]; then
|
|
msg "Skipping branch cleanup: not on a main branch ($current_branch)"
|
|
return 0
|
|
fi
|
|
|
|
# Get list of merged branches, excluding current branch, master, main, and develop
|
|
output=$(git branch --merged | grep -v -E "^\*|master|main|develop" | sed 's/^[[:space:]]*//')
|
|
|
|
if [[ -n "$output" ]]; then
|
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
msg "Cleaning up merged branches in $(pwd):"
|
|
echo "$output" | while read -r branch; do
|
|
msg " - $branch"
|
|
done
|
|
fi
|
|
|
|
# Delete branches
|
|
for branch in $output; do
|
|
if [[ -n "$branch" ]]; then
|
|
if git branch -d "$branch" &> /dev/null; then
|
|
((cleaned++))
|
|
log "INFO" "Deleted merged branch $branch in $(pwd)"
|
|
else
|
|
log "WARNING" "Failed to delete branch $branch in $(pwd)"
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
return $cleaned
|
|
}
|
|
|
|
# Function to update a git repository
|
|
# $1 - directory (string)
|
|
update_repo()
|
|
{
|
|
local dir output exit_status git_args current_branch \
|
|
remote_name cleaned_branches
|
|
|
|
dir="$1"
|
|
log "INFO" "Processing repository: $dir"
|
|
|
|
# Increment the processed counter
|
|
((PROCESSED++))
|
|
|
|
# Show progress before starting the operation
|
|
show_progress "$PROCESSED" "$TOTAL" "${dir%/}"
|
|
|
|
cd "$dir" 2> /dev/null || {
|
|
log "ERROR" "Could not enter directory $dir"
|
|
echo -e "\n${RED}Error: Could not enter directory $dir${NC}" >&2
|
|
((FAILED++))
|
|
return 1
|
|
}
|
|
|
|
# If there are no remotes, skip
|
|
if ! git remote -v &> /dev/null; then
|
|
log "INFO" "Skipping directory with no remotes: $dir"
|
|
msg "Skipping directory with no remotes: $dir"
|
|
((SKIPPED++))
|
|
cd - > /dev/null || true
|
|
return 1
|
|
fi
|
|
|
|
# Get current branch name
|
|
current_branch=$(git symbolic-ref --short HEAD 2> /dev/null)
|
|
if [[ -z "$current_branch" ]]; then
|
|
log "INFO" "Skipping repository in detached HEAD state: $dir"
|
|
msg "Skipping repository in detached HEAD state: $dir"
|
|
((SKIPPED++))
|
|
cd - > /dev/null || true
|
|
return 1
|
|
fi
|
|
|
|
# Check if current branch has tracking information
|
|
eval "git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null" &> /dev/null || {
|
|
log "INFO" "Skipping branch '$current_branch' without tracking info in $dir"
|
|
msg "Skipping branch '$current_branch' without tracking info in $dir"
|
|
((SKIPPED++))
|
|
cd - > /dev/null || true
|
|
return 1
|
|
}
|
|
|
|
# Check if remote is accessible
|
|
remote_name=$(git config --get branch."$current_branch".remote)
|
|
if [[ -n "$remote_name" ]]; then
|
|
if ! git ls-remote --exit-code "$remote_name" &> /dev/null; then
|
|
log "WARNING" "Skipping repository with inaccessible remote '$remote_name': $dir"
|
|
msg "Skipping repository with inaccessible remote: $dir"
|
|
((SKIPPED++))
|
|
cd - > /dev/null || true
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Check for unmerged files before attempting pull
|
|
if has_unmerged_files; then
|
|
log "WARNING" "Skipping repository with unmerged files: $dir"
|
|
msg "Skipping repository with unmerged files: $dir"
|
|
((UNMERGED++))
|
|
cd - > /dev/null || true
|
|
return 1
|
|
fi
|
|
|
|
# Configure Git arguments based on verbosity
|
|
git_args="--rebase --autostash --prune"
|
|
if [[ "$VERBOSE" -eq 0 ]]; then
|
|
git_args="$git_args --quiet"
|
|
fi
|
|
|
|
# Disable Git hints and set other environment variables
|
|
export GIT_MERGE_AUTOEDIT=no
|
|
export GIT_CONFIG_COUNT=4
|
|
export GIT_CONFIG_KEY_0="advice.skipHints"
|
|
export GIT_CONFIG_VALUE_0="true"
|
|
export GIT_CONFIG_KEY_1="advice.detachedHead"
|
|
export GIT_CONFIG_VALUE_1="false"
|
|
export GIT_CONFIG_KEY_2="advice.pushUpdateRejected"
|
|
export GIT_CONFIG_VALUE_2="false"
|
|
export GIT_CONFIG_KEY_3="advice.statusHints"
|
|
export GIT_CONFIG_VALUE_3="false"
|
|
|
|
# Capture the output of git pull
|
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
# shellcheck disable=SC2086
|
|
output=$(git pull $git_args 2>&1)
|
|
exit_status=$?
|
|
# In verbose mode, show the git output
|
|
[[ "$QUIET" -eq 0 ]] && echo -e "\n$output\n"
|
|
log "DEBUG" "Git pull output: $output"
|
|
else
|
|
# In non-verbose mode, suppress normal output but capture errors
|
|
# shellcheck disable=SC2086
|
|
output=$(git pull $git_args 2>&1) || {
|
|
exit_status=$?
|
|
}
|
|
|
|
# If no error occurred, set exit_status to 0
|
|
exit_status=${exit_status:-0}
|
|
fi
|
|
|
|
# Unset environment variables
|
|
unset GIT_MERGE_AUTOEDIT GIT_CONFIG_COUNT \
|
|
GIT_CONFIG_KEY_0 GIT_CONFIG_KEY_1 \
|
|
GIT_CONFIG_KEY_2 GIT_CONFIG_KEY_3 \
|
|
GIT_CONFIG_VALUE_0 GIT_CONFIG_VALUE_1 \
|
|
GIT_CONFIG_VALUE_2 GIT_CONFIG_VALUE_3
|
|
|
|
# Check for specific error conditions
|
|
if echo "$output" | grep -q "Merge conflict"; then
|
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
echo ""
|
|
echo -e "${YELLOW}Merge conflict detected in $dir. Aborting update.${NC}" >&2
|
|
fi
|
|
log "WARNING" "Merge conflict detected in $dir. Aborting update."
|
|
git rebase --abort &> /dev/null || git merge --abort &> /dev/null || true
|
|
((CONFLICTS++))
|
|
elif echo "$output" | grep -q "unmerged files"; then
|
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
echo ""
|
|
echo -e "${YELLOW}Unmerged files detected in $dir. Aborting update.${NC}" >&2
|
|
fi
|
|
log "WARNING" "Unmerged files detected in $dir. Aborting update."
|
|
((UNMERGED++))
|
|
elif echo "$output" | grep -q "untracked working tree files would be overwritten by merge"; then
|
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
echo ""
|
|
echo -e "${YELLOW}Untracked files would be overwritten in $dir. Aborting update.${NC}" >&2
|
|
fi
|
|
log "WARNING" "Untracked files would be overwritten in $dir. Aborting update."
|
|
((UNTRACKED++))
|
|
elif [[ $exit_status -ne 0 ]]; then
|
|
if [[ "$VERBOSE" -eq 1 || "$QUIET" -eq 0 ]]; then
|
|
echo ""
|
|
echo -e "${RED}Error updating $dir${NC}" >&2
|
|
echo "$output" >&2
|
|
fi
|
|
log "ERROR" "Failed to update $dir: $output"
|
|
((FAILED++))
|
|
else
|
|
# Check if any changes were pulled
|
|
if echo "$output" | grep -qE '(file changed|files changed|insertions|deletions)' \
|
|
|| ! echo "$output" | grep -q "Already up to date."; then
|
|
log "INFO" "Repository updated with changes: $dir"
|
|
((UPDATED++))
|
|
else
|
|
log "INFO" "Repository already up to date: $dir"
|
|
fi
|
|
((SUCCESS++))
|
|
|
|
# Clean up branches if requested
|
|
if [[ "$CLEANUP" -eq 1 ]]; then
|
|
cleaned_branches=$(cleanup_branches)
|
|
if [[ $cleaned_branches -gt 0 ]]; then
|
|
((BRANCHES_CLEANED += cleaned_branches))
|
|
log "INFO" "Cleaned up $cleaned_branches merged branches in $dir"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Return to original directory
|
|
cd - > /dev/null || true
|
|
|
|
# Show progress after completion
|
|
show_progress "$PROCESSED" "$TOTAL" "${dir%/} - Done"
|
|
}
|
|
|
|
# Main function to update all subfolder git repositories
|
|
main()
|
|
{
|
|
local current_dir start_time end_time duration
|
|
|
|
# Record start time
|
|
start_time=$(date +%s)
|
|
|
|
# Process command-line args before doing anything else
|
|
process_args "$@"
|
|
|
|
# Save current directory to return to it later
|
|
current_dir=$(pwd)
|
|
log "INFO" "Starting repository updates in $current_dir"
|
|
|
|
# Count repositories and set TOTAL
|
|
TOTAL=$(count_git_repos)
|
|
print "Found $TOTAL git repositories to update"
|
|
|
|
# Reset other counters
|
|
PROCESSED=0
|
|
SUCCESS=0
|
|
FAILED=0
|
|
CONFLICTS=0
|
|
UPDATED=0
|
|
SKIPPED=0
|
|
UNTRACKED=0
|
|
UNMERGED=0
|
|
BRANCHES_CLEANED=0
|
|
|
|
# Process each repository
|
|
for dir in */; do
|
|
# Skip if excluded
|
|
if excluded_path "$dir"; then
|
|
continue
|
|
fi
|
|
update_repo "$dir"
|
|
done
|
|
|
|
# Return to original directory
|
|
cd "$current_dir" || true
|
|
|
|
# Clear the progress line completely
|
|
[[ "$QUIET" -eq 0 ]] && printf "\r%-${TERM_WIDTH}s\r" " "
|
|
|
|
# Calculate duration
|
|
end_time=$(date +%s)
|
|
duration=$((end_time - start_time))
|
|
minutes=$((duration / 60))
|
|
seconds=$((duration % 60))
|
|
|
|
# Format duration nicely
|
|
if [[ $minutes -gt 0 ]]; then
|
|
duration_str="${minutes}m ${seconds}s"
|
|
else
|
|
duration_str="${seconds}s"
|
|
fi
|
|
|
|
# Print summary unless quiet mode is enabled
|
|
if [[ "$QUIET" -eq 0 ]]; then
|
|
echo ""
|
|
print "${GREEN}Summary: Updated $SUCCESS/$TOTAL repositories successfully in $duration_str.${NC}"
|
|
print "${CYAN}Repositories with changes pulled: $UPDATED${NC}"
|
|
|
|
if [[ $SKIPPED -gt 0 ]]; then
|
|
print "${YELLOW}Skipped $SKIPPED repositories (no tracking branch or other issues).${NC}"
|
|
fi
|
|
|
|
if [[ $UNMERGED -gt 0 ]]; then
|
|
print "${YELLOW}Skipped $UNMERGED repositories with unmerged files.${NC}"
|
|
fi
|
|
|
|
if [[ $UNTRACKED -gt 0 ]]; then
|
|
print "${YELLOW}Skipped $UNTRACKED repositories with untracked files that would be overwritten.${NC}"
|
|
fi
|
|
|
|
if [[ $CONFLICTS -gt 0 ]]; then
|
|
print "${YELLOW}Encountered merge conflicts in $CONFLICTS repositories.${NC}"
|
|
fi
|
|
|
|
if [[ $CLEANUP -eq 1 && $BRANCHES_CLEANED -gt 0 ]]; then
|
|
print "${BLUE}Cleaned up $BRANCHES_CLEANED merged branches.${NC}"
|
|
fi
|
|
|
|
if [[ $FAILED -gt 0 ]]; then
|
|
echo -e "${RED}Failed to update $FAILED repositories.${NC}" >&2
|
|
else
|
|
print "${GREEN}Done.${NC}"
|
|
fi
|
|
fi
|
|
|
|
# Log final summary
|
|
if [[ -n "$LOG_FILE" ]]; then
|
|
log "INFO" "Completed in $duration_str"
|
|
log "INFO" "Summary: $SUCCESS/$TOTAL repositories updated successfully"
|
|
log "INFO" "Repositories with changes pulled: $UPDATED"
|
|
log "INFO" "Skipped: $SKIPPED, Unmerged: $UNMERGED, Untracked: $UNTRACKED, Conflicts: $CONFLICTS, Failed: $FAILED"
|
|
if [[ $CLEANUP -eq 1 ]]; then
|
|
log "INFO" "Branches cleaned up: $BRANCHES_CLEANED"
|
|
fi
|
|
fi
|
|
|
|
# Return appropriate exit code
|
|
[[ $FAILED -gt 0 ]] && return 1 || return 0
|
|
}
|
|
|
|
# Call main with all arguments
|
|
main "$@"
|