Add configuration for git

- Add .config/git/config
  + Define aliases
  + Configure default behavior in various situations
  + Add user information with signing key
- Add .config/git/commit_msg_template.txt
- Add .config/git/ignore
- Integrate `git` aliases into the shell
This commit is contained in:
Alexander Hess 2026-06-11 18:32:27 +02:00
commit 721c2fd905
Signed by: alexander
GPG key ID: B46EDB4DFC26805D
21 changed files with 1122 additions and 66 deletions

35
.local/bin/git-aliases Executable file
View file

@ -0,0 +1,35 @@
#!/bin/sh
# List the names of all `git` aliases, one per line
#
# With the "--real" flag, only the "real" aliases are listed,
# which are those with less than 7 characters in their name
# as defined under "[alias]" in ~/.config/git/config
#
# Usage: git aliases [--real]
set -eu
_max_length=999
for arg in "$@"; do
case "$arg" in
--real)
_max_length=7
;;
*)
echo 'Usage: git aliases [--real]' >&2
exit 2
;;
esac
done
git --list-cmds=alias \
| awk -v max="$_max_length" 'length($0) < max' \
| sort

View file

@ -0,0 +1,15 @@
#!/bin/sh
# List the names of all `git` aliases and custom sub-commands, one per line
#
# Usage: git aliases-n-sub-commands
set -eu
{
git --list-cmds=alias
git --list-cmds=others
} | sort -u

93
.local/bin/git-cat-all Executable file
View file

@ -0,0 +1,93 @@
#!/bin/sh
# Print the contents of all tracked text files to stdout,
# each preceded by a header with its filename
#
# With "-a", binary files are included as well
#
# With "-m", only files with unstaged modifications are printed
#
# With "-n", the headers are omitted
#
# Usage: git cat-all [-a] [-m] [-n] [<pathspec> ...]
set -eu
# Error if we are not in a `git` repository
git rev-parse --git-dir >/dev/null
# Without any commit, there is nothing to print reliably
git rev-parse --verify --quiet HEAD >/dev/null || {
echo "fatal: no commits yet" >&2
exit 1
}
_textonly=yes
_lsflags=
_cmd='printf "\n\n===== %s =====\n\n" "{}"; cat "{}"'
_headers=yes
while [ $# -gt 0 ]; do
case $1 in
-a) _textonly=no
shift
;;
-m) _lsflags=-m
shift
;;
-n) _cmd='printf "\n\n"; cat "{}"'
_headers=no
shift
;;
--) shift
break
;;
-*) echo "Usage: git cat-all [-a] [-m] [-n] [<pathspec> ...]" >&2
exit 2
;;
*) break
;;
esac
done
# Because `git` runs scripts on the `$PATH` in the current directory
cd "$(git rev-parse --show-toplevel)"
# An empty file counts as text
# => `grep` alone would misjudge it
# because it never matches in a file without any lines
if [ "$_textonly" = "yes" ]; then
_cmd='if [ ! -s "{}" ] || grep -Iq "" "{}"; then '"$_cmd"'; fi'
fi
# `git ls-files -m` also lists files deleted from the working tree
# => Skip them because `cat` would fail
if [ -n "$_lsflags" ]; then
_cmd='if [ -f "{}" ]; then '"$_cmd"'; fi'
fi
if [ "$_headers" = "yes" ]; then
printf "Repository: %s\n" "$(basename "$PWD")"
printf "Branch: %s @ %s\n" \
"$(git rev-parse --abbrev-ref HEAD)" \
"$(git rev-parse --short HEAD)"
fi
git ls-files \
-z $_lsflags \
-- \
"$@" \
| xargs -0 -I{} sh -c "$_cmd"

8
.local/bin/git-checkout-dev Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
# Check out the "develop" branch, whatever it is called
set -eu
exec git checkout-first-existing-branch dev develop development

View file

@ -0,0 +1,28 @@
#!/bin/sh
# Check out the first of the given branches that exists,
# looking at both local branches and the ones on `origin`
#
# Usage: git checkout-first-existing-branch <branch> [<branch> ...]
set -eu
if [ $# -eq 0 ]; then
echo "Usage: git checkout-first-existing-branch <branch> [<branch> ...]" >&2
exit 2
fi
for _branch in "$@"; do
if git show-ref --verify --quiet "refs/heads/$_branch" \
|| git show-ref --verify --quiet "refs/remotes/origin/$_branch"; then
exec git checkout "$_branch"
fi
done
echo "None of these branches exists: $*" >&2
exit 1

8
.local/bin/git-checkout-main Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
# Check out the "main" branch, whatever it is called
set -eu
exec git checkout-first-existing-branch main master

54
.local/bin/git-clean-all Executable file
View file

@ -0,0 +1,54 @@
#!/bin/sh
# Reset the working directory to `HEAD` and remove everything not tracked,
# except the files listed in $KEEP in ~/.local/bin/git-clean-ignored
#
# This is irreversible, so the changes are shown first and confirmed,
# unless "--force" is passed
#
# Usage: git clean-all [--force]
set -eu
# `git` runs scripts on the `$PATH` in the current directory,
# whereas it runs "!" aliases at the top level of the repository
cd "$(git rev-parse --show-toplevel)"
case "${1:-}" in
--force) _forced=1 ;;
"") _forced=0 ;;
*) echo "Usage: git clean-all [--force]" >&2; exit 2 ;;
esac
if [ "$_forced" -ne 1 ]; then
echo "Tracked files to be reset to HEAD:"
git diff --stat HEAD || true
echo ""
echo "Untracked and ignored files to be removed:"
# Keep the "-e" flags in sync with the other two copies:
# - the "-e" flags of `clean-untracked` in ~/.config/git/config
# - `$KEEP` in ~/.local/bin/git-clean-ignored
git clean -x -d -n -e ".python-version" -e ".env"
echo ""
printf "This CANNOT be undone. Continue? [y/N] "
read -r _answer || _answer=""
case "$_answer" in
[yY] | [yY][eE][sS]) ;;
*) echo "Aborted" >&2; exit 1 ;;
esac
fi
git reset --hard
git clean-ignored
git clean-untracked

