mirror of
https://github.com/ivuorinen/dotfiles.git
synced 2026-01-27 22:45:27 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75147c7dd6 | ||
| f28ad41f67 | |||
| 61b66d3114 | |||
| 282f760a4f | |||
| 4a9c9b4cb9 | |||
| 16311ee5b4 | |||
| 2fddfa82c0 | |||
| 8f5f44db2d | |||
| 8ad1f5c4d0 | |||
| ac0aa1fbc0 | |||
| e8c6794ff6 | |||
| 4de9a649f0 | |||
|
|
e7f115680e | ||
| f3b4551d0c | |||
| 64725c57dc | |||
| b32ee414e3 | |||
|
|
6ea7807718 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -46,3 +46,5 @@ config/vim/.netrwhist
|
||||
config/vim/extra/*
|
||||
config/gh/hosts.yml
|
||||
dependency-check-report.html
|
||||
local/bin/yabai
|
||||
local/man/yabai.1
|
||||
|
||||
@@ -49,7 +49,7 @@ repos:
|
||||
- id: actionlint
|
||||
|
||||
- repo: https://github.com/renovatebot/pre-commit-hooks
|
||||
rev: 39.227.2
|
||||
rev: 39.248.1
|
||||
hooks:
|
||||
- id: renovate-config-validator
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.13.2
|
||||
3.13.3
|
||||
|
||||
@@ -9,18 +9,21 @@ test -e "$HOME/.config/fish/exports.fish" &&
|
||||
source "$HOME/.config/fish/exports.fish"
|
||||
|
||||
if status is-interactive
|
||||
# Commands to run in interactive sessions can go here
|
||||
# Commands to run in interactive shell
|
||||
|
||||
# Start tmux if not already running and not in SSH
|
||||
open-tmux # defined in functions/open-tmux.fish
|
||||
# version manager initializers
|
||||
type -q rbenv; and source (rbenv init -|psub)
|
||||
type -q pyenv; and source (pyenv init -|psub)
|
||||
type -q pyenv; and source (pyenv virtualenv-init -)
|
||||
type -q goenv; and source (goenv init -|psub)
|
||||
# type -q fnm; and fnm env --use-on-cd --shell fish | source
|
||||
type -q load_nvm; and load_nvm > /dev/stderr
|
||||
|
||||
# Start tmux if not already running and not in SSH
|
||||
open-tmux # defined in functions/open-tmux.fish
|
||||
end
|
||||
|
||||
# Added by LM Studio CLI (lms)
|
||||
set -gx PATH $PATH $HOME/.lmstudio/bin
|
||||
|
||||
type -q rbenv; and source (rbenv init -|psub)
|
||||
type -q pyenv; and source (pyenv init -|psub)
|
||||
type -q goenv; and source (goenv init -|psub)
|
||||
type -q fnm; and fnm env --use-on-cd --shell fish | source
|
||||
type -q load_nvm; and load_nvm > /dev/stderr
|
||||
|
||||
# vim: ft=fish ts=4 sw=4 et:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"" Source your .vimrc
|
||||
source ~/.dotfiles/config/vim/vimrc
|
||||
source $HOME/.dotfiles/config/vim/vimrc
|
||||
|
||||
" https://github.com/ville6000/dotfiles/blob/main/vimrc
|
||||
" To get a list of Actions run `:actionlist `
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# yabai -m signal --add event=dock_did_restart action="sudo yabai --load-sa"
|
||||
|
||||
yabai -m config \
|
||||
active_window_border_color 0xff775759 \
|
||||
auto_balance on \
|
||||
layout bsp \
|
||||
top_padding 0 \
|
||||
|
||||
@@ -5,20 +5,14 @@
|
||||
"assistant": {
|
||||
"default_model": {
|
||||
"provider": "copilot_chat",
|
||||
"model": "claude-3-5-sonnet"
|
||||
"model": "claude-3-7-sonnet"
|
||||
},
|
||||
"version": "2"
|
||||
},
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "prettier",
|
||||
"arguments": [
|
||||
"--stdin-filepath",
|
||||
"{buffer_path}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"languages": {
|
||||
"Shell Script": {
|
||||
"enable_language_server": true
|
||||
},
|
||||
"JavaScript": {
|
||||
"enable_language_server": true,
|
||||
"code_actions_on_format": {
|
||||
|
||||
@@ -6,8 +6,15 @@ Some problematic code has been fixed per `shellcheck` suggestions.
|
||||
## Homegrown
|
||||
|
||||
- dfm
|
||||
- git-dirty (based on git-extra-tools)
|
||||
- git-fsck-dirs
|
||||
- git-update-dirs
|
||||
- php-switcher
|
||||
- x-backup-folder
|
||||
- x-backup-mysql-with-prefix
|
||||
- x-check-git-attributes
|
||||
- x-clean-vendordirs
|
||||
- x-env-list
|
||||
- x-open-ports
|
||||
|
||||
## Sourced
|
||||
@@ -25,9 +32,15 @@ Some problematic code has been fixed per `shellcheck` suggestions.
|
||||
| `x-when-up` | skx/sysadmin-util |
|
||||
|
||||
- Sources:
|
||||
- [skx/sysadmin-utils](https://github.com/skx/sysadmin-util/)
|
||||
- [skx/sysadmin-utils][skx]
|
||||
- Tools for Linux/Unix sysadmins.
|
||||
- [Licence](https://github.com/skx/sysadmin-util/blob/master/LICENSE)
|
||||
- [onnimonni](https://github.com/onnimonni)
|
||||
- [validate_sha256sum](https://gist.github.com/onnimonni/b49779ebc96216771a6be3de46449fa1)
|
||||
- [mvdan/dotfiles](https://github.com/mvdan/dotfiles)
|
||||
- [Licence][skx-license]
|
||||
- [onnimonni][onnimonni]
|
||||
- [validate_sha256sum][onnimonni-gist]
|
||||
- [mvdan/dotfiles][mvdan]
|
||||
|
||||
[onnimonni]: https://github.com/onnimonni
|
||||
[onnimonni-gist]: https://gist.github.com/onnimonni/b49779ebc96216771a6be3de46449fa1
|
||||
[skx]: https://github.com/skx/sysadmin-util
|
||||
[skx-license]: https://github.com/skx/sysadmin-util/blob/master/LICENSE
|
||||
[mvdan]: https://github.com/mvdan/dotfiles
|
||||
|
||||
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
|
||||
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 $?
|
||||
|
||||
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.
|
||||
# License: MIT <https://opensource.org/license/mit/>
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
# Enable verbosity with VERBOSE=1
|
||||
VERBOSE="${VERBOSE:-0}"
|
||||
# Script version
|
||||
VERSION="1.0.0"
|
||||
|
||||
# Default settings
|
||||
VERBOSE=0
|
||||
QUIET=0
|
||||
EXCLUDE_DIRS=""
|
||||
CLEANUP=0
|
||||
CONFIG_FILE=""
|
||||
LOG_FILE=""
|
||||
|
||||
# Define color variables if terminal supports it
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
CYAN=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
# Counters
|
||||
TOTAL=0
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
CONFLICTS=0
|
||||
UPDATED=0
|
||||
PROCESSED=0
|
||||
SKIPPED=0
|
||||
UNTRACKED=0
|
||||
UNMERGED=0
|
||||
BRANCHES_CLEANED=0
|
||||
|
||||
# Function to display help message
|
||||
show_help()
|
||||
{
|
||||
BIN=$(basename "$0")
|
||||
cat << EOF
|
||||
Usage: $BIN [OPTIONS]
|
||||
|
||||
Updates all git repositories in subdirectories.
|
||||
|
||||
Options:
|
||||
--help, -h Display this help message and exit
|
||||
--version, -v Display version information and exit
|
||||
--verbose Display detailed output
|
||||
--quiet, -q Suppress all output except errors
|
||||
--exclude DIR Exclude directory from updates (can be used multiple times)
|
||||
--cleanup Remove local branches that have been merged into current branch
|
||||
--config FILE Read options from configuration file
|
||||
--log FILE Log details and errors to FILE
|
||||
|
||||
Environment variables:
|
||||
VERBOSE Set to 1 to enable verbose output
|
||||
EXCLUDE_DIRS Space-separated list of directories to exclude
|
||||
|
||||
Examples:
|
||||
$BIN Update all git repositories
|
||||
$BIN --verbose Update with detailed output
|
||||
$BIN --exclude node_modules --exclude vendor
|
||||
Update repositories but skip node_modules
|
||||
and vendor dirs
|
||||
$BIN --cleanup Update and clean up merged branches
|
||||
$BIN --config ~/.gitupdate.conf
|
||||
Use options from config file
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Function to display version
|
||||
show_version()
|
||||
{
|
||||
echo "$(basename "$0") version $VERSION"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Function to log messages
|
||||
# $1 - level (string: INFO, WARNING, ERROR)
|
||||
# $2 - message (string)
|
||||
log()
|
||||
{
|
||||
local level message timestamp
|
||||
|
||||
level="$1"
|
||||
message="$2"
|
||||
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if [[ -n "$LOG_FILE" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# For errors, also log to stderr if in verbose mode
|
||||
if [[ "$level" == "ERROR" && "$VERBOSE" -eq 1 && "$QUIET" -eq 0 ]]; then
|
||||
echo -e "${RED}[$timestamp] [$level] $message${NC}" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Process command-line arguments
|
||||
process_args()
|
||||
{
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help | -h)
|
||||
show_help
|
||||
;;
|
||||
--version | -v)
|
||||
show_version
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
;;
|
||||
--quiet | -q)
|
||||
QUIET=1
|
||||
;;
|
||||
--exclude)
|
||||
if [[ -n "$2" ]]; then
|
||||
EXCLUDE_DIRS="$EXCLUDE_DIRS $2"
|
||||
shift
|
||||
else
|
||||
echo "Error: --exclude requires a directory argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--cleanup)
|
||||
CLEANUP=1
|
||||
;;
|
||||
--config)
|
||||
if [[ -n "$2" && -f "$2" ]]; then
|
||||
CONFIG_FILE="$2"
|
||||
shift
|
||||
else
|
||||
echo "Error: --config requires a valid file argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
--log)
|
||||
if [[ -n "$2" ]]; then
|
||||
LOG_FILE="$2"
|
||||
shift
|
||||
else
|
||||
echo "Error: --log requires a file argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Use --help for usage information" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Process config file if specified
|
||||
if [[ -n "$CONFIG_FILE" && -f "$CONFIG_FILE" ]]; then
|
||||
log "INFO" "Reading configuration from $CONFIG_FILE"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${line// /}" ]] && continue
|
||||
|
||||
# Process each option from the config file
|
||||
option=$(echo "$line" | awk '{print $1}')
|
||||
value=$(echo "$line" | cut -d' ' -f2-)
|
||||
|
||||
case "$option" in
|
||||
exclude) EXCLUDE_DIRS="$EXCLUDE_DIRS $value" ;;
|
||||
verbose) VERBOSE=1 ;;
|
||||
quiet) QUIET=1 ;;
|
||||
cleanup) CLEANUP=1 ;;
|
||||
log) LOG_FILE="$value" ;;
|
||||
*) log "WARNING" "Unknown option in config file: $option" ;;
|
||||
esac
|
||||
done < "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Environment variables override command-line options
|
||||
[[ -n "${VERBOSE:-}" && "$VERBOSE" -eq 1 ]] && VERBOSE=1
|
||||
# shellcheck disable=SC2269
|
||||
[[ -n "${EXCLUDE_DIRS:-}" ]] && EXCLUDE_DIRS="${EXCLUDE_DIRS}"
|
||||
|
||||
# Initialize log file if specified
|
||||
if [[ -n "$LOG_FILE" ]]; then
|
||||
# Create log directory if it doesn't exist
|
||||
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||
# Initialize log file
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] [INFO] Started git-update-dirs version $VERSION" > "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Terminal width for progress bar
|
||||
TERM_WIDTH=$(tput cols 2> /dev/null || echo 120)
|
||||
PROGRESS_WIDTH=$((TERM_WIDTH - 40))
|
||||
MAX_DIR_LENGTH=$((TERM_WIDTH - PROGRESS_WIDTH - 25)) # Add 5 for extra padding
|
||||
|
||||
# Last status message, used for clearing properly
|
||||
LAST_STATUS_LENGTH=0
|
||||
|
||||
# Function to print messages if VERBOSE is enabled
|
||||
# $1 - message (string)
|
||||
msg()
|
||||
{
|
||||
[ "$VERBOSE" -eq 1 ] && echo "$1"
|
||||
local message
|
||||
message="$1"
|
||||
if [[ "$VERBOSE" -eq 1 && "$QUIET" -eq 0 ]]; then
|
||||
echo "$message"
|
||||
[[ -n "$LOG_FILE" ]] && log "INFO" "$message"
|
||||
elif [[ -n "$LOG_FILE" ]]; then
|
||||
log "DEBUG" "$message"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print normal output unless QUIET is enabled
|
||||
# $1 - message (string)
|
||||
print()
|
||||
{
|
||||
local message
|
||||
message="$1"
|
||||
if [[ "$QUIET" -eq 0 ]]; then
|
||||
echo -e "$message"
|
||||
[[ -n "$LOG_FILE" ]] && log "INFO" "$message"
|
||||
elif [[ -n "$LOG_FILE" ]]; then
|
||||
log "INFO" "$message"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to display progress bar
|
||||
# $1 - current (int)
|
||||
# $2 - total (int)
|
||||
# $3 - status message (string)
|
||||
show_progress()
|
||||
{
|
||||
[[ "$QUIET" -eq 1 ]] && return
|
||||
|
||||
local current total status percent filled empty
|
||||
|
||||
current=$1
|
||||
total=$2
|
||||
status=$3
|
||||
|
||||
# If TERM_WIDTH is less than LAST_STATUS_LENGTH set TERM_WIDTH
|
||||
# to it.
|
||||
if [[ $TERM_WIDTH -lt $LAST_STATUS_LENGTH ]]; then
|
||||
TERM_WIDTH=$LAST_STATUS_LENGTH
|
||||
fi
|
||||
|
||||
# Clear the entire line before updating to avoid artifacts
|
||||
printf "\r%-${TERM_WIDTH}s" " "
|
||||
|
||||
# Avoid division by zero
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
percent=0
|
||||
else
|
||||
percent=$((current * 100 / total))
|
||||
fi
|
||||
|
||||
filled=$((percent * PROGRESS_WIDTH / 100))
|
||||
# Ensure filled doesn't exceed PROGRESS_WIDTH
|
||||
[[ $filled -gt $PROGRESS_WIDTH ]] && filled=$PROGRESS_WIDTH
|
||||
empty=$((PROGRESS_WIDTH - filled))
|
||||
|
||||
# Truncate status message if too long
|
||||
if [[ ${#status} -gt $MAX_DIR_LENGTH ]]; then
|
||||
status="...${status:$((${#status} - MAX_DIR_LENGTH + 4))}"
|
||||
fi
|
||||
|
||||
# Pad the status message to ensure consistent width and add extra space
|
||||
printf -v padded_status "%-${MAX_DIR_LENGTH}s" "$status"
|
||||
|
||||
# Create and display the progress bar with fixed width for percentage and colors
|
||||
printf "\r[${BLUE}%s${NC}%s] ${GREEN}%3d%%${NC} ${CYAN}%s${NC}" \
|
||||
"$(printf '#%.0s' $(seq 1 $filled))" \
|
||||
"$(printf ' %.0s' $(seq 1 $empty))" \
|
||||
"$percent" \
|
||||
"$padded_status"
|
||||
|
||||
# Store the length of the current status
|
||||
LAST_STATUS_LENGTH=${#status}
|
||||
|
||||
# Log progress if logging is enabled
|
||||
[[ -n "$LOG_FILE" ]] && log "DEBUG" "Progress: $percent% - $status"
|
||||
}
|
||||
|
||||
# Is the directory path excluded?
|
||||
# $1: Directory path
|
||||
# Return 0 if the directory should be skipped, 1 otherwise
|
||||
excluded_path()
|
||||
{
|
||||
local dir home
|
||||
dir="$(realpath "$1")"
|
||||
home="$(realpath "$HOME")"
|
||||
|
||||
# Check if directory should be excluded
|
||||
for exclude in $EXCLUDE_DIRS; do
|
||||
# Check for parts of the directory name
|
||||
if [[ "$dir" == *"$exclude"* ]] || [[ "$dir" == "$exclude" ]]; then
|
||||
msg "Skipping excluded directory: $dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Run only if home is not empty
|
||||
if [[ -n "$home" ]]; then
|
||||
# Remove home directory from path
|
||||
relative_dir="${dir/"$home"/}"
|
||||
|
||||
# Check if we should exclude based on relative paths based on the home directory
|
||||
if [[ "$relative_dir" == *"$exclude"* ]] || [[ "$relative_dir" == "$exclude" ]]; then
|
||||
msg "Skipping excluded relative directory: $dir"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if it's a git repository
|
||||
if [[ ! -d "$dir/.git" ]]; then
|
||||
msg "Skipping non-git directory: $dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to count git repositories
|
||||
count_git_repos()
|
||||
{
|
||||
local count=0
|
||||
for dir in */; do
|
||||
if ! excluded_path "$dir"; then
|
||||
((count++))
|
||||
fi
|
||||
done
|
||||
echo $count
|
||||
}
|
||||
|
||||
# Check for unmerged files or conflicts in a git repository
|
||||
# Returns 0 if there are unmerged files, 1 otherwise
|
||||
has_unmerged_files()
|
||||
{
|
||||
git ls-files --unmerged | grep -q "^" \
|
||||
&& return 0 || return 1
|
||||
}
|
||||
|
||||
# Check for clean working directory
|
||||
# Returns 0 if working directory is clean, 1 otherwise
|
||||
is_repo_clean()
|
||||
{
|
||||
git diff --quiet \
|
||||
&& git diff --cached --quiet \
|
||||
&& return 0 || return 1
|
||||
}
|
||||
|
||||
# Function to clean up local branches that have been merged
|
||||
# Returns the number of branches cleaned
|
||||
cleanup_branches()
|
||||
{
|
||||
local cleaned=0
|
||||
local current_branch output
|
||||
|
||||
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
|
||||
# Skip branch cleanup if we're not on a main branch
|
||||
if [[ ! "$current_branch" =~ ^(master|main|develop)$ ]]; then
|
||||
msg "Skipping branch cleanup: not on a main branch ($current_branch)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get list of merged branches, excluding current branch, master, main, and develop
|
||||
output=$(git branch --merged | grep -v -E "^\*|master|main|develop" | sed 's/^[[:space:]]*//')
|
||||
|
||||
if [[ -n "$output" ]]; then
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
msg "Cleaning up merged branches in $(pwd):"
|
||||
echo "$output" | while read -r branch; do
|
||||
msg " - $branch"
|
||||
done
|
||||
fi
|
||||
|
||||
# Delete branches
|
||||
for branch in $output; do
|
||||
if [[ -n "$branch" ]]; then
|
||||
if git branch -d "$branch" &>/dev/null; then
|
||||
((cleaned++))
|
||||
log "INFO" "Deleted merged branch $branch in $(pwd)"
|
||||
else
|
||||
log "WARNING" "Failed to delete branch $branch in $(pwd)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
return $cleaned
|
||||
}
|
||||
|
||||
# Function to update a git repository
|
||||
# $1 - directory (string)
|
||||
update_repo()
|
||||
{
|
||||
local dir=$1
|
||||
(
|
||||
cd "$dir" || exit
|
||||
msg "Updating $dir"
|
||||
git pull --rebase --autostash --prune
|
||||
)
|
||||
local dir output exit_status git_args current_branch \
|
||||
remote_name cleaned_branches
|
||||
|
||||
dir="$1"
|
||||
log "INFO" "Processing repository: $dir"
|
||||
|
||||
# Increment the processed counter
|
||||
((PROCESSED++))
|
||||
|
||||
# Show progress before starting the operation
|
||||
show_progress "$PROCESSED" "$TOTAL" "${dir%/}"
|
||||
|
||||
cd "$dir" 2>/dev/null || {
|
||||
log "ERROR" "Could not enter directory $dir"
|
||||
echo -e "\n${RED}Error: Could not enter directory $dir${NC}" >&2
|
||||
((FAILED++))
|
||||
return 1
|
||||
}
|
||||
|
||||
# If there are no remotes, skip
|
||||
if ! git remote -v &> /dev/null; then
|
||||
log "INFO" "Skipping directory with no remotes: $dir"
|
||||
msg "Skipping directory with no remotes: $dir"
|
||||
((SKIPPED++))
|
||||
cd - >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get current branch name
|
||||
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
if [[ -z "$current_branch" ]]; then
|
||||
log "INFO" "Skipping repository in detached HEAD state: $dir"
|
||||
msg "Skipping repository in detached HEAD state: $dir"
|
||||
((SKIPPED++))
|
||||
cd - >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if current branch has tracking information
|
||||
eval "git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null" &>/dev/null || {
|
||||
log "INFO" "Skipping branch '$current_branch' without tracking info in $dir"
|
||||
msg "Skipping branch '$current_branch' without tracking info in $dir"
|
||||
((SKIPPED++))
|
||||
cd - >/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if remote is accessible
|
||||
remote_name=$(git config --get branch."$current_branch".remote)
|
||||
if [[ -n "$remote_name" ]]; then
|
||||
if ! git ls-remote --exit-code "$remote_name" &>/dev/null; then
|
||||
log "WARNING" "Skipping repository with inaccessible remote '$remote_name': $dir"
|
||||
msg "Skipping repository with inaccessible remote: $dir"
|
||||
((SKIPPED++))
|
||||
cd - >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for unmerged files before attempting pull
|
||||
if has_unmerged_files; then
|
||||
log "WARNING" "Skipping repository with unmerged files: $dir"
|
||||
msg "Skipping repository with unmerged files: $dir"
|
||||
((UNMERGED++))
|
||||
cd - >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Configure Git arguments based on verbosity
|
||||
git_args="--rebase --autostash --prune"
|
||||
if [[ "$VERBOSE" -eq 0 ]]; then
|
||||
git_args="$git_args --quiet"
|
||||
fi
|
||||
|
||||
# Disable Git hints and set other environment variables
|
||||
export GIT_MERGE_AUTOEDIT=no
|
||||
export GIT_CONFIG_COUNT=4
|
||||
export GIT_CONFIG_KEY_0="advice.skipHints"
|
||||
export GIT_CONFIG_VALUE_0="true"
|
||||
export GIT_CONFIG_KEY_1="advice.detachedHead"
|
||||
export GIT_CONFIG_VALUE_1="false"
|
||||
export GIT_CONFIG_KEY_2="advice.pushUpdateRejected"
|
||||
export GIT_CONFIG_VALUE_2="false"
|
||||
export GIT_CONFIG_KEY_3="advice.statusHints"
|
||||
export GIT_CONFIG_VALUE_3="false"
|
||||
|
||||
# Capture the output of git pull
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
# shellcheck disable=SC2086
|
||||
output=$(git pull $git_args 2>&1)
|
||||
exit_status=$?
|
||||
# In verbose mode, show the git output
|
||||
[[ "$QUIET" -eq 0 ]] && echo -e "\n$output\n"
|
||||
log "DEBUG" "Git pull output: $output"
|
||||
else
|
||||
# In non-verbose mode, suppress normal output but capture errors
|
||||
# shellcheck disable=SC2086
|
||||
output=$(git pull $git_args 2>&1) || {
|
||||
exit_status=$?
|
||||
}
|
||||
|
||||
# If no error occurred, set exit_status to 0
|
||||
exit_status=${exit_status:-0}
|
||||
fi
|
||||
|
||||
# Unset environment variables
|
||||
unset GIT_MERGE_AUTOEDIT GIT_CONFIG_COUNT \
|
||||
GIT_CONFIG_KEY_0 GIT_CONFIG_KEY_1 \
|
||||
GIT_CONFIG_KEY_2 GIT_CONFIG_KEY_3 \
|
||||
GIT_CONFIG_VALUE_0 GIT_CONFIG_VALUE_1 \
|
||||
GIT_CONFIG_VALUE_2 GIT_CONFIG_VALUE_3
|
||||
|
||||
# Check for specific error conditions
|
||||
if echo "$output" | grep -q "Merge conflict"; then
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Merge conflict detected in $dir. Aborting update.${NC}" >&2
|
||||
fi
|
||||
log "WARNING" "Merge conflict detected in $dir. Aborting update."
|
||||
git rebase --abort &> /dev/null || git merge --abort &> /dev/null || true
|
||||
((CONFLICTS++))
|
||||
elif echo "$output" | grep -q "unmerged files"; then
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Unmerged files detected in $dir. Aborting update.${NC}" >&2
|
||||
fi
|
||||
log "WARNING" "Unmerged files detected in $dir. Aborting update."
|
||||
((UNMERGED++))
|
||||
elif echo "$output" | grep -q "untracked working tree files would be overwritten by merge"; then
|
||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Untracked files would be overwritten in $dir. Aborting update.${NC}" >&2
|
||||
fi
|
||||
log "WARNING" "Untracked files would be overwritten in $dir. Aborting update."
|
||||
((UNTRACKED++))
|
||||
elif [[ $exit_status -ne 0 ]]; then
|
||||
if [[ "$VERBOSE" -eq 1 || "$QUIET" -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${RED}Error updating $dir${NC}" >&2
|
||||
echo "$output" >&2
|
||||
fi
|
||||
log "ERROR" "Failed to update $dir: $output"
|
||||
((FAILED++))
|
||||
else
|
||||
# Check if any changes were pulled
|
||||
if echo "$output" | grep -qE '(file changed|files changed|insertions|deletions)' \
|
||||
|| ! echo "$output" | grep -q "Already up to date."; then
|
||||
log "INFO" "Repository updated with changes: $dir"
|
||||
((UPDATED++))
|
||||
else
|
||||
log "INFO" "Repository already up to date: $dir"
|
||||
fi
|
||||
((SUCCESS++))
|
||||
|
||||
# Clean up branches if requested
|
||||
if [[ "$CLEANUP" -eq 1 ]]; then
|
||||
cleaned_branches=$(cleanup_branches)
|
||||
if [[ $cleaned_branches -gt 0 ]]; then
|
||||
((BRANCHES_CLEANED += cleaned_branches))
|
||||
log "INFO" "Cleaned up $cleaned_branches merged branches in $dir"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Return to original directory
|
||||
cd - >/dev/null || true
|
||||
|
||||
# Show progress after completion
|
||||
show_progress "$PROCESSED" "$TOTAL" "${dir%/} - Done"
|
||||
}
|
||||
|
||||
# Main function to update all subfolder git repositories
|
||||
main()
|
||||
{
|
||||
local current_dir start_time end_time duration
|
||||
|
||||
# Record start time
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Process command-line args before doing anything else
|
||||
process_args "$@"
|
||||
|
||||
# Save current directory to return to it later
|
||||
current_dir=$(pwd)
|
||||
log "INFO" "Starting repository updates in $current_dir"
|
||||
|
||||
# Count repositories and set TOTAL
|
||||
TOTAL=$(count_git_repos)
|
||||
print "Found $TOTAL git repositories to update"
|
||||
|
||||
# Reset other counters
|
||||
PROCESSED=0
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
CONFLICTS=0
|
||||
UPDATED=0
|
||||
SKIPPED=0
|
||||
UNTRACKED=0
|
||||
UNMERGED=0
|
||||
BRANCHES_CLEANED=0
|
||||
|
||||
# Process each repository
|
||||
for dir in */; do
|
||||
# Skip if excluded
|
||||
if excluded_path "$dir"; then
|
||||
continue
|
||||
fi
|
||||
update_repo "$dir"
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
echo ""
|
||||
# Return to original directory
|
||||
cd "$current_dir" || true
|
||||
|
||||
# Clear the progress line completely
|
||||
[[ "$QUIET" -eq 0 ]] && printf "\r%-${TERM_WIDTH}s\r" " "
|
||||
|
||||
# Calculate duration
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
minutes=$((duration / 60))
|
||||
seconds=$((duration % 60))
|
||||
|
||||
# Format duration nicely
|
||||
if [[ $minutes -gt 0 ]]; then
|
||||
duration_str="${minutes}m ${seconds}s"
|
||||
else
|
||||
duration_str="${seconds}s"
|
||||
fi
|
||||
|
||||
# Print summary unless quiet mode is enabled
|
||||
if [[ "$QUIET" -eq 0 ]]; then
|
||||
echo ""
|
||||
print "${GREEN}Summary: Updated $SUCCESS/$TOTAL repositories successfully in $duration_str.${NC}"
|
||||
print "${CYAN}Repositories with changes pulled: $UPDATED${NC}"
|
||||
|
||||
if [[ $SKIPPED -gt 0 ]]; then
|
||||
print "${YELLOW}Skipped $SKIPPED repositories (no tracking branch or other issues).${NC}"
|
||||
fi
|
||||
|
||||
if [[ $UNMERGED -gt 0 ]]; then
|
||||
print "${YELLOW}Skipped $UNMERGED repositories with unmerged files.${NC}"
|
||||
fi
|
||||
|
||||
if [[ $UNTRACKED -gt 0 ]]; then
|
||||
print "${YELLOW}Skipped $UNTRACKED repositories with untracked files that would be overwritten.${NC}"
|
||||
fi
|
||||
|
||||
if [[ $CONFLICTS -gt 0 ]]; then
|
||||
print "${YELLOW}Encountered merge conflicts in $CONFLICTS repositories.${NC}"
|
||||
fi
|
||||
|
||||
if [[ $CLEANUP -eq 1 && $BRANCHES_CLEANED -gt 0 ]]; then
|
||||
print "${BLUE}Cleaned up $BRANCHES_CLEANED merged branches.${NC}"
|
||||
fi
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo -e "${RED}Failed to update $FAILED repositories.${NC}" >&2
|
||||
else
|
||||
print "${GREEN}Done.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Log final summary
|
||||
if [[ -n "$LOG_FILE" ]]; then
|
||||
log "INFO" "Completed in $duration_str"
|
||||
log "INFO" "Summary: $SUCCESS/$TOTAL repositories updated successfully"
|
||||
log "INFO" "Repositories with changes pulled: $UPDATED"
|
||||
log "INFO" "Skipped: $SKIPPED, Unmerged: $UNMERGED, Untracked: $UNTRACKED, Conflicts: $CONFLICTS, Failed: $FAILED"
|
||||
if [[ $CLEANUP -eq 1 ]]; then
|
||||
log "INFO" "Branches cleaned up: $BRANCHES_CLEANED"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Return appropriate exit code
|
||||
[[ $FAILED -gt 0 ]] && return 1 || return 0
|
||||
}
|
||||
|
||||
# Call main with all arguments
|
||||
main "$@"
|
||||
|
||||
116
local/bin/git-update-dirs.md
Normal file
116
local/bin/git-update-dirs.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# git-update-dirs
|
||||
|
||||
A tool that efficiently updates all Git repositories in subdirectories
|
||||
of the current folder.
|
||||
|
||||
## Overview
|
||||
|
||||
`git-update-dirs` scans the current directory for Git repositories
|
||||
and updates them with:
|
||||
|
||||
- Fast parallel execution
|
||||
- Intelligent error handling
|
||||
- Progress visualization
|
||||
- Detailed logging
|
||||
- Optional branch cleanup
|
||||
|
||||
## Installation
|
||||
|
||||
Place the script in your PATH and make it executable:
|
||||
|
||||
```bash
|
||||
# Using wget
|
||||
wget -O ~/bin/git-update-dirs https://raw.githubusercontent.com/ivuorinen/dotfiles/main/local/bin/git-update-dirs
|
||||
chmod +x ~/bin/git-update-dirs
|
||||
|
||||
# Or simply copy the script to a location in your PATH
|
||||
cp git-update-dirs ~/bin/
|
||||
chmod +x ~/bin/git-update-dirs
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
Usage: git-update-dirs [OPTIONS]
|
||||
|
||||
Updates all git repositories in subdirectories.
|
||||
|
||||
Options:
|
||||
--help, -h Display this help message and exit
|
||||
--version, -v Display version information and exit
|
||||
--verbose Display detailed output
|
||||
--quiet, -q Suppress all output except errors
|
||||
--exclude DIR Exclude directory from updates
|
||||
(can be used multiple times)
|
||||
--cleanup Remove local branches that have been merged into
|
||||
current branch
|
||||
--config FILE Read options from configuration file
|
||||
--log FILE Log details and errors to FILE
|
||||
|
||||
Environment variables:
|
||||
VERBOSE Set to 1 to enable verbose output
|
||||
EXCLUDE_DIRS Space-separated list of directories to exclude
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Basic usage to update all repositories:
|
||||
|
||||
```bash
|
||||
git-update-dirs
|
||||
```
|
||||
|
||||
Update with detailed output:
|
||||
|
||||
```bash
|
||||
git-update-dirs --verbose
|
||||
```
|
||||
|
||||
Exclude specific directories:
|
||||
|
||||
```bash
|
||||
git-update-dirs --exclude node_modules --exclude vendor
|
||||
```
|
||||
|
||||
Update and clean up merged branches:
|
||||
|
||||
```bash
|
||||
git-update-dirs --cleanup
|
||||
```
|
||||
|
||||
Use options from a configuration file:
|
||||
|
||||
```bash
|
||||
git-update-dirs --config ~/.gitupdate.conf
|
||||
```
|
||||
|
||||
## Configuration File
|
||||
|
||||
You can create a configuration file to store your preferred options:
|
||||
|
||||
```text
|
||||
# Example ~/.gitupdate.conf
|
||||
verbose
|
||||
exclude node_modules
|
||||
exclude vendor
|
||||
cleanup
|
||||
log ~/.gitupdate.log
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Updates**: Uses `--rebase --autostash --prune`
|
||||
for clean updates
|
||||
- **Error Handling**: Skips repositories with conflicts or
|
||||
untracked files that would be overwritten
|
||||
- **Visual Progress**: Shows a progress bar with current status
|
||||
- **Repository Management**: Optionally cleans up merged branches
|
||||
- **Detailed Logging**: Records all operations with timestamps
|
||||
|
||||
## License
|
||||
|
||||
[MIT License][MIT] - Copyright 2023 Ismo Vuorinen
|
||||
|
||||
[MIT]: https://opensource.org/license/mit/
|
||||
|
||||
<!-- vim: set ft=markdown cc=80 : -->
|
||||
443
local/bin/php-switcher
Executable file
443
local/bin/php-switcher
Executable file
@@ -0,0 +1,443 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Brew PHP Switcher
|
||||
#
|
||||
# Use to switch between PHP versions installed via Homebrew.
|
||||
#
|
||||
# Usage: php-switcher <version> [--help|--installed|--current|--auto]
|
||||
# Example: php-switcher 7.4
|
||||
# Example: php-switcher 8.0
|
||||
# Example: php-switcher latest
|
||||
# Example: php-switcher --auto
|
||||
#
|
||||
# Created by Ismo Vuorinen <https://github.com/ivuorinen> (2025)
|
||||
# Licensed under the MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
set -euo pipefail # Add error handling
|
||||
|
||||
# Configuration
|
||||
LATEST_VERSION_FORMULA="php" # The formula name for latest PHP version
|
||||
PHP_VERSION_FILE=".php-version" # File name to look for when auto-switching
|
||||
|
||||
# Switch brew php version
|
||||
function check_dependencies()
|
||||
{
|
||||
if ! command -v brew > /dev/null 2>&1; then
|
||||
echo "Error: Homebrew is not installed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function usage()
|
||||
{
|
||||
echo "Brew PHP Switcher - Switch between PHP versions installed via Homebrew"
|
||||
echo ""
|
||||
echo "Usage: php-switcher <version> [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --help Show this help message"
|
||||
echo " --installed List installed PHP versions"
|
||||
echo " --current Show currently active PHP version"
|
||||
echo " --auto Auto-switch based on .php-version file in current directory"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " php-switcher 7.4"
|
||||
echo " php-switcher 8.0"
|
||||
echo " php-switcher latest"
|
||||
echo " php-switcher --auto"
|
||||
echo ""
|
||||
echo "Auto-switching:"
|
||||
echo " Create a .php-version file in your project directory with a PHP version"
|
||||
echo " Example .php-version content: 8.1"
|
||||
echo ""
|
||||
exit 0
|
||||
}
|
||||
|
||||
function list_php_versions()
|
||||
{
|
||||
# Check Homebrew's installation path for PHP versions
|
||||
local brew_cellar
|
||||
brew_cellar="$(brew --cellar)"
|
||||
local php_paths=()
|
||||
local versions=()
|
||||
local formulas=()
|
||||
local active=()
|
||||
|
||||
# Look for all PHP installations in Homebrew Cellar
|
||||
if [[ -d "$brew_cellar/php" ]]; then
|
||||
php_paths+=("$brew_cellar/php")
|
||||
fi
|
||||
|
||||
# Look for versioned PHP installations
|
||||
while IFS= read -r dir; do
|
||||
if [[ -d $dir ]]; then
|
||||
php_paths+=("$dir")
|
||||
fi
|
||||
done < <(find "$brew_cellar" \
|
||||
-maxdepth 1 -name 'php@*' \
|
||||
-type d 2> /dev/null || echo "")
|
||||
|
||||
if [[ ${#php_paths[@]} -eq 0 ]]; then
|
||||
echo "No PHP versions installed through Homebrew."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find which version is currently linked
|
||||
local current_bin
|
||||
current_bin=$(readlink -f \
|
||||
"$(command -v php 2> /dev/null)" \
|
||||
2> /dev/null || echo "")
|
||||
|
||||
# Collect data for each installed PHP version
|
||||
for path in "${php_paths[@]}"; do
|
||||
local formula
|
||||
formula=$(basename "$path")
|
||||
local version_label
|
||||
|
||||
if [[ $formula == "php" ]]; then
|
||||
version_label="latest"
|
||||
else
|
||||
version_label="${formula#php@}"
|
||||
fi
|
||||
|
||||
# Find the actual version from the directory structure
|
||||
local version_dir
|
||||
version_dir=$(find "$path" -maxdepth 1 -type d \
|
||||
| grep -v "^$path$" | sort -V | tail -1)
|
||||
|
||||
if [[ -n $version_dir && -d "$version_dir/bin" ]]; then
|
||||
local full_version
|
||||
full_version=$("$version_dir/bin/php" -v 2> /dev/null \
|
||||
| grep -oE 'PHP [0-9]+\.[0-9]+\.[0-9]+' \
|
||||
| head -1 \
|
||||
| cut -d' ' -f2 \
|
||||
|| echo "$version_label.x")
|
||||
|
||||
# Determine if this is the active version
|
||||
local is_active="No"
|
||||
if [[ -n $current_bin && $current_bin == "$version_dir/bin/php" ]]; then
|
||||
is_active="Yes"
|
||||
fi
|
||||
|
||||
# Handle the 'latest' case - replace with actual version number
|
||||
local display_version
|
||||
if [[ $version_label == "latest" ]]; then
|
||||
display_version="${full_version%.*}" # Get major.minor version
|
||||
else
|
||||
display_version="$version_label"
|
||||
fi
|
||||
|
||||
# Store data for table display
|
||||
versions+=("$display_version")
|
||||
formulas+=("$formula")
|
||||
active+=("$is_active")
|
||||
fi
|
||||
done
|
||||
|
||||
# Calculate maximum column widths
|
||||
local max_version_width=7 # "Version" header length
|
||||
local max_formula_width=7 # "Formula" header length
|
||||
local max_active_width=6 # "Active" header length
|
||||
|
||||
local count=${#versions[@]}
|
||||
for ((i = 0; i < count; i++)); do
|
||||
# Update max widths if needed
|
||||
if [[ ${#versions[i]} -gt $max_version_width ]]; then
|
||||
max_version_width=${#versions[i]}
|
||||
fi
|
||||
if [[ ${#formulas[i]} -gt $max_formula_width ]]; then
|
||||
max_formula_width=${#formulas[i]}
|
||||
fi
|
||||
done
|
||||
|
||||
# Build header with correct widths
|
||||
local header_format="| %-${max_version_width}s | %-${max_formula_width}s | "
|
||||
header_format+="%-${max_active_width}s |"
|
||||
|
||||
local separator_line="|"
|
||||
for ((i = 0; i < max_version_width + 2; i++)); do
|
||||
separator_line="${separator_line}-"
|
||||
done
|
||||
separator_line="${separator_line}|"
|
||||
|
||||
for ((i = 0; i < max_formula_width + 2; i++)); do
|
||||
separator_line="${separator_line}-"
|
||||
done
|
||||
separator_line="${separator_line}|"
|
||||
|
||||
for ((i = 0; i < max_active_width + 2; i++)); do
|
||||
separator_line="${separator_line}-"
|
||||
done
|
||||
separator_line="${separator_line}|"
|
||||
|
||||
# Print table header
|
||||
# shellcheck disable=SC2059
|
||||
printf "$header_format\n" "Version" "Formula" "Active"
|
||||
echo "$separator_line"
|
||||
|
||||
# Print table rows
|
||||
local row_format="| %-${max_version_width}s | %-${max_formula_width}s | "
|
||||
row_format+="%-${max_active_width}s |"
|
||||
|
||||
for ((i = 0; i < count; i++)); do
|
||||
# shellcheck disable=SC2059
|
||||
printf "$row_format\n" "${versions[i]}" "${formulas[i]}" "${active[i]}"
|
||||
done
|
||||
}
|
||||
|
||||
function get_php_formula_for_version()
|
||||
{
|
||||
local version="$1"
|
||||
|
||||
# Handle "latest" as a special case
|
||||
if [[ $version == "latest" ]]; then
|
||||
echo "$LATEST_VERSION_FORMULA"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# The regular version case (e.g., 7.4, 8.1)
|
||||
echo "php@$version"
|
||||
}
|
||||
|
||||
function check_formula_installed()
|
||||
{
|
||||
local formula="$1"
|
||||
local brew_cellar
|
||||
brew_cellar="$(brew --cellar)"
|
||||
|
||||
if [[ $formula == "php" ]]; then
|
||||
if [[ -d "$brew_cellar/php" ]]; then
|
||||
return 0
|
||||
fi
|
||||
elif [[ -d "$brew_cellar/$formula" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function unlink_current_php()
|
||||
{
|
||||
local current_formula=""
|
||||
|
||||
# Find formulas more safely
|
||||
while IFS= read -r formula; do
|
||||
if [[ -n $formula ]]; then
|
||||
local linked
|
||||
linked=$(brew info --json=v1 "$formula" \
|
||||
| grep -o '"linked_keg":"[^"]*"' \
|
||||
| grep -v ':"null"')
|
||||
if [[ -n $linked ]]; then
|
||||
current_formula="$formula"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done < <(brew list --formula | grep -E '^php(@[0-9]+\.[0-9]+)?$' || echo "")
|
||||
|
||||
# If we found a linked formula, unlink it
|
||||
if [[ -n $current_formula ]]; then
|
||||
echo "Unlinking current PHP version ($current_formula)..."
|
||||
brew unlink "$current_formula" > /dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
function link_php_version()
|
||||
{
|
||||
local formula="$1"
|
||||
|
||||
if ! check_formula_installed "$formula"; then
|
||||
echo "Error: PHP formula '$formula' is not installed"
|
||||
echo "Available versions:"
|
||||
list_php_versions
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Linking $formula..."
|
||||
if ! brew link --force --overwrite "$formula" > /dev/null 2>&1; then
|
||||
echo "Error: Failed to link $formula. Try running manually:"
|
||||
echo " brew link --force --overwrite $formula"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the switch worked
|
||||
if ! command -v php > /dev/null 2>&1; then
|
||||
echo "Warning: PHP was linked but may not be working correctly"
|
||||
fi
|
||||
}
|
||||
|
||||
function get_current_version()
|
||||
{
|
||||
if ! command -v php > /dev/null 2>&1; then
|
||||
echo "No PHP currently linked"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local version
|
||||
version=$(php -v 2> /dev/null \
|
||||
| grep -oE 'PHP [0-9]+\.[0-9]+\.[0-9]+' \
|
||||
| head -1)
|
||||
|
||||
if [[ -z $version ]]; then
|
||||
echo "Unable to determine PHP version"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find the corresponding formula
|
||||
local current_version
|
||||
current_version=$(echo "$version" | cut -d' ' -f2)
|
||||
local major_minor
|
||||
major_minor=$(echo "$current_version" | cut -d'.' -f1,2)
|
||||
|
||||
# Check if it's the latest version
|
||||
if check_formula_installed "php" \
|
||||
&& brew info --json=v1 php \
|
||||
| grep -o '"linked_keg":"[^"]*"' \
|
||||
| grep -v ':"null"' \
|
||||
| grep -q .; then
|
||||
echo "Current PHP version: $current_version (latest)"
|
||||
else
|
||||
echo "Current PHP version: $current_version (php@$major_minor)"
|
||||
fi
|
||||
}
|
||||
|
||||
function validate_version()
|
||||
{
|
||||
local version="$1"
|
||||
|
||||
# Valid formats: x.y or latest
|
||||
if [[ ! $version =~ ^([0-9]+\.[0-9]+|latest)$ ]]; then
|
||||
echo "Error: Invalid PHP version format. Use x.y format (e.g., 7.4) or"
|
||||
echo " 'latest'"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function find_php_version_file()
|
||||
{
|
||||
local dir="$PWD"
|
||||
|
||||
# Look for .php-version file in current directory and all parent directories
|
||||
while [[ $dir != "/" ]]; do
|
||||
if [[ -f "$dir/$PHP_VERSION_FILE" ]]; then
|
||||
echo "$dir/$PHP_VERSION_FILE"
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
done
|
||||
|
||||
# Check the root directory as well
|
||||
if [[ -f "/$PHP_VERSION_FILE" ]]; then
|
||||
echo "/$PHP_VERSION_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function auto_switch_php_version()
|
||||
{
|
||||
local version_file
|
||||
|
||||
# Try to find a .php-version file
|
||||
version_file=$(find_php_version_file) || {
|
||||
echo "No .php-version file found in current directory or any parent"
|
||||
echo "directory. Create a $PHP_VERSION_FILE file with your desired"
|
||||
echo "PHP version (e.g., 8.1)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Read the version from the file
|
||||
local version
|
||||
version=$(tr -d '[:space:]' < "$version_file")
|
||||
|
||||
echo "Found $PHP_VERSION_FILE file at: $version_file"
|
||||
echo "Requested PHP version: $version"
|
||||
|
||||
# Validate the version
|
||||
validate_version "$version"
|
||||
|
||||
# Switch to the specified version
|
||||
switch_php_version "$version"
|
||||
}
|
||||
|
||||
function switch_php_version()
|
||||
{
|
||||
local version="$1"
|
||||
|
||||
# Get the formula name for the version
|
||||
local formula
|
||||
formula=$(get_php_formula_for_version "$version")
|
||||
|
||||
# Check if the requested PHP version is installed
|
||||
if ! check_formula_installed "$formula"; then
|
||||
echo "Error: PHP version $version is not installed"
|
||||
echo ""
|
||||
list_php_versions
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current version info for comparison
|
||||
local current_info
|
||||
current_info=$(get_current_version 2> /dev/null || echo "None")
|
||||
|
||||
# Skip if we're already on the requested version
|
||||
if [[ $current_info == *"$version"* ]]; then
|
||||
echo "PHP version $version is already active"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Perform the switch
|
||||
unlink_current_php
|
||||
link_php_version "$formula"
|
||||
|
||||
# Verify the switch
|
||||
echo ""
|
||||
echo "Switched to:"
|
||||
get_current_version
|
||||
echo ""
|
||||
echo "PHP executable: $(command -v php)"
|
||||
}
|
||||
|
||||
function main()
|
||||
{
|
||||
local version=""
|
||||
|
||||
# Parse arguments
|
||||
case "${1:-}" in
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
--installed)
|
||||
list_php_versions
|
||||
exit 0
|
||||
;;
|
||||
--current)
|
||||
get_current_version
|
||||
exit 0
|
||||
;;
|
||||
--auto)
|
||||
auto_switch_php_version
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
version="$1"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Validate and switch to the specified version
|
||||
validate_version "$version"
|
||||
switch_php_version "$version"
|
||||
}
|
||||
|
||||
# Run the script
|
||||
check_dependencies
|
||||
if [[ ${#@} -eq 0 ]]; then
|
||||
usage
|
||||
else
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
# vim: ft=bash sw=4 ts=4 et tw=80 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
|
||||
#
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
[[ -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
|
||||
|
||||
Reference in New Issue
Block a user