fun bug fix
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user