fun bug fix

This commit is contained in:
2026-10-13 13:16:08 +02:00
parent 9723a6ad93
commit 229422eda0
3 changed files with 176 additions and 85 deletions
+3 -3
View File
@@ -108,10 +108,10 @@ A bar-style prompt showing current time, execution time of the last command
| `genpwd` | pwd | Generate one or more random secure passwords with configurable constraints |
| `ggraph` | git | Display a decorated git history graph |
| `gpid` | processes | Give the list of PIDs matching the given process name(s) |
| `gprune` | git | Delete local branches already merged into the main branch |
| `greset` | git | Reset the current branch to upstream, stashing local changes first |
| `gprune` | git | Delete local branches already merged into the main branch; `-D`/`--deep` also deletes (force) branches whose upstream branch was deleted on the remote, e.g. after a merge request |
| `greset` | git | Reset the current branch to upstream, stashing local changes first (the target is checked before anything is stashed; the previous HEAD is printed for recovery); `-x` also removes ignored files, which cannot be undone |
| `groot` | git | Display the repository root path, or change directory to it with `-g` |
| `gsync` | git | Fetch and rebase the current branch onto its upstream |
| `gsync` | git | Fetch (the remote the branch tracks, or the given one) and rebase the current branch onto its upstream; needs a checked-out branch |
| `gst` | git | Display compact git status with branch tracking information |
| `gwip` | git | Create a quick WIP checkpoint commit |
| `hack` | fun | Simulate a dramatic multi-phase hacking sequence |
+9
View File
@@ -71,6 +71,15 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
`busy` documented in its usage now works; `grep`/`read` no longer mangle
patterns starting with `-` or lines with leading blanks; `awk` is no longer
forked for each pause.
- `git.sh`: `gprune --deep` never deleted anything (`<branch>@{upstream}` fails
once the remote branch is gone; it now reads the configured upstream ref);
`gprune` returned 1 on success and could not be told which branches are
protected (`GIT_MAIN_BRANCH` is now honoured); `greset` stashed the local
changes before checking that the target exists, ignored untracked files
outside the current directory and cleaned only below it; `gsync` could fetch
a different remote than the one it rebased on; `gacp`/`gsync` handle remote
and branch names containing slashes, and refuse a detached HEAD; `ggraph`
rejects a limit of 0; internal helpers are exported with the commands.
- `compress.sh`: option flags and loop variables no longer leak into the
interactive shell.
+164 -82
View File
@@ -60,6 +60,7 @@ _git_require_repo()
return 0
}
export -f _git_require_repo
# ------------------------------------------------------------------------------
@@ -79,6 +80,41 @@ _git_default_branch()
printf "%s\n" "$GIT_MAIN_BRANCH"
return 0
}
export -f _git_default_branch
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: print the name of the checked-out branch; fail with an error
# message on a detached HEAD (commands that push or rebase need a branch).
_git_current_branch()
{
local b
b=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || {
disp E "HEAD is detached: check out a branch first."
return 1
}
printf "%s\n" "$b"
}
export -f _git_current_branch
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: print "<remote><TAB><branch>" for the remote branch tracked
# by local branch $1, read from its configuration (branch.<name>.remote/.merge).
# Unlike parsing the "origin/feature/x" form given by @{u}, this is exact for
# remote and branch names containing slashes. Fails when nothing is tracked or
# when the branch tracks another local branch (remote ".").
_git_tracking()
{
local branch=$1 remote merge
remote=$(git config --get "branch.${branch}.remote") || return 1
merge=$(git config --get "branch.${branch}.merge") || return 1
[[ $remote == "." ]] && return 1
printf "%s\t%s\n" "$remote" "${merge#refs/heads/}"
}
export -f _git_tracking
# ------------------------------------------------------------------------------
@@ -95,9 +131,8 @@ gst()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -144,9 +179,8 @@ ggraph()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
local limit=30
while true; do
case "$1" in
@@ -172,7 +206,7 @@ ggraph()
esac
done
[[ $limit =~ ^[0-9]+$ ]] || {
[[ $limit =~ ^[1-9][0-9]*$ ]] || {
disp E "Invalid limit: must be a positive integer."
return 1
}
@@ -197,9 +231,8 @@ gsync()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -220,11 +253,22 @@ gsync()
_git_require_repo || return 1
local remote="${1:-$GIT_DEFAULT_REMOTE}"
local branch upstream
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
local branch tracking remote upstream
branch=$(_git_current_branch) || return 1
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
# Without argument, fetch the remote the branch actually tracks, so the
# fetched refs and the rebase target always match; fall back to the
# configured default remote.
remote="${1:-}"
if [[ -z $remote ]]; then
if tracking=$(_git_tracking "$branch"); then
remote="${tracking%%$'\t'*}"
else
remote="$GIT_DEFAULT_REMOTE"
fi
fi
disp I "Fetching from $remote..."
git fetch --prune "$remote" || return 1
@@ -254,9 +298,8 @@ gacp()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
local msg="" auto_add=0
case "${GIT_GACP_AUTO_ADD,,}" in
1|true|yes|on) auto_add=1 ;;
@@ -302,10 +345,15 @@ gacp()
return 1
fi
local branch upstream remote tracking_branch behind counts
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
local branch upstream remote tracking_branch behind counts tracking
branch=$(_git_current_branch) || return 1
# Remote and branch tracked by the current branch (empty when none, or when
# it tracks another local branch)
if tracking=$(_git_tracking "$branch"); then
remote="${tracking%%$'\t'*}"
tracking_branch="${tracking#*$'\t'}"
upstream="${remote}/${tracking_branch}"
fi
if [[ $# -gt 0 ]]; then
auto_add=0
disp I "Adding selected paths..."
@@ -326,10 +374,7 @@ gacp()
disp I "Creating commit..."
git commit -m "$msg" || return 1
if [[ -n $upstream ]]; then
remote="${upstream%%/*}"
tracking_branch="${upstream#*/}"
else
if [[ -z $upstream ]]; then
remote="$GIT_DEFAULT_REMOTE"
tracking_branch="$branch"
fi
@@ -342,7 +387,10 @@ gacp()
read -r _ behind <<< "$counts"
if [[ ${behind:-0} -gt 0 ]]; then
disp I "Remote branch is ahead, rebasing before push..."
git pull --rebase "$remote" "$tracking_branch" || return 1
git pull --rebase "$remote" "$tracking_branch" || {
disp E "Rebase failed. Your commit is kept locally: fix the conflicts (or 'git rebase --abort') and push manually."
return 1
}
fi
fi
@@ -374,9 +422,8 @@ greset()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
local clean_ignored=0
while true; do
case "$1" in
@@ -384,7 +431,8 @@ greset()
printf "greset: Reset current branch to upstream, stashing local changes first.\n"
printf "Usage: greset [target]\n"
printf "Options:\n"
printf "\t-x, --with-ignored\tAlso remove ignored files (git clean -fdx)\n"
printf "\t-x, --with-ignored\tAlso remove ignored files (git clean -fdx); they are\n"
printf "\t\t\t\tnot stashed, so this cannot be undone\n"
printf "\n"
printf "Default target is current branch upstream (@{u}).\n"
printf "If no upstream exists, fallback target is <remote>/<branch>.\n"
@@ -409,14 +457,23 @@ greset()
_git_require_repo || return 1
local branch upstream target remote old_head stash_msg stash_out stash_created=0 dropped=0
local branch upstream target remote old_head stash_msg stash_out stash_created=0 dropped=0 root
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
target="$1"
# A target starting with "-" would be taken for an option by git
if [[ $target == -* ]]; then
disp E "Invalid target '$target'."
return 1
fi
if [[ -z $target ]]; then
if [[ -n $upstream ]]; then
target="$upstream"
elif [[ $branch == "HEAD" ]]; then
disp E "HEAD is detached and has no upstream: give the target explicitly."
return 1
else
remote="$GIT_DEFAULT_REMOTE"
target="${remote}/${branch}"
@@ -429,7 +486,25 @@ greset()
old_head=$(git rev-parse HEAD 2>/dev/null) || return 1
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then
# Fetch and validate the target BEFORE touching the working tree: with an
# unknown target, the function must fail without having stashed anything.
# Only fetch when the derived remote is an actual configured remote; a bare
# tag or local-branch target has no remote to update from.
if git remote get-url "$remote" >/dev/null 2>&1; then
disp I "Fetching from $remote..."
git fetch --prune "$remote" || return 1
else
disp I "Target '$target' is not a remote ref; skipping fetch."
fi
if ! git rev-parse --verify --quiet "${target}^{commit}" >/dev/null; then
disp E "Target '$target' does not exist."
return 1
fi
# "status --porcelain" covers the whole repository (untracked files
# included), whatever the current subdirectory is.
if [[ -n $(git status --porcelain 2>/dev/null) ]]; then
stash_msg="greset:${branch}:$(date +'%Y-%m-%d %H:%M:%S')"
disp I "Stashing local changes as '$stash_msg'..."
local stash_before stash_after
@@ -441,21 +516,9 @@ greset()
}
stash_after=$(git rev-parse --quiet --verify refs/stash 2>/dev/null) || true
# A new stash means the top refs/stash sha changed (locale-independent).
[[ -n $stash_after && $stash_after != "$stash_before" ]] && stash_created=1
fi
# Only fetch when the derived remote is an actual configured remote; a bare
# tag or local-branch target has no remote to update from.
if git remote get-url "$remote" >/dev/null 2>&1; then
disp I "Fetching from $remote..."
git fetch --prune "$remote" || return 1
else
disp I "Target '$target' is not a remote ref; skipping fetch."
fi
if ! git rev-parse --verify --quiet "$target" >/dev/null; then
disp E "Target '$target' does not exist."
return 1
if [[ -n $stash_after && $stash_after != "$stash_before" ]]; then
stash_created=1
fi
fi
dropped=$(git rev-list --count "${target}..${old_head}" 2>/dev/null || printf "0")
@@ -463,16 +526,20 @@ greset()
disp W "Hard-resetting $branch to $target..."
git reset --hard "$target" || return 1
# git clean only works below the current directory: run it from the root so
# it covers the same tree as the reset and the stash above.
root=$(git rev-parse --show-toplevel) || return 1
if (( clean_ignored )); then
git clean -fdx || return 1
git -C "$root" clean -fdx || return 1
else
git clean -fd || return 1
git -C "$root" clean -fd || return 1
fi
if (( stash_created )); then
disp I "Local changes were stashed. Use 'git stash list' and 'git stash pop' when needed."
fi
disp I "greset complete. Dropped local-only commits: $dropped"
disp I "Previous HEAD was ${old_head:0:12}; 'git reset --hard ${old_head:0:12}' restores it."
return 0
}
export -f greset
@@ -492,9 +559,8 @@ gwip()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -551,9 +617,8 @@ gprune()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -584,51 +649,69 @@ gprune()
_git_require_repo || return 1
local base="${1:-$(_git_default_branch "$GIT_DEFAULT_REMOTE")}" current deleted=0
local base="${1:-$(_git_default_branch "$GIT_DEFAULT_REMOTE")}" current deleted=0 b
current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
if ! git rev-parse --verify --quiet "refs/heads/${base}" >/dev/null; then
disp E "Base branch '$base' does not exist locally."
return 1
fi
# Branches that are never deleted: the base, the checked-out one, the usual
# trunk names and the configured main branch.
local -a protected=("$base" "$current" master main develop dev "$GIT_MAIN_BRANCH")
_gprune_is_protected()
{
local p
for p in "${protected[@]}"; do
[[ $1 == "$p" ]] && return 0
done
return 1
}
# ── Standard mode: branches locally merged into base ──────────────────────
disp I "Pruning branches merged into $base..."
# for-each-ref gives plain names (git branch output carries "*", "+" and
# colour markers); "-d" refuses anything not fully merged.
while IFS= read -r b; do
[[ -z $b ]] && continue
[[ $b == "$current" ]] && continue
[[ $b == "$base" ]] && continue
[[ $b == "master" || $b == "main" || $b == "develop" || $b == "dev" ]] && continue
git branch -d "$b" >/dev/null 2>&1 && {
_gprune_is_protected "$b" && continue
if git branch -d "$b" >/dev/null 2>&1; then
printf "Deleted (merged): %s\n" "$b"
deleted=$(( deleted+1 ))
}
done < <(git branch --merged "$base" | sed -E 's/^\*?\s*//')
deleted=$(( deleted + 1 ))
fi
done < <(git for-each-ref --merged="$base" --format='%(refname:short)' refs/heads/)
# ── Deep mode: branches whose remote tracking ref was deleted upstream ─────
if (( deep )); then
disp I "Deep mode: pruning remote-tracking refs, then checking for gone branches..."
git fetch --prune --quiet
git fetch --prune --quiet || disp W "Fetch failed: using the remote-tracking refs as they are."
local upstream sha
while IFS= read -r b; do
# %(upstream) is the full ref configured as upstream, and stays set once
# the remote branch is gone, whereas "git rev-parse <branch>@{upstream}"
# fails then. It also avoids parsing the localized "[gone]" marker.
while IFS=$'\t' read -r b upstream; do
[[ -z $b ]] && continue
[[ $b == "$current" ]] && continue
[[ $b == "$base" ]] && continue
[[ $b == "master" || $b == "main" || $b == "develop" || $b == "dev" ]] && continue
_gprune_is_protected "$b" && continue
[[ -z $upstream ]] && continue # no tracking branch at all — skip
# Detect a gone upstream without relying on localized "gone]" text:
# resolve the branch's upstream, then check its remote-tracking ref.
upstream=$(git rev-parse --abbrev-ref "${b}@{upstream}" 2>/dev/null) || true
[[ -z "$upstream" ]] && continue # no tracking branch at all — skip
# If the remote-tracking ref still exists, the upstream is not gone.
git show-ref --verify --quiet "refs/remotes/$upstream" 2>/dev/null && continue
# If the upstream ref still exists, it is not gone.
git show-ref --verify --quiet "$upstream" 2>/dev/null && continue
sha=$(git rev-parse --short "$b" 2>/dev/null)
git branch -D "$b" >/dev/null 2>&1 && {
if git branch -D "$b" >/dev/null 2>&1; then
printf "Deleted (gone upstream): %s (%s)\n" "$b" "$sha"
deleted=$(( deleted+1))
}
done < <(git for-each-ref --format='%(refname:short)' refs/heads/)
deleted=$(( deleted + 1 ))
fi
done < <(git for-each-ref --format='%(refname:short)%09%(upstream)' refs/heads/)
fi
(( deleted == 0 )) && disp I "No branches to delete."
unset -f _gprune_is_protected
# "if" keeps the exit status at 0 when branches were deleted
if (( deleted == 0 )); then
disp I "No branches to delete."
fi
}
export -f gprune
# ------------------------------------------------------------------------------
@@ -647,9 +730,8 @@ groot()
return 1
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
local do_go=0
while true; do
case "$1" in