Compare commits

...

7 Commits

12 changed files with 2813 additions and 1501 deletions

File diff suppressed because it is too large Load Diff

185
local/bin/git-dirty.md Normal file
View File

@@ -0,0 +1,185 @@
# git-dirty
A powerful tool to recursively check Git repository status across multiple directories.
## Overview
`git-dirty` scans directories to identify Git repositories and reports their status.
It quickly shows which repositories have uncommitted changes, untracked files, or need
to be pushed, making it easy to maintain clean workspaces across multiple projects.
## Features
- 🔍 **Recursive scanning** of directories to find Git repositories
- 🚦 **Visual indicators** showing repository status (clean/dirty/not git)
- 🔄 **Parallel processing** for faster scanning of large directory structures
- 🌳 **Tree-like display** with customizable depth
- 📊 **Progress tracking** for large repository scans
- 🎨 **Colorized output** (can be disabled)
- 📏 **Path truncation** for cleaner display
- 🔀 **Branch display** with smart formatting for main branches
- ⏱️ **Performance metrics** showing scan speed and ETA
- 📈 **Smart sorting** to maintain tree hierarchy in output
- ⚙️ **Configurable** via environment variables or config files
## Installation
Place the script in your PATH and make it executable:
```bash
# Clone the repository or download the script
curl -o ~/.local/bin/git-dirty https://raw.githubusercontent.com/ivuorinen/dotfiles/main/local/bin/git-dirty
chmod +x ~/.local/bin/git-dirty
```
## Usage
```bash
git-dirty [OPTIONS] [DIRECTORY]
# or if the file is in the PATH, you can use it as an git command
git dirty [OPTIONS] [DIRECTORY]
# to show help
git dirty -h
```
If no directory is specified, it will use `$HOME/Code` as the default.
### Options
- `-h` Show help message and exit
- `-d NUM` Set maximum depth for showing non-git directories (default: 5)
- `-p` Process directories in parallel (requires 'parallel' command)
- `-v` Enable verbose output
- `-a` Show all status details (stash, untracked files, etc.)
- `-e PATTERNS` Additional patterns to exclude (comma separated)
- `-m NUM` Set maximum recursion depth (default: 15)
- `-c` Toggle colorized output
- `-t` Toggle path truncation
- `-b` Toggle branch name display
### Examples
```bash
# Check default directory
git-dirty
# Check specific directory
git-dirty ~/Projects
# Check with extended status information
git-dirty -a ~/Code
# Exclude certain directories
git-dirty -e 'build,dist,node_modules' ~/Code
# Use parallel processing for faster results
git-dirty -p ~/large-directory
# Hide branch names in output
git-dirty -b ~/Code
```
## Status Indicators
The script uses the following status indicators:
- ✅ Clean repository
- ❌ Dirty repository with details:
- `M` = Modified files
- `S` = Staged changes
- `?` = Untracked files (with `-a` flag)
- `$` = Stashed changes (with `-a` flag)
- `↑` = Unpushed commits
- ⚠️ Not a Git repository
## Branch Display
The script shows branch names for repositories not on main branches. This helps identify
repositories where work is happening on feature branches. Main branches (configurable as
`main`, `master`, and `trunk` by default) are hidden to reduce output clutter.
## Configuration
You can customize the default behavior using environment variables:
```bash
# in your .bashrc, .zshrc, etc.
export GIT_DIRTY_DIR="$HOME/Projects" # Set default directory
export GIT_DIRTY_DEPTH=3 # Show non-git dirs up to depth 3
export GIT_DIRTY_MAXDEPTH=15 # Maximum recursion depth
export GIT_DIRTY_COLOR=1 # Enable colorized output (0 to disable)
export GIT_DIRTY_TRUNCATE=1 # Enable path truncation (0 to disable)
export GIT_DIRTY_SHOW_BRANCH=1 # Show branch names (0 to disable)
export GIT_DIRTY_MAIN_BRANCHES="main master trunk" # Main branches (not shown in output)
export GIT_DIRTY_EXCLUDE="node_modules vendor .cache build dist .tests .test" # Default excludes
```
### Config File
You can also create a configuration file at `$XDG_CONFIG_HOME/git-dirty/config`
(typically `~/.config/git-dirty/config`):
```bash
# Example config file
GIT_DIRTY_DIR="$HOME/Projects"
GIT_DIRTY_DEPTH=3
GIT_DIRTY_CHECK_STASH=1
GIT_DIRTY_SHOW_BRANCH=1
GIT_DIRTY_MAIN_BRANCHES="main master trunk develop"
GIT_DIRTY_EXCLUDE="node_modules vendor .cache build dist tmp"
```
## Skip Directories from Checking
If you want to skip a directory from being checked, add a `.ignore` file next to the `.git` folder.
You can add `.ignore` to your global `.gitignore` file to avoid committing these files.
## Performance Features
- **Parallel processing**: Significant speed improvements when using the `-p` flag
- **Progress bars**: Real-time feedback on scanning progress with ETA
- **Rate limiting**: Controls parallel jobs to prevent system overloading
- **Smart directory traversal**: Skips excluded directories for faster processing
## Tips
1. **Add an alias**: Create an alias in your shell configuration:
```bash
alias gd='git-dirty'
```
2. **Use it with specific directories**:
```bash
git-dirty ~/specific/project
```
3. **Run in parallel mode for large codebases**:
```bash
git-dirty -p ~/huge-monorepo
```
4. **Turn off branch display for cleaner output**:
```bash
git-dirty -b
```
## Requirements
- Bash (version 4+)
- Git
- Optional: GNU Parallel for parallel processing
## License
MIT
## Credits
Created with ❤️ by Ismo Vuorinen
<!-- vim: set ft=markdown cc=80 : -->

