feat(git): add sign-branch and check-signed aliases
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -100,6 +100,7 @@ sway/scripts/*
|
|||||||
!bin
|
!bin
|
||||||
bin/*
|
bin/*
|
||||||
!bin/ssh-menu
|
!bin/ssh-menu
|
||||||
|
!bin/git-sign-branch
|
||||||
|
|
||||||
# swaync
|
# swaync
|
||||||
!swaync
|
!swaync
|
||||||
|
|||||||
74
bin/git-sign-branch
Executable file
74
bin/git-sign-branch
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# git sign-branch [-n] [--reset-author] [<branch> [<upstream>]]
|
||||||
|
# re-creates every commit in <upstream>..<branch> with a signature, keeping trees, parents (mapped to
|
||||||
|
# the new ids), author, committer, dates and message byte for byte; merges are copied, never re-run
|
||||||
|
# --reset-author: author and committer become this repo's user.name and user.email (the signer); dates stay
|
||||||
|
# a branch with a merge of a signed tag (mergetag header) is refused, commit-tree can't carry the tag over
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dry=0 reset=0
|
||||||
|
while [[ ${1:-} == -* ]]; do
|
||||||
|
case $1 in
|
||||||
|
-n | --dry-run) dry=1 ;;
|
||||||
|
--reset-author) reset=1 ;;
|
||||||
|
*) echo "unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
if (( reset )); then
|
||||||
|
me_name=$(git config user.name) me_email=$(git config user.email)
|
||||||
|
fi
|
||||||
|
branch=${1:-$(git symbolic-ref --short HEAD)}
|
||||||
|
upstream=${2:-main}
|
||||||
|
ref=refs/heads/$branch
|
||||||
|
old_tip=$(git rev-parse --verify "$ref^{commit}")
|
||||||
|
git rev-parse --verify -q "$upstream^{commit}" >/dev/null || { echo "no such upstream: $upstream" >&2; exit 1; }
|
||||||
|
|
||||||
|
# checked before anything is signed, so a refusal leaves no partial work
|
||||||
|
tagged=()
|
||||||
|
while read -r c; do
|
||||||
|
if [[ -n $(git cat-file commit "$c" | sed -n '1,/^$/{/^mergetag /p}') ]]; then
|
||||||
|
tagged+=("$(git log -1 --format='%h %s' "$c")")
|
||||||
|
fi
|
||||||
|
done < <(git rev-list "$upstream..$old_tip")
|
||||||
|
if (( ${#tagged[@]} )); then
|
||||||
|
echo "refusing to sign $branch: commit-tree can't carry over the signed tag these merges embed (mergetag):" >&2
|
||||||
|
printf ' %s\n' "${tagged[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A new # old id -> new id, only for commits in the range
|
||||||
|
msg=$(mktemp); trap 'rm -f "$msg"' EXIT
|
||||||
|
count=0
|
||||||
|
|
||||||
|
while read -r c parents; do
|
||||||
|
args=() changed=0
|
||||||
|
for p in $parents; do
|
||||||
|
np=${new[$p]:-$p}; [[ $np != "$p" ]] && changed=1
|
||||||
|
args+=(-p "$np")
|
||||||
|
done
|
||||||
|
# a commit already signed, with the same parents (and, with --reset-author, already ours) stays as it is
|
||||||
|
mine=1
|
||||||
|
if (( reset )) && [[ $(git log -1 --format='%an%x1f%ae%x1f%cn%x1f%ce' "$c") != "$me_name"$'\x1f'"$me_email"$'\x1f'"$me_name"$'\x1f'"$me_email" ]]; then mine=0; fi
|
||||||
|
if (( !changed && mine )) && git verify-commit "$c" >/dev/null 2>&1; then new[$c]=$c; continue; fi
|
||||||
|
count=$((count + 1))
|
||||||
|
if (( dry )); then
|
||||||
|
echo "would sign $(git log -1 --format='%h %s' "$c")"; new[$c]=dry-$c; continue
|
||||||
|
fi
|
||||||
|
git cat-file commit "$c" | sed '1,/^$/d' >"$msg" # message bytes after the header block
|
||||||
|
# a non-utf-8 commit keeps its encoding header; names are read in it so their bytes stay too
|
||||||
|
enc=$(git cat-file commit "$c" | sed -n '1,/^$/{s/^encoding //p}')
|
||||||
|
enc=${enc:-UTF-8}
|
||||||
|
IFS=$'\x1f' read -r an ae ad cn ce cd < <(git log -1 --encoding="$enc" --date=raw --format='%an%x1f%ae%x1f%ad%x1f%cn%x1f%ce%x1f%cd' "$c")
|
||||||
|
if (( reset )); then an=$me_name ae=$me_email cn=$me_name ce=$me_email; fi
|
||||||
|
new[$c]=$(GIT_AUTHOR_NAME=$an GIT_AUTHOR_EMAIL=$ae GIT_AUTHOR_DATE=$ad \
|
||||||
|
GIT_COMMITTER_NAME=$cn GIT_COMMITTER_EMAIL=$ce GIT_COMMITTER_DATE=$cd \
|
||||||
|
git -c i18n.commitEncoding="$enc" commit-tree -S "${args[@]}" -F "$msg" "$c^{tree}")
|
||||||
|
done < <(git rev-list --reverse --topo-order --parents "$upstream..$old_tip")
|
||||||
|
|
||||||
|
if (( dry )); then echo "$count commit(s) to sign on $branch"; exit 0; fi
|
||||||
|
new_tip=${new[$old_tip]:-$old_tip}
|
||||||
|
[[ $(git rev-parse "$new_tip^{tree}") == $(git rev-parse "$old_tip^{tree}") ]] || { echo "tip tree differs, ref left alone" >&2; exit 1; }
|
||||||
|
# compare-and-swap: fails if the branch moved while we were signing
|
||||||
|
git update-ref -m "sign-branch: signed $count commit(s)" "$ref" "$new_tip" "$old_tip"
|
||||||
|
echo "$branch: $old_tip -> $new_tip ($count signed); undo: git update-ref $ref $old_tip"
|
||||||
@@ -47,6 +47,10 @@
|
|||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
blame-ignore = "config blame.ignoreRevsFile .git-blame-ignore-revs"
|
blame-ignore = "config blame.ignoreRevsFile .git-blame-ignore-revs"
|
||||||
|
# re-sign <upstream>..<branch> as the repo identity, keeping trees and merge topology
|
||||||
|
sign-branch = "!~/.config/bin/git-sign-branch --reset-author"
|
||||||
|
# list every commit in <range> without a good signature; exits 1 if any, git's status if the range is bad
|
||||||
|
check-signed = "!f() { log=$(git log --format='%G? %h %s' \"${1:-main..HEAD}\") || exit; bad=$(printf '%s\\n' \"$log\" | grep -v -e '^[GU] ' -e '^$'); [ -z \"$bad\" ] && echo 'all signed' || { printf '%s\\n' \"$bad\"; exit 1; }; }; f"
|
||||||
|
|
||||||
[branch]
|
[branch]
|
||||||
sort = -committerdate
|
sort = -committerdate
|
||||||
|
|||||||
10
zsh/.zshrc
10
zsh/.zshrc
@@ -91,6 +91,16 @@ fi
|
|||||||
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh/zcompcache"
|
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh/zcompcache"
|
||||||
zstyle ':completion:*' menu select
|
zstyle ':completion:*' menu select
|
||||||
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
|
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
|
||||||
|
# _git picks these up for the matching git aliases (git/config)
|
||||||
|
_git-sign-branch() {
|
||||||
|
_arguments \
|
||||||
|
'(-n --dry-run)'{-n,--dry-run}'[list the commits that would be signed]' \
|
||||||
|
':branch:__git_branch_names' \
|
||||||
|
':upstream:__git_references'
|
||||||
|
}
|
||||||
|
_git-check-signed() {
|
||||||
|
_arguments ':commit range:__git_commit_ranges'
|
||||||
|
}
|
||||||
|
|
||||||
# tool hooks
|
# tool hooks
|
||||||
eval "$(ggman shellrc)"
|
eval "$(ggman shellrc)"
|
||||||
|
|||||||
Reference in New Issue
Block a user