mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-02-05 12:49:22 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f28ad41f67 | |||
| 61b66d3114 | |||
| 282f760a4f | |||
| 4a9c9b4cb9 | |||
| 16311ee5b4 | |||
| 2fddfa82c0 | |||
| 8f5f44db2d |
1080
local/bin/git-dirty
1080
local/bin/git-dirty
File diff suppressed because it is too large
Load Diff
185
local/bin/git-dirty.md
Normal file
185
local/bin/git-dirty.md
Normal 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 : -->
|
||||||
@@ -10,42 +10,116 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Enable verbosity with VERBOSE=1
|
# Enable verbosity with VERBOSE=1
|
||||||
VERBOSE="${VERBOSE:-0}"
|
VERBOSE="${VERBOSE:-0}"
|
||||||
|
DEBUG="${DEBUG:-0}"
|
||||||
|
|
||||||
|
if [ "$DEBUG" -eq 1 ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
# Function to print messages if VERBOSE is enabled
|
# Function to print messages if VERBOSE is enabled
|
||||||
# $1 - message (string)
|
# $1 - message (string)
|
||||||
msg()
|
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
|
# Function to perform git fsck on a repository
|
||||||
# $1 - directory (string)
|
# $1 - directory (string)
|
||||||
fsck_repo()
|
fsck_repo()
|
||||||
{
|
{
|
||||||
local dir=$1
|
local dir dirs collected_errors collected_repos
|
||||||
msg "Processing dir: $dir"
|
dir="$(realpath "$1")"
|
||||||
(
|
collected_errors="$2"
|
||||||
cd "$dir" || exit 1
|
collected_repos="$3"
|
||||||
if [ -d ".git" ]; then
|
|
||||||
git fsck --no-dangling --full --no-progress
|
msg "Processing: $dir"
|
||||||
echo ""
|
|
||||||
fi
|
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 function
|
||||||
main()
|
main()
|
||||||
{
|
{
|
||||||
local starting_path=${1:-$(pwd)}
|
local starting_path errors_file repo_count_file dirs dirs_count REPO_COUNT ERROR_COUNT
|
||||||
local dirs
|
|
||||||
|
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)
|
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
|
for dir in $dirs; do
|
||||||
fsck_repo "$dir"
|
fsck_repo "$dir" "$errors_file" "$repo_count_file"
|
||||||
done
|
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 ""
|
||||||
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 "$@"
|
main "$@"
|
||||||
|
exit $?
|
||||||
|
|||||||
65
local/bin/git-fsck-dirs.md
Normal file
65
local/bin/git-fsck-dirs.md
Normal 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 : -->
|
||||||
@@ -8,39 +8,688 @@
|
|||||||
# Copyright (c) 2023 Ismo Vuorinen. All Rights Reserved.
|
# Copyright (c) 2023 Ismo Vuorinen. All Rights Reserved.
|
||||||
# License: MIT <https://opensource.org/license/mit/>
|
# License: MIT <https://opensource.org/license/mit/>
|
||||||
|
|
||||||
set -euo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# Enable verbosity with VERBOSE=1
|
# Script version
|
||||||
VERBOSE="${VERBOSE:-0}"
|
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
|
# Function to print messages if VERBOSE is enabled
|
||||||
# $1 - message (string)
|
# $1 - message (string)
|
||||||
msg()
|
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
|
# Function to update a git repository
|
||||||
# $1 - directory (string)
|
# $1 - directory (string)
|
||||||
update_repo()
|
update_repo()
|
||||||
{
|
{
|
||||||
local dir=$1
|
local dir output exit_status git_args current_branch \
|
||||||
(
|
remote_name cleaned_branches
|
||||||
cd "$dir" || exit
|
|
||||||
msg "Updating $dir"
|
dir="$1"
|
||||||
git pull --rebase --autostash --prune
|
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 function to update all subfolder git repositories
|
||||||
main()
|
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
|
for dir in */; do
|
||||||
|
# Skip if excluded
|
||||||
|
if excluded_path "$dir"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
update_repo "$dir"
|
update_repo "$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Done."
|
# Return to original directory
|
||||||
echo ""
|
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 "$@"
|
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 : -->
|
||||||
@@ -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")
|
|
||||||
@@ -1,38 +1,66 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Get latest release version, branch tag, or latest commit from GitHub
|
# 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
|
# Author: Ismo Vuorinen <https://github.com/ivuorinen> 2024
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Environment variables, more under get_release_version() and get_latest_branch_tag()
|
# Environment variables, can be overridden by command line arguments
|
||||||
# functions. These can be overridden by the user.
|
|
||||||
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com/repos}"
|
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com/repos}"
|
||||||
VERBOSE="${VERBOSE:-0}"
|
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
|
# Prints a message if VERBOSE=1
|
||||||
msg()
|
msg()
|
||||||
{
|
{
|
||||||
[[ "$VERBOSE" -eq 1 ]] && echo "$1"
|
if [[ $VERBOSE -eq 1 ]]; then
|
||||||
|
echo "$1" >&2
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show usage information
|
# Show usage information
|
||||||
usage()
|
usage()
|
||||||
{
|
{
|
||||||
cat << EOF
|
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.
|
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:
|
Options:
|
||||||
- INCLUDE_PRERELEASES=1 Include prerelease versions (default: only stable releases).
|
-h, --help Show this help message and exit
|
||||||
- OLDEST_RELEASE=1 Fetch the oldest release instead of the latest.
|
-v, --verbose Enable verbose output
|
||||||
- BRANCH=<branch> Fetch the latest tag from a specific branch (default: main).
|
-p, --prereleases Include prerelease versions (default: only stable releases)
|
||||||
- LATEST_COMMIT=1 Fetch the latest commit SHA from the specified branch.
|
-o, --oldest Fetch the oldest release instead of the latest
|
||||||
- OUTPUT=json Return output as JSON (default: plain text).
|
-b, --branch <branch> Fetch the latest tag from a specific branch (default: main)
|
||||||
- GITHUB_API_URL=<url> Override GitHub API URL (useful for GitHub Enterprise).
|
-c, --commit Fetch the latest commit SHA from the specified branch
|
||||||
- GITHUB_TOKEN=<token> Use GitHub API token to increase rate limits (default: unauthenticated).
|
-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:
|
Requirements:
|
||||||
- curl
|
- curl
|
||||||
@@ -40,28 +68,34 @@ Requirements:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Fetch the latest stable release
|
# Fetch the latest stable release
|
||||||
$0 ivuorinen/dotfiles
|
$BIN ivuorinen/dotfiles
|
||||||
|
|
||||||
# Fetch the latest release including prereleases
|
# Fetch the latest release including prereleases
|
||||||
INCLUDE_PRERELEASES=1 $0 ivuorinen/dotfiles
|
$BIN ivuorinen/dotfiles --prereleases
|
||||||
|
|
||||||
# Fetch the oldest release
|
# Fetch the oldest release
|
||||||
OLDEST_RELEASE=1 $0 ivuorinen/dotfiles
|
$BIN ivuorinen/dotfiles --oldest
|
||||||
|
|
||||||
# Fetch the latest tag from the 'develop' branch
|
# 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
|
# 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 result in JSON format
|
||||||
OUTPUT=json $0 ivuorinen/dotfiles
|
$BIN ivuorinen/dotfiles --json
|
||||||
|
|
||||||
# Use GitHub API token for higher rate limits
|
# 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
|
# 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
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -77,6 +111,140 @@ check_dependencies()
|
|||||||
done
|
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
|
# Fetches the latest release or the oldest if OLDEST_RELEASE=1
|
||||||
# $1 - GitHub repository (string)
|
# $1 - GitHub repository (string)
|
||||||
get_release_version()
|
get_release_version()
|
||||||
@@ -86,38 +254,55 @@ get_release_version()
|
|||||||
local oldest_release="${OLDEST_RELEASE:-0}"
|
local oldest_release="${OLDEST_RELEASE:-0}"
|
||||||
local api_url="${GITHUB_API_URL}/${repo}/releases"
|
local api_url="${GITHUB_API_URL}/${repo}/releases"
|
||||||
|
|
||||||
local auth_header=()
|
msg "Fetching release data from: $api_url " + \
|
||||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
"(Include prereleases: $include_prereleases, Oldest: $oldest_release)"
|
||||||
auth_header=(-H "Authorization: token $GITHUB_TOKEN")
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg "Fetching release data from: $api_url (Include prereleases: $include_prereleases, Oldest: $oldest_release)"
|
|
||||||
|
|
||||||
local json_response
|
local json_response
|
||||||
json_response=$(curl -sSL "${auth_header[@]}" "$api_url")
|
json_response=$(api_request "$api_url")
|
||||||
|
|
||||||
# Check for API errors
|
local version=""
|
||||||
if echo "$json_response" | jq -e 'has("message")' > /dev/null; then
|
local prerelease_version=""
|
||||||
msg "GitHub API error: $(echo "$json_response" | jq -r '.message')"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local filter='.[] | select(.tag_name)'
|
# Get stable release version
|
||||||
[[ "$include_prereleases" -eq 0 ]] && filter+='.prerelease == false'
|
if [[ $oldest_release -eq 1 ]]; then
|
||||||
|
version=$(echo "$json_response" \
|
||||||
local version
|
| jq -r '[.[] | select(.tag_name != null and .prerelease == false)] | sort_by(.created_at) | first.tag_name // empty')
|
||||||
if [[ "$oldest_release" -eq 1 ]]; then
|
|
||||||
version=$(echo "$json_response" | jq -r "[${filter}] | last.tag_name // empty")
|
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
if [[ -z "$version" ]]; then
|
# Get prerelease version if requested
|
||||||
msg "Failed to fetch release version for repository: $repo"
|
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
|
exit 1
|
||||||
fi
|
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
|
# 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"
|
msg "Fetching latest tag for branch '$branch' from: $api_url"
|
||||||
|
|
||||||
local json_response
|
local json_response
|
||||||
json_response=$(curl -sSL "$api_url")
|
json_response=$(api_request "$api_url")
|
||||||
|
|
||||||
local version
|
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
|
if [[ -z $version && $COMBINED -eq 0 ]]; then
|
||||||
msg "Failed to fetch latest tag for branch: $branch"
|
echo "No tags found for branch: $branch in repository: $repo" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
echo "$version"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,42 +364,240 @@ get_latest_commit()
|
|||||||
msg "Fetching latest commit SHA from: $api_url"
|
msg "Fetching latest commit SHA from: $api_url"
|
||||||
|
|
||||||
local json_response
|
local json_response
|
||||||
json_response=$(curl -sSL "$api_url")
|
json_response=$(api_request "$api_url")
|
||||||
|
|
||||||
local sha
|
local sha
|
||||||
sha=$(echo "$json_response" | jq -r '.sha // empty')
|
sha=$(echo "$json_response" | jq -r '.sha // empty')
|
||||||
|
|
||||||
if [[ -z "$sha" ]]; then
|
if [[ -z $sha && $COMBINED -eq 0 ]]; then
|
||||||
msg "Failed to fetch latest commit SHA for branch: $branch"
|
echo "Failed to fetch latest commit SHA for branch: $branch in repository: $repo" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
msg "Found commit SHA: $sha"
|
||||||
echo "$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
|
# Main function
|
||||||
# $1 - GitHub repository (string)
|
|
||||||
main()
|
main()
|
||||||
{
|
{
|
||||||
if [[ $# -ne 1 ]]; then
|
# Parse command line arguments
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
# Show help if requested
|
||||||
|
if [[ $SHOW_HELP -eq 1 ]]; then
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
check_dependencies
|
check_dependencies
|
||||||
|
|
||||||
local repo="$1"
|
# Check rate limits before making other API calls
|
||||||
local result
|
check_rate_limits
|
||||||
|
|
||||||
if [[ "${LATEST_COMMIT:-0}" -eq 1 ]]; then
|
# Validate repository existence and get normalized repository name
|
||||||
result=$(get_latest_commit "$repo")
|
local repo_fullname
|
||||||
elif [[ -n "${BRANCH:-}" ]]; then
|
repo_fullname=$(check_repository "$REPOSITORY")
|
||||||
result=$(get_latest_branch_tag "$repo")
|
|
||||||
else
|
# If --all specified, get all information types
|
||||||
result=$(get_release_version "$repo")
|
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
|
fi
|
||||||
|
|
||||||
if [[ "${OUTPUT:-text}" == "json" ]]; then
|
# Not combined mode - get only the requested information type
|
||||||
echo "{\"repository\": \"$repo\", \"result\": \"$result\"}"
|
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
|
else
|
||||||
echo "$result"
|
echo "$result"
|
||||||
fi
|
fi
|
||||||
|
|||||||
196
local/bin/x-gh-get-latest-version.md
Normal file
196
local/bin/x-gh-get-latest-version.md
Normal 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 : -->
|
||||||
BIN
local/bin/yabai
BIN
local/bin/yabai
Binary file not shown.
1313
local/man/yabai.1
1313
local/man/yabai.1
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@ msgr run "Installing go packages"
|
|||||||
|
|
||||||
! x-have "go" && msgr err "go hasn't been installed yet." && exit 0
|
! x-have "go" && msgr err "go hasn't been installed yet." && exit 0
|
||||||
|
|
||||||
[[ -z "$ASDF_GOLANG_DEFAULT_PACKAGES_FILE" ]] && \
|
[[ -z "$ASDF_GOLANG_DEFAULT_PACKAGES_FILE" ]] \
|
||||||
ASDF_GOLANG_DEFAULT_PACKAGES_FILE="$DOTFILES/config/asdf/golang-packages"
|
&& ASDF_GOLANG_DEFAULT_PACKAGES_FILE="$DOTFILES/config/asdf/golang-packages"
|
||||||
|
|
||||||
# Packages are defined in $DOTFILES/config/asdf/golang-packages, one per line
|
# Packages are defined in $DOTFILES/config/asdf/golang-packages, one per line
|
||||||
# Skip comments and empty lines
|
# Skip comments and empty lines
|
||||||
|
|||||||
Reference in New Issue
Block a user