View File

@@ -10,42 +10,116 @@ set -euo pipefail
# Enable verbosity with VERBOSE=1
VERBOSE="${VERBOSE:-0}"
DEBUG="${DEBUG:-0}"
if [ "$DEBUG" -eq 1 ]; then
set -x
fi
# Function to print messages if VERBOSE is enabled
# $1 - message (string)
msg()
{
[ "$VERBOSE" -eq 1 ] && echo "$1"
if [ "$VERBOSE" -eq 1 ]; then
echo "$1"
fi
return 0
}
# Show red error message
# $1 - message (string)
msg_err()
{
echo "$(tput setaf 1)Error: $1$(tput sgr0)"
}
# Function to perform git fsck on a repository
# $1 - directory (string)
fsck_repo()
{
local dir=$1
msg "Processing dir: $dir"
(
cd "$dir" || exit 1
if [ -d ".git" ]; then
git fsck --no-dangling --full --no-progress
echo ""
fi
)
local dir dirs collected_errors collected_repos
dir="$(realpath "$1")"
collected_errors="$2"
collected_repos="$3"
msg "Processing: $dir"
if [ ! -d "$dir/.git" ]; then
echo "$dir" >> "$collected_errors"
msg " (!) Skipping (no .git)"
return
fi
echo "$dir" >> "$collected_repos"
if ! git -C "$dir" fsck --no-dangling --full --no-progress 2>&1 | grep -vE '^notice:'; then
echo "$dir" >> "$collected_errors"
msg " (!) Issues found in: $dir"
fi
}
# Main function
main()
{
local starting_path=${1:-$(pwd)}
local dirs
local starting_path errors_file repo_count_file dirs dirs_count REPO_COUNT ERROR_COUNT
starting_path=${1:-$(pwd)}
errors_file="${2:-/tmp/git-fsck-errors.txt}"
repo_count_file="${3:-/tmp/git-fsck-repo-count.txt}"
# If starting_point=. or starting_point=.., set it to the current directory
if [ "$starting_path" = "." ]; then
starting_path="$(pwd)"
elif [ "$starting_path" = ".." ]; then
starting_path="$(dirname "$(pwd)")"
fi
# Check if starting_path exists
if [ ! -d "$starting_path" ]; then
msg_err "Error: Directory '$starting_path' not found."
return 1
fi
# Collect the directories
dirs=$(find "$starting_path" -mindepth 1 -maxdepth 1 -type d)
# Filter out unwanted directories
dirs=$(echo "$dirs" \
| grep -vE '^\./\.git$' \
| grep -vE '^\./\.svn$' \
| grep -vE '^\./\.hg$' \
| grep -vE '^\./\.bzr$')
# Count the directories for reporting and processing
dirs_count=$(echo "$dirs" | wc -l | tr -d ' ')
# If dirs_count is 0, exit early
if [ "$dirs_count" -eq 0 ]; then
msg_err "No directories found in $starting_path."
return 0
fi
echo "Checking $dirs_count directories in $starting_path..."
for dir in $dirs; do
fsck_repo "$dir"
fsck_repo "$dir" "$errors_file" "$repo_count_file"
done
# Collect the results and trim the output
REPO_COUNT=$(wc -l < "$repo_count_file" | tr -d ' ')
ERROR_COUNT=$(wc -l < "$errors_file" | tr -d ' ')
rm -f "$errors_file" "$repo_count_file"
echo ""
echo "Done."
echo "Summary:"
echo "Checked $REPO_COUNT repositories from $dirs_count directories."
if [ "$ERROR_COUNT" -gt 0 ]; then
echo "Found issues in $ERROR_COUNT repositories."
return 1
else
echo "All repositories passed."
return 0
fi
}
main "$@"
exit $?

View File

