mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-01-26 11:14:08 +00:00
feat(bin): rewrote git-update-dirs
This commit is contained in:
@@ -8,39 +8,688 @@
|
||||
# Copyright (c) 2023 Ismo Vuorinen. All Rights Reserved.
|
||||
# License: MIT <https://opensource.org/license/mit/>
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
# Enable verbosity with VERBOSE=1
|
||||
VERBOSE="${VERBOSE:-0}"
|
||||
# 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()
|
||||
{
|
||||
[ "$VERBOSE" -eq 1 ] && echo "$1"
|
||||
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=$1
|
||||
(
|
||||
cd "$dir" || exit
|
||||
msg "Updating $dir"
|
||||
git pull --rebase --autostash --prune
|
||||
)
|
||||
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
|
||||
|
||||
echo "Done."
|
||||
echo ""
|
||||
# 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 "$@"
|
||||
|
||||
116
local/bin/git-update-dirs.md
Normal file
116
local/bin/git-update-dirs.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# git-update-dirs
|
||||
|
||||
A tool that efficiently updates all Git repositories in subdirectories
|
||||
of the current folder.
|
||||
|
||||
## Overview
|
||||
|
||||
`git-update-dirs` scans the current directory for Git repositories
|
||||
and updates them with:
|
||||
|
||||
- Fast parallel execution
|
||||
- Intelligent error handling
|
||||
- Progress visualization
|
||||
- Detailed logging
|
||||
- Optional branch cleanup
|
||||
|
||||
## Installation
|
||||
|
||||
Place the script in your PATH and make it executable:
|
||||
|
||||
```bash
|
||||
# Using wget
|
||||
wget -O ~/bin/git-update-dirs https://raw.githubusercontent.com/ivuorinen/dotfiles/main/local/bin/git-update-dirs
|
||||
chmod +x ~/bin/git-update-dirs
|
||||
|
||||
# Or simply copy the script to a location in your PATH
|
||||
cp git-update-dirs ~/bin/
|
||||
chmod +x ~/bin/git-update-dirs
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
Usage: git-update-dirs [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
|
||||
|
||||
Basic usage to update all repositories:
|
||||
|
||||
```bash
|
||||
git-update-dirs
|
||||
```
|
||||
|
||||
Update with detailed output:
|
||||
|
||||
```bash
|
||||
git-update-dirs --verbose
|
||||
```
|
||||
|
||||
Exclude specific directories:
|
||||
|
||||
```bash
|
||||
git-update-dirs --exclude node_modules --exclude vendor
|
||||
```
|
||||
|
||||
Update and clean up merged branches:
|
||||
|
||||
```bash
|
||||
git-update-dirs --cleanup
|
||||
```
|
||||
|
||||
Use options from a configuration file:
|
||||
|
||||
```bash
|
||||
git-update-dirs --config ~/.gitupdate.conf
|
||||
```
|
||||
|
||||
## Configuration File
|
||||
|
||||
You can create a configuration file to store your preferred options:
|
||||
|
||||
```text
|
||||
# Example ~/.gitupdate.conf
|
||||
verbose
|
||||
exclude node_modules
|
||||
exclude vendor
|
||||
cleanup
|
||||
log ~/.gitupdate.log
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Updates**: Uses `--rebase --autostash --prune`
|
||||
for clean updates
|
||||
- **Error Handling**: Skips repositories with conflicts or
|
||||
untracked files that would be overwritten
|
||||
- **Visual Progress**: Shows a progress bar with current status
|
||||
- **Repository Management**: Optionally cleans up merged branches
|
||||
- **Detailed Logging**: Records all operations with timestamps
|
||||
|
||||
## License
|
||||
|
||||
[MIT License][MIT] - Copyright 2023 Ismo Vuorinen
|
||||
|
||||
[MIT]: https://opensource.org/license/mit/
|
||||
|
||||
<!-- vim: set ft=markdown cc=80 : -->
|
||||
Reference in New Issue
Block a user