46
.local/bin/git-clean-ignored Executable file
View file

@ -0,0 +1,46 @@
#!/bin/sh
# Remove all files ignored by `git`, except the ones listed in $KEEP below
#
# `git clean` does not accept "-e" together with "-X",
# so the files to be kept are moved aside and then restored
#
# Usage: git clean-ignored [<git clean options>]
set -eu
# Keep this list in sync with the other two copies:
# - the "-e" flags of `clean-untracked` in ~/.config/git/config
# - the preview in ~/.local/bin/git-clean-all
KEEP=".env .python-version"
SUFFIX=".git-clean-ignored.bak"
# `git` runs scripts on the `$PATH` in the current directory,
# whereas it runs "!" aliases at the top level of the repository
cd "$(git rev-parse --show-toplevel)"
_restore() {
for _file in $KEEP; do
if [ -f "$_file$SUFFIX" ]; then
mv "$_file$SUFFIX" "$_file"
fi
done
}
# Restore the kept files even if `git clean` fails or the user aborts
trap _restore EXIT HUP INT TERM
for _file in $KEEP; do
if [ -f "$_file" ]; then
mv "$_file" "$_file$SUFFIX"
fi
done
git clean -X -d -f "$@"

32
.local/bin/git-ff-all Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
# Fast-forward all local branches that track a remote branch
#
# Branches with local-only commits are skipped
#
# Usage: git ff-all
set -eu
git fetch --all --prune
current="$(git branch --show-current)"
git for-each-ref --format='%(refname:short) %(upstream:short)' refs/heads/ |
while read -r branch upstream; do
[ -n "$upstream" ] || continue
if [ "$branch" = "$current" ]; then
git merge --ff-only "$upstream" 2>/dev/null \
|| echo "Skipped $branch (not a fast forward)"
continue
fi
git fetch . "$upstream:$branch" 2>/dev/null \
|| echo "Skipped $branch (not a fast forward)"
done

18
.local/bin/git-gc-everything Executable file
View file

@ -0,0 +1,18 @@
#!/bin/sh
# Delete EVERYTHING not reachable from a branch from the repository
#
# This expires the reflog as well!
#
# Usage: git gc-everything [<git gc options>]
set -eu
exec git \
-c gc.pruneExpire=now \
-c gc.reflogExpire=0 \
-c gc.reflogExpireUnreachable=0 \
-c gc.rerereresolved=0 \
-c gc.rerereunresolved=0 \
gc "$@"

35
.local/bin/git-match-remote Executable file
View file

@ -0,0 +1,35 @@
#!/bin/sh
# Make all local branches match the remote branch they track,
# discarding local-only commits and, on the current branch,
# uncommitted changes as well
#
# Branches without an upstream are left alone
#
# Usage: git match-remote
set -eu
git fetch --all --prune
current="$(git branch --show-current)"
git for-each-ref --format='%(refname:short) %(upstream:short)' refs/heads/ |
while read -r branch upstream; do
[ -n "$upstream" ] || continue
git rev-parse --verify --quiet "$upstream" >/dev/null || continue
if [ "$branch" = "$current" ]; then
git reset --hard "$upstream"
continue
fi
git branch -f "$branch" "$upstream"
echo "Moved $branch to $upstream"
done

21
.local/bin/git-point-branch-at Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
# Move a branch to a commit, whether or not it is checked out
#
# Usage: git point-branch-at [<branch>] <commit>
set -eu
case $# in
1) _branch=$(git rev-parse --abbrev-ref HEAD); _commit=$1 ;;
2) _branch=$1; _commit=$2 ;;
*) echo "Usage: git point-branch-at [<branch>] <commit>" >&2; exit 2 ;;
esac
if [ "$_branch" = "$(git rev-parse --abbrev-ref HEAD)" ]; then
exec git reset --keep "$_commit"
else
exec git branch --force "$_branch" "$_commit"
fi

23
.local/bin/git-prune-delete Executable file
View file

@ -0,0 +1,23 @@
#!/bin/sh
# Delete the local branches whose upstream branch was deleted on the remote
#
# `git branch --delete` without "--force" is used,
# so branches holding unmerged commits are kept
#
# Usage: git prune-delete
set -eu
_branches=$(git prune-show)
if [ -z "$_branches" ]; then
echo "No branches to prune"
exit 0
fi
# shellcheck disable=SC2086 # because branch names never contain whitespace
git branch --delete $_branches

15
.local/bin/git-prune-show Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
# Show the local branches whose upstream branch was deleted on the remote
#
# Usage: git prune-show
set -eu
git fetch --prune
git branch \
--format='%(refname:short) %(upstream:track)' \
--list \
| awk '$2 == "[gone]" { print $1 }'