From 721c2fd905abd3d1979baf75b073377603ff2164 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Thu, 11 Jun 2026 18:32:27 +0200 Subject: [PATCH] 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 --- .bashrc | 63 -- .config/git/README.md | 10 + .config/git/commit_msg_template.txt | 17 + .config/git/config | 626 ++++++++++++++++++ .config/git/ignore | 6 + .config/shell/aliases | 31 +- .local/bin/git-aliases | 35 + .local/bin/git-aliases-n-sub-commands | 15 + .local/bin/git-cat-all | 93 +++ .local/bin/git-checkout-dev | 8 + .local/bin/git-checkout-first-existing-branch | 28 + .local/bin/git-checkout-main | 8 + .local/bin/git-clean-all | 54 ++ .local/bin/git-clean-ignored | 46 ++ .local/bin/git-ff-all | 32 + .local/bin/git-gc-everything | 18 + .local/bin/git-match-remote | 35 + .local/bin/git-point-branch-at | 21 + .local/bin/git-prune-delete | 23 + .local/bin/git-prune-show | 15 + .zshrc | 4 + 21 files changed, 1122 insertions(+), 66 deletions(-) create mode 100644 .config/git/README.md create mode 100644 .config/git/commit_msg_template.txt create mode 100644 .config/git/config create mode 100644 .config/git/ignore create mode 100755 .local/bin/git-aliases create mode 100755 .local/bin/git-aliases-n-sub-commands create mode 100755 .local/bin/git-cat-all create mode 100755 .local/bin/git-checkout-dev create mode 100755 .local/bin/git-checkout-first-existing-branch create mode 100755 .local/bin/git-checkout-main create mode 100755 .local/bin/git-clean-all create mode 100755 .local/bin/git-clean-ignored create mode 100755 .local/bin/git-ff-all create mode 100755 .local/bin/git-gc-everything create mode 100755 .local/bin/git-match-remote create mode 100755 .local/bin/git-point-branch-at create mode 100755 .local/bin/git-prune-delete create mode 100755 .local/bin/git-prune-show diff --git a/.bashrc b/.bashrc index 40da8c4..f741755 100644 --- a/.bashrc +++ b/.bashrc @@ -69,7 +69,6 @@ PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' # Enable tab completion - if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then . /usr/share/bash-completion/bash_completion @@ -78,68 +77,6 @@ if ! shopt -oq posix; then fi fi -# Add tab completion for all aliases to commands with completion functions -# (must come after bash completions have been set up) -# Source: https://superuser.com/a/437508 -_alias_completion() { - local namespace="alias_completion" - # parse function based completion definitions, where capture group 2 => function and 3 => trigger - local compl_regex='complete( +[^ ]+)* -F ([^ ]+) ("[^"]+"|[^ ]+)' - # parse alias definitions, where capture group 1 => trigger, 2 => command, 3 => command arguments - local alias_regex="alias ([^=]+)='(\"[^\"]+\"|[^ ]+)(( +[^ ]+)*)'" - # create array of function completion triggers, keeping multi-word triggers together - eval "local completions=($(complete -p | sed -Ene "/$compl_regex/s//'\3'/p"))" - (( ${#completions[@]} == 0 )) && return 0 - # create temporary file for wrapper functions and completions - rm -f "/tmp/${namespace}-*.tmp" # preliminary cleanup - local tmp_file; tmp_file="$(mktemp "/tmp/${namespace}-${RANDOM}XXX.tmp")" || return 1 - local completion_loader; completion_loader="$(complete -p -D 2>/dev/null | sed -Ene 's/.* -F ([^ ]*).*/\1/p')" - # read in " '' ''" lines from defined aliases - local line; while read line; do - eval "local alias_tokens; alias_tokens=($line)" 2>/dev/null || continue # some alias arg patterns cause an eval parse error - local alias_name="${alias_tokens[0]}" alias_cmd="${alias_tokens[1]}" alias_args="${alias_tokens[2]# }" - # skip aliases to pipes, boolean control structures and other command lists - # (leveraging that eval errs out if $alias_args contains unquoted shell metacharacters) - eval "local alias_arg_words; alias_arg_words=($alias_args)" 2>/dev/null || continue - # avoid expanding wildcards - read -a alias_arg_words <<< "$alias_args" - # skip alias if there is no completion function triggered by the aliased command - if [[ ! " ${completions[*]} " =~ " $alias_cmd " ]]; then - if [[ -n "$completion_loader" ]]; then - # force loading of completions for the aliased command - eval "$completion_loader $alias_cmd" - # 124 means completion loader was successful - [[ $? -eq 124 ]] || continue - completions+=($alias_cmd) - else - continue - fi - fi - local new_completion="$(complete -p "$alias_cmd")" - # create a wrapper inserting the alias arguments if any - if [[ -n $alias_args ]]; then - local compl_func="${new_completion/#* -F /}"; compl_func="${compl_func%% *}" - # avoid recursive call loops by ignoring our own functions - if [[ "${compl_func#_$namespace::}" == $compl_func ]]; then - local compl_wrapper="_${namespace}::${alias_name}" - echo "function $compl_wrapper { - (( COMP_CWORD += ${#alias_arg_words[@]} )) - COMP_WORDS=($alias_cmd $alias_args \${COMP_WORDS[@]:1}) - (( COMP_POINT -= \${#COMP_LINE} )) - COMP_LINE=\${COMP_LINE/$alias_name/$alias_cmd $alias_args} - (( COMP_POINT += \${#COMP_LINE} )) - $compl_func - }" >> "$tmp_file" - new_completion="${new_completion/ -F $compl_func / -F $compl_wrapper }" - fi - fi - # replace completion trigger by alias - new_completion="${new_completion% *} $alias_name" - echo "$new_completion" >> "$tmp_file" - done < <(alias -p | sed -Ene "s/$alias_regex/\1 '\2' '\3'/p") - . "$tmp_file" && \rm -f "$tmp_file" -}; _alias_completion - # Jump to frequently used directories with `z ` if _command_exists zoxide; then diff --git a/.config/git/README.md b/.config/git/README.md new file mode 100644 index 0000000..bf7436f --- /dev/null +++ b/.config/git/README.md @@ -0,0 +1,10 @@ +# `git`-related Configurations + +This folder contains + the [config](./config), + [global ignores](./ignore), and + the [commit message template](./commit_msg_template.txt) + for [`git`](https://git-scm.com/). + +Machine-local settings (e.g., signing keys) + go into an untracked `config_local`. diff --git a/.config/git/commit_msg_template.txt b/.config/git/commit_msg_template.txt new file mode 100644 index 0000000..5e556d9 --- /dev/null +++ b/.config/git/commit_msg_template.txt @@ -0,0 +1,17 @@ +# SUBJECT -> What does the commit do? +# - Imperative mood, no "." at the end +# - Start with "Add", "Fix", "Make", "Remove", ... +#---------- 50 characters / 1 line ---------------| + + +# BODY -> The why and how +# - Use plain text without formatting +# + Use "-" and "+" as bullets +# - "See: URL" to link to external resources +#---------- 72 characters / multiple lines -----------------------------| + + +# ISSUE TRACKER -> Uncomment one of the lines below +# Closes #41 +# Fixes #42 +# Resolves #43 diff --git a/.config/git/config b/.config/git/config new file mode 100644 index 0000000..e60e973 --- /dev/null +++ b/.config/git/config @@ -0,0 +1,626 @@ +# This config requires `git` v2.40+ +# (e.g., Debian 12's v2.39 is too old) +# +# The most recent features used are: +# +# v2.40 help.autocorrect with "prompt" +# v2.38 rebase.updateRefs +# v2.37 push.autoSetupRemote +# v2.35 merge.conflictstyle with "zdiff3" +# +# Older `git` versions do NOT fail on all of these +# but may warn about some unknown values + + + +[alias] + +# `git` commands are defined in three places: +# +# 1) Plain abbreviations and short "!" one-liners +# are defined in this file +# +# 2) ~/.local/bin/ holds `git-*` utilities +# for everything with control flow or an error path, +# which `git` registers as sub-commands automatically +# +# 3) ~/.config/shell/aliases defines short form aliases for the shell, +# for example, `gap` for `git add --patch` +# +# Below, the aliases are grouped by task: +# Full-name aliases come first in each group, +# followed by short abbreviations +# +# Tab complete works out of the box in `bash`, +# whereas a `zstyle` line in ~/.zshrc makes that work for `zsh` +# +# Run: +# - `git aliases` for just 1) +# - `git aliases-n-sub-commands` for 1) and 2) +# - `git help -a` for ALL sub-commands, built-in and custom + + +# Inspect the repository +# +# See also this sub-command in ~/.local/bin: +# +# git-cat-all + +current-branch = rev-parse --abbrev-ref HEAD +project-root = rev-parse --show-toplevel + +# List all authors with their commit counts +contributors = shortlog --summary --numbered --email + +# Ignore whitespace and follow lines moved or copied between files +blame-follow = blame -w -C -C -C + +bi = bisect +bl = blame-follow +rl = reflog +sh = show + +s = status +ss = status --short + + +# Stage changes & sync the working directory into the index + +sync = !git sync-deleted && git add . --all +sync-deleted = !git ls-files -z --deleted | xargs -r -0 git rm + +a = add +aa = add --all +ap = add --patch + +# No-ops so that the `gmv` and `grm` aliases are created +mv = mv +rm = rm + + +# Commit changes + +# Keep the commit message and skip the editor +amend-commit = commit --amend --no-edit + +ci = commit +cia = commit --amend +cian = amend-commit +cif = commit --fixup +cim = commit --message + + +# Undo things, named as "specified" in the `git status` header + +discard = checkout -- +uncommit = reset --soft HEAD~1 +unstage = reset HEAD -- + +rev = revert + +rh = reset +rhh = reset --hard +rhs = reset --soft + +rs = restore +rst = restore --staged + +u = uncommit + + +# Manage tags + +de = describe --tags + +ta = tag +taa = tag --annotate +tad = tag --delete +tal = tag --list + + +# Make minimal diff the default + +diff-minimal = diff --color-words=. --ws-error-highlight=all +diff-last-commit = diff --color-words=. --ws-error-highlight=all HEAD +diff-staged = diff --color-words=. --ws-error-highlight=all --staged + +d = diff-minimal +dlc = diff-last-commit +ds = diff-staged + + +# Make the logs look nice by default + +log-format1 = log --pretty='%C(auto)%h: %s%d%Creset%n%C(dim)%an @ %ad => %ar%n' --date=format:'%a %Y-%m-%d %H:%M:%S %z' +log-format2 = log --pretty='%C(auto)%h: %s%d%Creset%n%C(dim)%aN @ %ad => %ar%n%+b' --date=format:'%a %Y-%m-%d %H:%M:%S %z' + +history = log-format2 --graph +summary = log-format1 --graph +oneline = log --pretty='%C(auto)%h: %s%d' --graph + +last-commit = log-format2 -1 -p --stat + +lg = log +lga = log --all + +hi = history +hia = history --all + +su = summary +sua = summary --all + +ol = oneline +ola = oneline --all + +lc = last-commit + + +# Search the history of specific files/paths + +# Follow a single file through renames +history-file = log-format2 --graph --follow -- +history-file-diff = log-format2 --graph --follow -p --stat -- + +# Show all commits touching a path (e.g., a folder) +history-path = log-format2 --graph -- +history-path-diff = log-format2 --graph -p --stat -- + +hif = history-file +hifd = history-file-diff + +hip = history-path +hipd = history-path-diff + + +# Search the repository + +# Search the tracked files for source code, +# showing the enclosing function of each match +grep-code = grep --break --context 1 --full-name --heading --line-number --show-function + +# Search the commit messages across all branches +grep-commit-msg = log --all --regexp-ignore-case --pretty='%C(auto)%h: %s%+D%Creset%n%C(reverse)%C(dim)%aN @ %ad = %ar%Creset%n%+b' --date=format:'%a %Y-%m-%d %H:%M:%S' --grep + +# Find commits whose diff contains added or removed lines +# matching a regex, with or without the patches shown +grep-diff = log-format1 -p -G +grep-diff-list = log-format1 -G + +# Find commits that change the NUMBER of occurrences of a regex, +# which ignores lines merely moved around (unlike grep-diff) +grep-pickaxe = log-format1 -p --pickaxe-regex -S + +# Search the tracked files for plain text, ignoring case +grep-text = grep --break --context 1 --full-name --heading --ignore-case --line-number + +repc = grep-code +repcim = grep-commit-msg +repd = grep-diff +repdl = grep-diff-list +repp = grep-pickaxe +rept = grep-text + + +# Manage the branches & move between them +# +# See also these sub-commands in ~/.local/bin: +# +# git-checkout-dev +# git-checkout-main +# git-checkout-first-existing-branch +# +# git-point-branch-at +# +# git-prune-delete +# git-prune-show + +# `git branch` sorts by recency (see [branch] below) +# => This alias restores the alphabetical listing +branch-alpha = branch --sort=refname + +rename-branch = branch --move + +br = branch +bra = branch --all + +brd = branch --delete +brdd = branch --delete --force + +brm = rename-branch +brp = point-branch-at + +# Which branches are (not) fully contained in `HEAD` +brme = branch --all --merged +brnome = branch --all --no-merged + +co = checkout +cob = checkout -b +cod = checkout-dev +com = checkout-main + +sw = switch +swc = switch --create + + +# Integrate changes from other branches + +cp = cherry-pick + +mb = merge-base + +me = merge +mea = merge --abort +mec = merge --continue +meff = merge --ff-only +menoff = merge --no-ff + +rb = rebase --committer-date-is-author-date +rba = rebase --abort +rbc = rebase --continue +rbi = rebase --interactive # --autosquash is added in [rebase] below +rbia = rebase --interactive --autosquash +rbq = rebase --quit +rbs = rebase --skip + + +# Exchange changes with the remotes +# +# See also these sub-commands in ~/.local/bin: +# +# git-ff-all +# git-match-remote + +cl = clone + +fe = fetch # --prune is added in +fea = fetch --all # [fetch] below +fep = fetch --prune +fepa = fetch --prune --all + +ff = ff-all +fff = match-remote # ~ fast-forward with force + +pl = pull +plrb = pull --rebase + +ps = push +psf = push --force-with-lease +psff = push --force + +re = remote --verbose + + +# Put changes aside temporarily + +st = stash +sta = stash push --include-untracked +stam = stash push --include-untracked --message +stapp = stash apply +stl = stash list +stp = stash pop +stsh = stash show + + +# Work with several working directories + +wt = worktree +wta = worktree add +wtl = worktree list +wtm = worktree move +wtp = worktree prune +wtr = worktree remove + + +# Clean the project folder with intuitive commands +# +# See also these sub-commands in ~/.local/bin: +# +# git-clean-all +# git-clean-ignored +# +# git-gc-everything + +# Keep this "-e" list in sync with the other two copies: +# - `$KEEP` in ~/.local/bin/git-clean-ignored +# - the preview in ~/.local/bin/git-clean-all +clean-untracked = clean -x -d -e ".python-version" -e ".env" -f # because the "-e" flag does not work with "-X" + +cln = clean --interactive + + + +[branch] + +# `git branch` lists the most recently used branches first +sort = -committerdate + + + +[clean] + +# Refuse to run `git clean` without --force or --dry-run +requireforce = true + + + +[column] + +# Print listings (e.g., branches) in columns +ui = auto + + + +[color "branch"] + +current = cyan dim bold reverse +local = green bold +remote = cyan bold + + +[color "decorate"] + +HEAD = cyan dim bold reverse +branch = green bold +remoteBranch = cyan bold +stash = magenta dim bold reverse +tag = magenta bold + + +[color "diff"] + +context = white +frag = blue dim bold reverse +meta = yellow dim bold reverse +new = green bold +old = red bold +whitespace = red dim bold reverse + +# Lines that are merely moved around (see "diff.colorMoved" below) +newMoved = cyan bold +oldMoved = magenta bold + +# Alternation shades to tell adjacent moved blocks apart (~ "zebra") +newMovedAlternative = cyan +oldMovedAlternative = magenta + + +[color "grep"] + +context = white +filename = yellow dim bold reverse +function = white bold +linenumber = blue dim bold reverse +match = yellow bold +selected = white +separator = blue dim bold reverse + + +[color "interactive"] + +error = red dim bold reverse +header = white +help = blue bold +prompt = white dim bold reverse + + +[color "status"] + +added = green bold +branch = cyan dim bold reverse +changed = yellow bold +header = white +localBranch = green bold +nobranch = red dim bold reverse +remoteBranch = cyan bold +unmerged = red dim bold reverse +untracked = blue bold + + + +[commit] + +# Set this in ~/.config/git/config_local to enable signing +# +# gpgSign = true + +# Strip comment lines and excess whitespace from the message +cleanup = strip + +template = ~/.config/git/commit_msg_template.txt + +# Show the staged diff below the commit message template in the editor +verbose = true + + + +[core] + +excludesfile = ~/.config/git/ignore +pager = less --chop-long-lines --ignore-case --LONG-PROMPT --no-init --quit-if-one-screen --RAW-CONTROL-CHARS --status-column + +# Speed up `git status` by caching untracked files +untrackedCache = true + +# Flag tabs in the indentation but allow spaces before tabs +whitespace = -space-before-tab,tab-in-indent + + + +[diff] + +# Better diff hunks for reordered code than the default "myers" +algorithm = histogram + +# Color lines that are merely moved around differently +# from actually added/removed ones +# => "default" is a synonym for the "zebra" mode +colorMoved = default + +# Show "i/" (index) and "w/" (working tree) etc. instead of "a/" and "b/" +mnemonicPrefix = true + +# Detect renamed files instead of showing delete-plus-add +renames = true + +# Show submodule changes as their list of commits +submodule = log + + + +[fetch] + +# Auto-remove remote-tracking refs deleted on the remote +prune = true + + + +[help] + +# Suggest a command on a typo and ask before running it +autocorrect = prompt + + + +[init] + +defaultBranch = main + + + +[merge] + +# Show the common ancestor's version inside conflict markers, +# minus the lines that both sides agree on +conflictstyle = zdiff3 + +# Refuse `git merge` if a merge commit would be needed +# => Use `git menoff` to create one on purpose +ff = only + + + +[pull] + +# Replay local commits on top of the fetched branch +# and do not create merge +rebase = true + +# Guard: A merge-style pull must be a fast-forward +ff = only + + + +[push] + +# Auto-create the upstream on the first push of a new branch +autoSetupRemote = true + +# Push the current branch to its upstream +default = upstream + +# Refuse to push if a submodule's commits are unpushed +recursesubmodules = check + + + +[rebase] + +# Auto-apply "fixup!" commits in every interactive rebase +autosquash = true + +# Stash away and restore local changes around a rebase +autostash = true + +# Also move stacked branches that point into the rebased range +updateRefs = true + + + +[rerere] + +# Record conflict resolutions and reuse them when they reappear +enabled = true + + + +[tag] + +# Sort "v1.10" after "v1.9" +sort = version:refname + + + +[url "ssh://git@bitbucket.org/"] + +# Enables, for example, `git clone bb:atlassian/aui` +insteadOf = bb: + +# Push always via SSH, even for HTTPS-based remotes +pushInsteadOf = bbh: +pushInsteadOf = https://bitbucket.org/ + +[url "https://bitbucket.org/"] + +# The HTTPS fallback on machines without SSH keys +insteadOf = bbh: + + +[url "ssh://git@github.com/"] + +# Enables, for example, `git clone gh:torvalds/linux` +insteadOf = gh: + +# Push always via SSH, even for HTTPS-based remotes +pushInsteadOf = ghh: +pushInsteadOf = https://github.com/ + +[url "https://github.com/"] + +# The HTTPS fallback on machines without SSH keys +insteadOf = ghh: + + +[url "ssh://git@gitlab.com/"] + +# Enables, for example, `git clone gl:gitlab-org/gitlab` +insteadOf = gl: + +# Push always via SSH, even for HTTPS-based remotes +pushInsteadOf = glh: +pushInsteadOf = https://gitlab.com/ + +[url "https://gitlab.com/"] + +# The HTTPS fallback on machines without SSH keys +insteadOf = glh: + + +[url "ssh://git@git.webartifex.de/"] + +# Enables, for example, `git clone wa:alexander/dotfiles` +insteadOf = wa: + +# Push always via SSH, even for HTTPS-based remotes +pushInsteadOf = wah: +pushInsteadOf = https://code.webartifex.de/ + +[url "https://code.webartifex.de/"] + +# The HTTPS fallback on machines without SSH keys +insteadOf = wah: + + + +[user] + +# Set this in ~/.config/git/config_local to enable committing and signing +# +# name = ... +# email = ... +# signingKey = ... + + + +[include] + +path = ~/.config/git/config_local diff --git a/.config/git/ignore b/.config/git/ignore new file mode 100644 index 0000000..a0ed808 --- /dev/null +++ b/.config/git/ignore @@ -0,0 +1,6 @@ +# Generic temporary files +*.backup +*.bak +*.orig +*.temp +*.tmp diff --git a/.config/shell/aliases b/.config/shell/aliases index 7ac4938..83e61f8 100644 --- a/.config/shell/aliases +++ b/.config/shell/aliases @@ -3,9 +3,6 @@ # Aliases used in all kinds of shells -# Manage the bare `git` repository in ~/ holding the dotfiles -alias dotfiles='git --git-dir=$DOTFILES_DIR --work-tree=$HOME' - forget() { # the current history and restore $HISTFILE from disk @@ -72,3 +69,31 @@ alias la='ls --classify --format=vertical --almost-all' alias lal='ls --classify --format=long --almost-all' alias ll='ls --classify --format=long' alias lla='lal' + + + +if _command_exists git; then + + # Manage the bare `git` repository in ~/ holding the dotfiles + alias dotfiles='git --git-dir=$DOTFILES_DIR --work-tree=$HOME' + + alias g='git' + + # Create "g" prefixed aliases from the "real" aliases + # defined under "[alias]" in ~/.config/git/config, + # for example, `gap` for `git add --patch` + # + # Interactive shells only: This spawns a `git` process, + # which every non-interactive `bash` via `$BASH_ENV` + # (e.g., ":!cmd" in `vi`) would pay on each command + # => There, `g ap` works just as well as `gap` + case $- in + *i*) + for al in $(git aliases --real); do + eval "alias g$al='git $al'" + done + unset al + ;; + esac + +fi diff --git a/.local/bin/git-aliases b/.local/bin/git-aliases new file mode 100755 index 0000000..682d68a --- /dev/null +++ b/.local/bin/git-aliases @@ -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 diff --git a/.local/bin/git-aliases-n-sub-commands b/.local/bin/git-aliases-n-sub-commands new file mode 100755 index 0000000..469bfaf --- /dev/null +++ b/.local/bin/git-aliases-n-sub-commands @@ -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 diff --git a/.local/bin/git-cat-all b/.local/bin/git-cat-all new file mode 100755 index 0000000..ed328d2 --- /dev/null +++ b/.local/bin/git-cat-all @@ -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] [ ...] + +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] [ ...]" >&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" diff --git a/.local/bin/git-checkout-dev b/.local/bin/git-checkout-dev new file mode 100755 index 0000000..c401b78 --- /dev/null +++ b/.local/bin/git-checkout-dev @@ -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 diff --git a/.local/bin/git-checkout-first-existing-branch b/.local/bin/git-checkout-first-existing-branch new file mode 100755 index 0000000..b2e5e39 --- /dev/null +++ b/.local/bin/git-checkout-first-existing-branch @@ -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 [ ...] + +set -eu + + +if [ $# -eq 0 ]; then + echo "Usage: git checkout-first-existing-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 diff --git a/.local/bin/git-checkout-main b/.local/bin/git-checkout-main new file mode 100755 index 0000000..48cc456 --- /dev/null +++ b/.local/bin/git-checkout-main @@ -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 diff --git a/.local/bin/git-clean-all b/.local/bin/git-clean-all new file mode 100755 index 0000000..d914f17 --- /dev/null +++ b/.local/bin/git-clean-all @@ -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 diff --git a/.local/bin/git-clean-ignored b/.local/bin/git-clean-ignored new file mode 100755 index 0000000..b40aca5 --- /dev/null +++ b/.local/bin/git-clean-ignored @@ -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 [] + +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 "$@" diff --git a/.local/bin/git-ff-all b/.local/bin/git-ff-all new file mode 100755 index 0000000..7c1cf83 --- /dev/null +++ b/.local/bin/git-ff-all @@ -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 diff --git a/.local/bin/git-gc-everything b/.local/bin/git-gc-everything new file mode 100755 index 0000000..0a5c4cf --- /dev/null +++ b/.local/bin/git-gc-everything @@ -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 [] + +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 "$@" diff --git a/.local/bin/git-match-remote b/.local/bin/git-match-remote new file mode 100755 index 0000000..4aeb927 --- /dev/null +++ b/.local/bin/git-match-remote @@ -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 diff --git a/.local/bin/git-point-branch-at b/.local/bin/git-point-branch-at new file mode 100755 index 0000000..5f471a1 --- /dev/null +++ b/.local/bin/git-point-branch-at @@ -0,0 +1,21 @@ +#!/bin/sh + +# Move a branch to a commit, whether or not it is checked out +# +# Usage: git point-branch-at [] + +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 [] " >&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 diff --git a/.local/bin/git-prune-delete b/.local/bin/git-prune-delete new file mode 100755 index 0000000..dcb6dcc --- /dev/null +++ b/.local/bin/git-prune-delete @@ -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 diff --git a/.local/bin/git-prune-show b/.local/bin/git-prune-show new file mode 100755 index 0000000..d7cae74 --- /dev/null +++ b/.local/bin/git-prune-show @@ -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 }' diff --git a/.zshrc b/.zshrc index e2b2f24..249a1c1 100644 --- a/.zshrc +++ b/.zshrc @@ -91,6 +91,10 @@ zstyle ':completion:*:messages' format '%d' zstyle ':completion:*:warnings' format 'No matches for: %d' zstyle ':completion:*' group-name '' +# Make `git` sub-commands in ~/.local/bin completable +zstyle ':completion:*:*:git:*' user-commands \ + ${${${(M)${(k)commands}:#git-*}#git-}:#(receive-pack|upload-pack|upload-archive|shell)} + # Jump to frequently used directories with `z ` if _command_exists zoxide; then