@@ -0,0 +1,65 @@
# git-fsck-dirs
A utility to check multiple Git repositories for corruption
using `git fsck`.
## Overview
`git-fsck-dirs` scans all subdirectories within a specified path
and performs a `git fsck` operation on each Git repository found.
This helps identify corrupted repositories or those with integrity
issues.
## Features
- Recursively checks all Git repositories in the given directory
- Provides a summary of repositories checked and any issues found
- Filters out common version control directories (.git, .svn, etc.)
- Supports verbose and debug modes
## Usage
```bash
git-fsck-dirs [path] [errors_file] [repo_count_file]
git fsck-dirs [path] [errors_file] [repo_count_file]
```
### Arguments
- `path`: Directory to scan (defaults to current directory)
- `errors_file`: Path to save errors (defaults to /tmp/git-fsck-errors.txt)
- `repo_count_file`: Path to save repository count
(defaults to /tmp/git-fsck-repo-count.txt)
### Environment Variables
- `VERBOSE=1`: Enable verbose output
- `DEBUG=1`: Enable debug mode (shows executed commands)
## Examples
Check repositories in the current directory:
```bash
git fsck-dirs
git-fsck-dirs
```
Check repositories in a specific directory:
```bash
git fsck-dirs ~/projects
git-fsck-dirs ~/projects
```
Enable verbose output:
```bash
VERBOSE=1 git-fsck-dirs
```
## License
MIT License - Copyright 2023 Ismo Vuorinen
<!-- vim: set ft=markdown cc=80 : -->

View File

@@ -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 "$@"

View 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 : -->

View File

@@ -1,49 +0,0 @@
#!/usr/bin/swift
// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Zalgo Text
// @raycast.mode silent
// @raycast.author Adam Zethraeus
// @raycast.authorURL https://github.com/adam-zethraeus
// @raycast.packageName Conversions
// @raycast.icon 👹
// @raycast.argument1 { "type": "text", "placeholder": "Text to Z̶̶͚̯͗a̩̞͜͜l̫͕ͬͨ̿g͈̫͂ͤ͆͢o̠͚̞ͥ" }
// @raycast.argument2 { "type": "text", "optional": true, "placeholder": "Intensity=5" }
// Documentation:
// @raycast.description Converts text to z̫̫̐a̳ͩl̓͂̀ͅg͔̚o̷̦̣͢ t̳͆ḛ̊͟ẍ̮̝́t̵̔ͯ͝
import Cocoa
// zalgo function credit mattt @ https://gist.github.com/mattt/b46ab5027f1ee6ab1a45583a41240033
func zalgo(_ string: String, intensity: Int = 5) -> String {
let combiningDiacriticMarks = 0x0300...0x036f
let latinAlphabetUppercase = 0x0041...0x005a
let latinAlphabetLowercase = 0x0061...0x007a
var output: [UnicodeScalar] = []
for scalar in string.unicodeScalars {
output.append(scalar)
guard (latinAlphabetUppercase).contains(numericCast(scalar.value)) ||
(latinAlphabetLowercase).contains(numericCast(scalar.value))
else {
continue
}
for _ in 0...(Int.random(in: 1...intensity)) {
let randomScalarValue = Int.random(in: combiningDiacriticMarks)
output.append(Unicode.Scalar(randomScalarValue)!)
}
}
return String(String.UnicodeScalarView(output))
}
NSPasteboard.general.clearContents()
let text = CommandLine.arguments[1]
let intensityString = CommandLine.arguments[2]
let intensity = Int(intensityString) ?? 5
let zalgoText = zalgo(text, intensity: intensity)
NSPasteboard.general.setString(zalgoText, forType: .string)
print("\(zalgoText) copied to clipboard")

View File

