add git helpers
This commit is contained in:
497
profile.d/git.sh
Executable file
497
profile.d/git.sh
Executable file
@@ -0,0 +1,497 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Built-in defaults (can be overridden from [git] section in profile.conf)
|
||||
: "${GIT_MAIN_BRANCH:=main}"
|
||||
: "${GIT_DEFAULT_REMOTE:=origin}"
|
||||
: "${GIT_WIP_PREFIX:=wip}"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Internal helper: ensure git is available and cwd is a git worktree
|
||||
_git_require_repo()
|
||||
{
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
disp E "git command not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
disp E "Current directory is not inside a git repository."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Internal helper: return default branch from remote HEAD, fallback to config
|
||||
_git_default_branch()
|
||||
{
|
||||
local remote="${1:-$GIT_DEFAULT_REMOTE}"
|
||||
local head
|
||||
|
||||
head=$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null) || true
|
||||
if [[ -n $head ]]; then
|
||||
printf "%s\n" "${head#${remote}/}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%s\n" "$GIT_MAIN_BRANCH"
|
||||
return 0
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display compact git status + branch tracking information
|
||||
# Usage: gst [path]
|
||||
gst()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gst' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gst --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gst: Display short git status and branch tracking info.\n"
|
||||
printf "Usage: gst [path]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gst --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local target="${1:-.}"
|
||||
git -C "$target" status --short --branch
|
||||
}
|
||||
export -f gst
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Show a readable commit graph
|
||||
# Usage: ggraph [-n limit]
|
||||
ggraph()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hn: --long help,limit: -n 'ggraph' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ggraph --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
local limit=30
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ggraph: Display decorated git history graph.\n"
|
||||
printf "Usage: ggraph [-n limit]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-n, --limit\tNumber of commits to display (default: 30)\n"
|
||||
return 0
|
||||
;;
|
||||
-n|--limit)
|
||||
limit="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ggraph --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $limit =~ ^[0-9]+$ ]] || {
|
||||
disp E "Invalid limit: must be a positive integer."
|
||||
return 1
|
||||
}
|
||||
|
||||
_git_require_repo || return 1
|
||||
git log --graph --decorate --oneline --all --max-count="$limit"
|
||||
}
|
||||
export -f ggraph
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sync current branch with remote (fetch + rebase)
|
||||
# Usage: gsync [remote]
|
||||
gsync()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gsync' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gsync --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gsync: Fetch and rebase current branch onto its remote tracking branch.\n"
|
||||
printf "Usage: gsync [remote]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gsync --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_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
|
||||
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
|
||||
|
||||
disp I "Fetching from $remote..."
|
||||
git fetch --prune "$remote" || return 1
|
||||
|
||||
if [[ -z $upstream ]]; then
|
||||
disp W "No upstream configured for $branch, skipping rebase."
|
||||
disp I "Set one with: git branch --set-upstream-to ${remote}/${branch} ${branch}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
disp I "Rebasing $branch onto $upstream..."
|
||||
git rebase "$upstream"
|
||||
}
|
||||
export -f gsync
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Reset local branch to exact upstream state (stash local changes first)
|
||||
# Usage: greset [target]
|
||||
greset()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hx --long help,with-ignored -n 'greset' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"greset --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
local clean_ignored=0
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
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 "\n"
|
||||
printf "Default target is current branch upstream (@{u}).\n"
|
||||
printf "If no upstream exists, fallback target is <remote>/<branch>.\n"
|
||||
printf "This command stashes local modifications (tracked + untracked),\n"
|
||||
printf "drops local unpushed commits by hard-reset, and cleans untracked files.\n"
|
||||
return 0
|
||||
;;
|
||||
-x|--with-ignored)
|
||||
clean_ignored=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"greset --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local branch upstream target remote old_head stash_msg stash_out stash_created=0 dropped=0
|
||||
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"
|
||||
|
||||
if [[ -z $target ]]; then
|
||||
if [[ -n $upstream ]]; then
|
||||
target="$upstream"
|
||||
else
|
||||
remote="$GIT_DEFAULT_REMOTE"
|
||||
target="${remote}/${branch}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z $remote ]]; then
|
||||
remote="${target%%/*}"
|
||||
fi
|
||||
|
||||
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
|
||||
stash_msg="greset:${branch}:$(date +'%Y-%m-%d %H:%M:%S')"
|
||||
disp I "Stashing local changes as '$stash_msg'..."
|
||||
stash_out=$(git stash push -u -m "$stash_msg" 2>&1) || {
|
||||
disp E "Failed to stash local changes."
|
||||
printf "%s\n" "$stash_out"
|
||||
return 1
|
||||
}
|
||||
[[ $stash_out != "No local changes to save"* ]] && stash_created=1
|
||||
fi
|
||||
|
||||
disp I "Fetching from $remote..."
|
||||
git fetch --prune "$remote" || return 1
|
||||
|
||||
if ! git rev-parse --verify --quiet "$target" >/dev/null; then
|
||||
disp E "Target '$target' does not exist."
|
||||
return 1
|
||||
fi
|
||||
|
||||
dropped=$(git rev-list --count "${target}..${old_head}" 2>/dev/null || printf "0")
|
||||
|
||||
disp W "Hard-resetting $branch to $target..."
|
||||
git reset --hard "$target" || return 1
|
||||
|
||||
if (( clean_ignored )); then
|
||||
git clean -fdx || return 1
|
||||
else
|
||||
git 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"
|
||||
return 0
|
||||
}
|
||||
export -f greset
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Create a quick WIP commit for local checkpointing
|
||||
# Usage: gwip [message]
|
||||
gwip()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gwip' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gwip --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gwip: Create a local checkpoint commit with all tracked/untracked changes.\n"
|
||||
printf "Usage: gwip [message]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gwip --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local msg
|
||||
if [[ $# -gt 0 ]]; then
|
||||
msg="$*"
|
||||
else
|
||||
msg="$GIT_WIP_PREFIX: $(date +'%Y-%m-%d %H:%M:%S')"
|
||||
fi
|
||||
|
||||
git add -A || return 1
|
||||
git commit -m "$msg"
|
||||
}
|
||||
export -f gwip
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Delete merged local branches (except protected branches)
|
||||
# Usage: gprune [main-branch]
|
||||
gprune()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gprune' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gprune --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gprune: Delete local branches already merged into main branch.\n"
|
||||
printf "Usage: gprune [main-branch]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gprune --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local base="${1:-$(_git_default_branch "$GIT_DEFAULT_REMOTE")}" current deleted=0
|
||||
current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
|
||||
|
||||
disp I "Pruning branches merged into $base..."
|
||||
|
||||
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 && {
|
||||
printf "Deleted: %s\n" "$b"
|
||||
((deleted++))
|
||||
}
|
||||
done < <(git branch --merged "$base" | sed -E 's/^\*?\s*//')
|
||||
|
||||
(( deleted == 0 )) && disp I "No merged branches to delete."
|
||||
}
|
||||
export -f gprune
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Print repository root path
|
||||
# Usage: groot
|
||||
groot()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hg --long help,go -n 'groot' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"groot --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
eval set -- "$PARSED"
|
||||
local do_go=0
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "groot: Display the absolute path of the current repository root.\n"
|
||||
printf "Usage: groot [-g|--go]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-g, --go\tChange current directory to repository root\n"
|
||||
return 0
|
||||
;;
|
||||
-g|--go)
|
||||
do_go=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"groot --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
local root
|
||||
root=$(git rev-parse --show-toplevel) || return 1
|
||||
|
||||
if (( do_go )); then
|
||||
cd "$root" || {
|
||||
disp E "Failed to move to repository root: $root"
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%s\n" "$root"
|
||||
}
|
||||
export -f groot
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
load_conf git
|
||||
|
||||
# EOF
|
||||
@@ -53,6 +53,13 @@ help()
|
||||
printf "finddead\tFind dead symbolic links in the given or current directory\n"
|
||||
printf "findzero\tFind empty files in the given or current directory\n"
|
||||
printf "genpwd\t\tGenerate one or more random secure passwords with configurable constraints\n"
|
||||
printf "ggraph\t\tDisplay decorated git history graph\n"
|
||||
printf "gprune\t\tDelete local branches already merged into main branch\n"
|
||||
printf "greset\t\tReset branch to upstream (stash local, drop local commits)\n"
|
||||
printf "groot\t\tDisplay repository root path (or cd to it with -g)\n"
|
||||
printf "gsync\t\tFetch and rebase current branch onto upstream\n"
|
||||
printf "gst\t\tDisplay short git status and branch tracking info\n"
|
||||
printf "gwip\t\tCreate a quick WIP checkpoint commit\n"
|
||||
printf "gpid\t\tGive the list of PIDs matching the given process name(s)\n"
|
||||
printf "isipv4\t\tTell if the given parameter is a valid IPv4 address\n"
|
||||
printf "isipv6\t\tTell if the given parameter is a valid IPv6 address\n"
|
||||
|
||||
Reference in New Issue
Block a user