@@ -1,38 +1,66 @@
#!/usr/bin/env bash
#
# Get latest release version, branch tag, or latest commit from GitHub
# Usage: x-gh-get-latest-version <repo>
# Usage: x-gh-get-latest-version <repo> [options]
# Author: Ismo Vuorinen <https://github.com/ivuorinen> 2024
set -euo pipefail
# Environment variables, more under get_release_version() and get_latest_branch_tag()
# functions. These can be overridden by the user.
# Environment variables, can be overridden by command line arguments
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com/repos}"
VERBOSE="${VERBOSE:-0}"
INCLUDE_PRERELEASES="${INCLUDE_PRERELEASES:-0}"
OLDEST_RELEASE="${OLDEST_RELEASE:-0}"
BRANCH=""
LATEST_COMMIT="${LATEST_COMMIT:-0}"
LATEST_TAG="${LATEST_TAG:-0}"
OUTPUT="${OUTPUT:-text}"
SHOW_HELP=0
REPOSITORY=""
COMBINED=0
BIN=$(basename "$0")
# Prints a message if VERBOSE=1
msg()
{
[[ "$VERBOSE" -eq 1 ]] && echo "$1"
if [[ $VERBOSE -eq 1 ]]; then
echo "$1" >&2
fi
}
# Show usage information
usage()
{
cat << EOF
Usage: $0 <repo> (e.g. ivuorinen/dotfiles)
Usage: $BIN <repo> [options]
Fetches the latest release version, latest branch tag, or latest commit SHA from GitHub.
Arguments:
<repo> Repository in format 'owner/repo' (e.g. ivuorinen/dotfiles)
Options:
- INCLUDE_PRERELEASES=1 Include prerelease versions (default: only stable releases).
- OLDEST_RELEASE=1 Fetch the oldest release instead of the latest.
- BRANCH=<branch> Fetch the latest tag from a specific branch (default: main).
- LATEST_COMMIT=1 Fetch the latest commit SHA from the specified branch.
- OUTPUT=json Return output as JSON (default: plain text).
- GITHUB_API_URL=<url> Override GitHub API URL (useful for GitHub Enterprise).
- GITHUB_TOKEN=<token> Use GitHub API token to increase rate limits (default: unauthenticated).
-h, --help Show this help message and exit
-v, --verbose Enable verbose output
-p, --prereleases Include prerelease versions (default: only stable releases)
-o, --oldest Fetch the oldest release instead of the latest
-b, --branch <branch> Fetch the latest tag from a specific branch (default: main)
-c, --commit Fetch the latest commit SHA from the specified branch
-t, --tag Fetch the latest Git tag (any branch)
-j, --json Return output as JSON (default: plain text)
-a, --all Fetch all information types in a combined output
Environment Variables (can be used instead of command line options):
- INCLUDE_PRERELEASES=1 Same as --prereleases
- OLDEST_RELEASE=1 Same as --oldest
- BRANCH=<branch> Same as --branch <branch>
- LATEST_COMMIT=1 Same as --commit
- LATEST_TAG=1 Same as --tag
- OUTPUT=json Same as --json
- GITHUB_API_URL=<url> Override GitHub API URL (useful for GitHub Enterprise)
- GITHUB_TOKEN=<token> Use GitHub API token to increase rate limits (default: unauthenticated)
- VERBOSE=1 Same as --verbose
Requirements:
- curl
@@ -40,28 +68,34 @@ Requirements:
Examples:
# Fetch the latest stable release
$0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles
# Fetch the latest release including prereleases
INCLUDE_PRERELEASES=1 $0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles --prereleases
# Fetch the oldest release
OLDEST_RELEASE=1 $0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles --oldest
# Fetch the latest tag from the 'develop' branch
BRANCH=develop $0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles --branch develop
# Fetch the latest commit SHA from 'main' branch
LATEST_COMMIT=1 $0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles --commit
# Fetch the latest Git tag (any branch)
$BIN ivuorinen/dotfiles --tag
# Fetch all information types in a combined output
$BIN ivuorinen/dotfiles --all
# Output result in JSON format
OUTPUT=json $0 ivuorinen/dotfiles
$BIN ivuorinen/dotfiles --json
# Use GitHub API token for higher rate limits
GITHUB_TOKEN="your_personal_access_token" $0 ivuorinen/dotfiles
GITHUB_TOKEN="your_personal_access_token" $BIN ivuorinen/dotfiles
# Use GitHub Enterprise API
GITHUB_API_URL="https://github.example.com/api/v3/repos" $0 ivuorinen/dotfiles
GITHUB_API_URL="https://github.example.com/api/v3/repos" $BIN ivuorinen/dotfiles
EOF
exit 1
}
@@ -77,6 +111,140 @@ check_dependencies()
done
}
# Check GitHub API rate limits and warn if they're getting low
check_rate_limits()
{
local auth_status="unauthenticated"
local auth_header=()
if [[ -n ${GITHUB_TOKEN:-} ]]; then
auth_status="authenticated"
auth_header=(-H "Authorization: token $GITHUB_TOKEN")
fi
msg "Making $auth_status GitHub API requests"
local rate_limit_info
rate_limit_info=$(curl -sSL "${auth_header[@]}" "https://api.github.com/rate_limit")
local remaining
local reset_timestamp
local reset_time
remaining=$(echo "$rate_limit_info" | jq -r '.resources.core.remaining')
reset_timestamp=$(echo "$rate_limit_info" | jq -r '.resources.core.reset')
# Handle date command differences between Linux and macOS
if date --version > /dev/null 2>&1; then
# GNU date (Linux)
reset_time=$(date -d "@$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null)
else
# BSD date (macOS)
reset_time=$(date -r "$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null)
fi
msg "Rate limit status: $remaining requests remaining, reset at $reset_time"
if [[ $remaining -le 5 ]]; then
echo "Warning: GitHub API rate limit nearly reached ($remaining requests left)" >&2
echo "Rate limits will reset at: $reset_time" >&2
if [[ $auth_status == "unauthenticated" ]]; then
echo "Tip: Set GITHUB_TOKEN to increase your rate limits (60 → 5000 requests/hour)" >&2
fi
fi
}
# Make a GitHub API request with proper error handling
api_request()
{
local url="$1"
local auth_header=()
if [[ -n ${GITHUB_TOKEN:-} ]]; then
auth_header=(-H "Authorization: token $GITHUB_TOKEN")
fi
local response
local status_code
# Use a temporary file to capture both headers and body
local tmp_file
tmp_file=$(mktemp)
msg "Making API request to: $url"
status_code=$(curl -sSL -w "%{http_code}" -o "$tmp_file" "${auth_header[@]}" "$url")
response=$(< "$tmp_file")
rm -f "$tmp_file"
# Check for HTTP errors
if [[ $status_code -ge 400 ]]; then
local error_msg
error_msg=$(echo "$response" | jq -r '.message // "Unknown error"')
if [[ $status_code -eq 403 && $error_msg == *"API rate limit exceeded"* ]]; then
# Extract rate limit reset info
local reset_timestamp
reset_timestamp=$(echo "$response" | jq -r '.rate.reset // empty')
local reset_time
if date --version > /dev/null 2>&1; then
# GNU date (Linux)
reset_time=$(date -d "@$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null \
|| echo "unknown time")
else
# BSD date (macOS)
reset_time=$(date -r "$reset_timestamp" "+%H:%M:%S %Z" 2> /dev/null \
|| echo "unknown time")
fi
echo "Error: GitHub API rate limit exceeded" >&2
echo "Rate limit will reset at: $reset_time" >&2
if [[ -z ${GITHUB_TOKEN:-} ]]; then
echo "Tip: Set GITHUB_TOKEN to increase your rate limits (60 → 5000 requests/hour)" >&2
else
echo "You've exceeded even authenticated rate limits (5000 requests/hour)" >&2
fi
exit 3
elif [[ $status_code -eq 404 ]]; then
echo "Error: Repository not found or no access permission: $url" >&2
exit 2
else
echo "GitHub API error ($status_code): $error_msg" >&2
exit 1
fi
fi
echo "$response"
}
# Check if repository exists before proceeding
check_repository()
{
local repo="$1"
local api_url="${GITHUB_API_URL}/${repo}"
msg "Checking if repository exists: $api_url"
local response
response=$(api_request "$api_url")
# If we got here, the repository exists (otherwise api_request would have exited)
msg "Repository found: $(echo "$response" | jq -r '.full_name')"
# Get default branch if no branch is specified
if [[ -z ${BRANCH} ]]; then
BRANCH=$(echo "$response" | jq -r '.default_branch')
msg "Using default branch: $BRANCH"
fi
# Return the repository full name (in case it differs from input due to redirects)
echo "$response" | jq -r '.full_name'
}
# Fetches the latest release or the oldest if OLDEST_RELEASE=1
# $1 - GitHub repository (string)
get_release_version()
@@ -86,38 +254,55 @@ get_release_version()
local oldest_release="${OLDEST_RELEASE:-0}"
local api_url="${GITHUB_API_URL}/${repo}/releases"
local auth_header=()
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
auth_header=(-H "Authorization: token $GITHUB_TOKEN")
fi
msg "Fetching release data from: $api_url (Include prereleases: $include_prereleases, Oldest: $oldest_release)"
msg "Fetching release data from: $api_url " + \
"(Include prereleases: $include_prereleases, Oldest: $oldest_release)"
local json_response
json_response=$(curl -sSL "${auth_header[@]}" "$api_url")
json_response=$(api_request "$api_url")
# Check for API errors
if echo "$json_response" | jq -e 'has("message")' > /dev/null; then
msg "GitHub API error: $(echo "$json_response" | jq -r '.message')"
exit 1
fi
local version=""
local prerelease_version=""
local filter='.[] | select(.tag_name)'
[[ "$include_prereleases" -eq 0 ]] && filter+='.prerelease == false'
local version
if [[ "$oldest_release" -eq 1 ]]; then
version=$(echo "$json_response" | jq -r "[${filter}] | last.tag_name // empty")
# Get stable release version
if [[ $oldest_release -eq 1 ]]; then
version=$(echo "$json_response" \
| jq -r '[.[] | select(.tag_name != null and .prerelease == false)] | sort_by(.created_at) | first.tag_name // empty')
else
version=$(echo "$json_response" | jq -r "[${filter}] | first.tag_name // empty")
version=$(echo "$json_response" \
| jq -r '[.[] | select(.tag_name != null and .prerelease == false)] | sort_by(.created_at) | reverse | first.tag_name // empty')
fi
if [[ -z "$version" ]]; then
msg "Failed to fetch release version for repository: $repo"
# Get prerelease version if requested
if [[ $include_prereleases -eq 1 ]]; then
if [[ $oldest_release -eq 1 ]]; then
prerelease_version=$(echo "$json_response" \
| jq -r '[.[] | select(.tag_name != null and .prerelease == true)] | sort_by(.created_at) | first.tag_name // empty')
else
prerelease_version=$(echo "$json_response" \
| jq -r '[.[] | select(.tag_name != null and .prerelease == true)] | sort_by(.created_at) | reverse | first.tag_name // empty')
fi
fi
# Error if no releases found and we're not in combined mode
if [[ -z $version && -z $prerelease_version && $COMBINED -eq 0 ]]; then
echo "No releases found for repository: $repo" >&2
exit 1
fi
echo "$version"
# Return both values for combined output
if [[ $COMBINED -eq 1 ]]; then
echo "$version"
echo "$prerelease_version"
else
# Return prerelease if specifically requested, otherwise stable
if [[ $include_prereleases -eq 1 && -n $prerelease_version ]]; then
msg "Found prerelease version: $prerelease_version"
echo "$prerelease_version"
else
msg "Found stable release version: $version"
echo "$version"
fi
fi
}
# Fetches the latest tag from the specified branch
@@ -130,16 +315,42 @@ get_latest_branch_tag()
msg "Fetching latest tag for branch '$branch' from: $api_url"
local json_response
json_response=$(curl -sSL "$api_url")
json_response=$(api_request "$api_url")
local version
version=$(echo "$json_response" | jq -r "[.[] | select(.ref | contains(\"refs/tags/$branch\"))] | last.ref | sub(\"refs/tags/\"; \"\") // empty")
version=$(echo "$json_response" \
| jq -r "[.[] | select(.ref | contains(\"refs/tags/$branch\"))] | sort_by(.ref) | reverse | first.ref | sub(\"refs/tags/\"; \"\") // empty")
if [[ -z "$version" ]]; then
msg "Failed to fetch latest tag for branch: $branch"
if [[ -z $version && $COMBINED -eq 0 ]]; then
echo "No tags found for branch: $branch in repository: $repo" >&2
exit 1
fi
msg "Found branch tag: $version"
echo "$version"
}
# Fetches the latest Git tag (regardless of branch)
get_latest_git_tag()
{
local repo="$1"
local api_url="${GITHUB_API_URL}/${repo}/git/refs/tags"
msg "Fetching latest Git tag from: $api_url"
local json_response
json_response=$(api_request "$api_url")
local version
version=$(echo "$json_response" \
| jq -r '[.[] | select(.ref | startswith("refs/tags/"))] | sort_by(.ref) | reverse | first.ref | sub("refs/tags/"; "") // empty')
if [[ -z $version && $COMBINED -eq 0 ]]; then
echo "No Git tags found in repository: $repo" >&2
exit 1
fi
msg "Found Git tag: $version"
echo "$version"
}
@@ -153,42 +364,240 @@ get_latest_commit()
msg "Fetching latest commit SHA from: $api_url"
local json_response
json_response=$(curl -sSL "$api_url")
json_response=$(api_request "$api_url")
local sha
sha=$(echo "$json_response" | jq -r '.sha // empty')
if [[ -z "$sha" ]]; then
msg "Failed to fetch latest commit SHA for branch: $branch"
if [[ -z $sha && $COMBINED -eq 0 ]]; then
echo "Failed to fetch latest commit SHA for branch: $branch in repository: $repo" >&2
exit 1
fi
msg "Found commit SHA: $sha"
echo "$sha"
}
# Format combined text output
format_combined_text()
{
local repo="$1"
local branch="$2"
local tag="$3"
local commit="$4"
local release="$5"
local prerelease="$6"
echo "Repository: $repo"
if [[ -n $branch ]]; then
echo "Branch: $branch"
fi
if [[ -n $tag ]]; then
echo "Git Tag: $tag"
fi
if [[ -n $commit ]]; then
echo "Commit: $commit"
fi
if [[ -n $prerelease ]]; then
echo "Prerelease: $prerelease"
fi
if [[ -n $release ]]; then
echo "Release: $release"
fi
}
# Format combined JSON output
format_combined_json()
{
local repo="$1"
local branch="$2"
local tag="$3"
local commit="$4"
local release="$5"
local prerelease="$6"
local json="{"
json+="\"repository\":\"$repo\""
if [[ -n $branch ]]; then
json+=",\"branch\":\"$branch\""
fi
if [[ -n $tag ]]; then
json+=",\"tag\":\"$tag\""
fi
if [[ -n $commit ]]; then
json+=",\"commit\":\"$commit\""
fi
if [[ -n $prerelease ]]; then
json+=",\"prerelease\":\"$prerelease\""
fi
if [[ -n $release ]]; then
json+=",\"release\":\"$release\""
fi
json+="}"
echo "$json"
}
# Parse command line arguments
parse_arguments()
{
# If no arguments provided, show usage
if [[ $# -eq 0 ]]; then
usage
fi
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
SHOW_HELP=1
shift
;;
-v | --verbose)
VERBOSE=1
shift
;;
-p | --prereleases)
INCLUDE_PRERELEASES=1
shift
;;
-o | --oldest)
OLDEST_RELEASE=1
shift
;;
-b | --branch)
if [[ $# -lt 2 ]]; then
echo "Error: --branch option requires a branch name" >&2
exit 1
fi
BRANCH="$2"
shift 2
;;
-c | --commit)
LATEST_COMMIT=1
shift
;;
-t | --tag)
LATEST_TAG=1
shift
;;
-j | --json)
OUTPUT="json"
shift
;;
-a | --all)
COMBINED=1
shift
;;
-*)
echo "Error: Unknown option: $1" >&2
usage
;;
*)
# If repository is already set, this is an error
if [[ -n $REPOSITORY ]]; then
echo "Error: Unexpected argument: $1" >&2
usage
fi
REPOSITORY="$1"
shift
;;
esac
done
# Validate that we have a repository
if [[ -z $REPOSITORY && $SHOW_HELP -eq 0 ]]; then
echo "Error: Repository argument is required" >&2
usage
fi
}
# Main function
# $1 - GitHub repository (string)
main()
{
if [[ $# -ne 1 ]]; then
# Parse command line arguments
parse_arguments "$@"
# Show help if requested
if [[ $SHOW_HELP -eq 1 ]]; then
usage
fi
check_dependencies
local repo="$1"
local result
# Check rate limits before making other API calls
check_rate_limits
if [[ "${LATEST_COMMIT:-0}" -eq 1 ]]; then
result=$(get_latest_commit "$repo")
elif [[ -n "${BRANCH:-}" ]]; then
result=$(get_latest_branch_tag "$repo")
else
result=$(get_release_version "$repo")
# Validate repository existence and get normalized repository name
local repo_fullname
repo_fullname=$(check_repository "$REPOSITORY")
# If --all specified, get all information types
if [[ $COMBINED -eq 1 ]]; then
local branch="${BRANCH:-main}"
local git_tag=""
local commit_sha=""
local release_version=""
local prerelease_version=""
# Get Git tag if requested
git_tag=$(get_latest_git_tag "$repo_fullname")
# Get commit SHA
commit_sha=$(get_latest_commit "$repo_fullname")
# Get release versions (stable and prerelease)
read -r release_version prerelease_version < <(get_release_version "$repo_fullname")
# Format output based on selected format
if [[ $OUTPUT == "json" ]]; then
format_combined_json \
"$repo_fullname" \
"$branch" \
"$git_tag" \
"$commit_sha" \
"$release_version" \
"$prerelease_version"
else
format_combined_text \
"$repo_fullname" \
"$branch" \
"$git_tag" \
"$commit_sha" \
"$release_version" \
"$prerelease_version"
fi
exit 0
fi
if [[ "${OUTPUT:-text}" == "json" ]]; then
echo "{\"repository\": \"$repo\", \"result\": \"$result\"}"
# Not combined mode - get only the requested information type
local result=""
if [[ $LATEST_COMMIT -eq 1 ]]; then
result=$(get_latest_commit "$repo_fullname")
elif [[ $LATEST_TAG -eq 1 ]]; then
result=$(get_latest_git_tag "$repo_fullname")
elif [[ -n $BRANCH ]]; then
result=$(get_latest_branch_tag "$repo_fullname")
else
result=$(get_release_version "$repo_fullname")
fi
# Output the result in the requested format
if [[ $OUTPUT == "json" ]]; then
echo "{\"repository\": \"$repo_fullname\", \"result\": \"$result\"}"
else
echo "$result"
fi

View File

@@ -0,0 +1,196 @@
# GitHub Latest Version Fetcher
`x-gh-get-latest-version` is a versatile command-line tool for fetching the
latest version information from GitHub repositories. It can retrieve release
versions, Git tags, branch tags, and commit SHAs with simple commands.
## Features
- Fetch latest or oldest stable releases
- Include prerelease versions
- Get latest Git tags from any branch
- Fetch latest commit SHA from a specific branch
- Output in plain text or JSON format
- Combined output mode to get all information at once
- Rate limit checking to avoid GitHub API throttling
- Authenticated requests with GitHub token support
## Requirements
- `curl` for making HTTP requests
- `jq` for processing JSON responses
- A GitHub personal access token
(optional, but recommended to avoid rate limiting)
## Installation
1. Save the script to a location in your PATH
2. Make it executable: `chmod +x x-gh-get-latest-version`
3. Optionally set up a GitHub token as an environment variable:
```bash
export GITHUB_TOKEN="your_personal_access_token"
```
## Usage
```text
Usage: x-gh-get-latest-version <repo> [options]
Arguments:
<repo> Repository in format 'owner/repo' (e.g. ivuorinen/dotfiles)
Options:
-h, --help Show this help message and exit
-v, --verbose Enable verbose output
-p, --prereleases Include prerelease versions (default: only stable releases)
-o, --oldest Fetch the oldest release instead of the latest
-b, --branch <branch> Fetch the latest tag from a specific branch (default: main)
-c, --commit Fetch the latest commit SHA from the specified branch
-t, --tag Fetch the latest Git tag (any branch)
-j, --json Return output as JSON (default: plain text)
-a, --all Fetch all information types in a combined output
```
## Examples
### Fetch the Latest Release Version
```bash
x-gh-get-latest-version ivuorinen/dotfiles
```
Output: `v1.2.3`
### Include Prereleases
```bash
x-gh-get-latest-version ivuorinen/dotfiles --prereleases
```
Output: `v1.3.0-rc.1`
### Get the Oldest Release
```bash
x-gh-get-latest-version ivuorinen/dotfiles --oldest
```
Output: `v0.1.0`
### Fetch from a Specific Branch
```bash
x-gh-get-latest-version ivuorinen/dotfiles --branch develop
```
Output: `develop-v1.3.0`
### Get Latest Commit SHA
```bash
x-gh-get-latest-version ivuorinen/dotfiles --commit
```
Output: `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0`
### Fetch Latest Git Tag
```bash
x-gh-get-latest-version ivuorinen/dotfiles --tag
```
Output: `v2.0.0-beta.1`
### Output as JSON
```bash
x-gh-get-latest-version ivuorinen/dotfiles --json
```
Output: `{"repository": "ivuorinen/dotfiles", "result": "v1.2.3"}`
### Combined Information Output
```bash
x-gh-get-latest-version ivuorinen/dotfiles --all
```
Output:
```text
Repository: ivuorinen/dotfiles
Branch: main
Git Tag: v2.0.0-beta.1
Commit: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Prerelease: v1.3.0-rc.1
Release: v1.2.3
```
### Combined Output as JSON
```bash
x-gh-get-latest-version ivuorinen/dotfiles --all --json
```
Output:
```json
{
"repository": "ivuorinen/dotfiles",
"branch": "main",
"tag": "v2.0.0-beta.1",
"commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"prerelease": "v1.3.0-rc.1",
"release": "v1.2.3"
}
```
## Environment Variables
You can use environment variables instead of command-line options:
- `INCLUDE_PRERELEASES=1` - Include prerelease versions
- `OLDEST_RELEASE=1` - Fetch the oldest release instead of the latest
- `BRANCH=branch_name` - Specify a branch to fetch tags from
- `LATEST_COMMIT=1` - Fetch latest commit SHA
- `LATEST_TAG=1` - Fetch latest Git tag
- `OUTPUT=json` - Output results as JSON
- `GITHUB_API_URL=url` - Override GitHub API URL (useful for GitHub Enterprise)
- `GITHUB_TOKEN=token` - Use GitHub API token to increase rate limits
- `VERBOSE=1` - Enable verbose output
## GitHub API Rate Limits
GitHub enforces rate limits on API requests:
- Unauthenticated requests: 60 requests per hour
- Authenticated requests: 5,000 requests per hour
For frequent use, it's strongly recommended to set up a GitHub token:
```bash
export GITHUB_TOKEN="your_personal_access_token"
```
The script will automatically warn you when you're approaching your rate limit
and suggest using a token if you haven't already.
## Error Handling
The script provides informative error messages for common issues:
- Repository not found
- Rate limit exceeded
- No releases/tags found
- Invalid arguments
## Author
Ismo Vuorinen (<https://github.com/ivuorinen>)
## License
MIT
<!-- vim: set ft=markdown spell spelllang=en_us cc=80 : -->

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,8 @@ msgr run "Installing go packages"
! x-have "go" && msgr err "go hasn't been installed yet." && exit 0
[[ -z "$ASDF_GOLANG_DEFAULT_PACKAGES_FILE" ]] && \
ASDF_GOLANG_DEFAULT_PACKAGES_FILE="$DOTFILES/config/asdf/golang-packages"
[[ -z "$ASDF_GOLANG_DEFAULT_PACKAGES_FILE" ]] \
&& ASDF_GOLANG_DEFAULT_PACKAGES_FILE="$DOTFILES/config/asdf/golang-packages"
# Packages are defined in $DOTFILES/config/asdf/golang-packages, one per line
# Skip comments and empty lines