From db2c3110c7c9e20e1f7a9dfcf7a8055b9d63601b Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Sat, 6 Jun 2026 15:55:01 +1000 Subject: [PATCH 1/2] git: Add agent auth helpers --- bin/git-assume | 40 +++++++++++++++++ bin/git-github-auth | 101 ++++++++++++++++++++++++++++++++++++++++++ git/gitconfig.symlink | 3 -- git/identity.zsh | 23 +--------- 4 files changed, 142 insertions(+), 25 deletions(-) create mode 100755 bin/git-assume create mode 100755 bin/git-github-auth diff --git a/bin/git-assume b/bin/git-assume new file mode 100755 index 0000000..8f89572 --- /dev/null +++ b/bin/git-assume @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -e + +identity="${1:-}" + +if [[ -z "$identity" ]]; then + echo "Usage: git-assume " >&2 + exit 2 +fi + +if ! name=$(git config "identity.$identity.name"); then + echo "Missing name for $identity" >&2 + exit 1 +fi + +if ! email=$(git config "identity.$identity.email"); then + echo "Missing email for $identity" >&2 + exit 1 +fi + +config_scope=() +if git_dir=$(git rev-parse --git-dir 2>/dev/null) && + git_common_dir=$(git rev-parse --git-common-dir 2>/dev/null) && + [[ "$git_dir" != "$git_common_dir" ]] && + [[ "$(git config --bool --get extensions.worktreeConfig 2>/dev/null)" == "true" ]]; then + config_scope=(--worktree) +fi + +git config "${config_scope[@]}" user.identity "$identity" +git config "${config_scope[@]}" user.name "$name" +git config "${config_scope[@]}" user.email "$email" + +if signingkey=$(git config "identity.$identity.signingkey"); then + git config "${config_scope[@]}" user.signingkey "$signingkey" + git config "${config_scope[@]}" commit.gpgsign true +else + git config "${config_scope[@]}" --unset user.signingkey 2>/dev/null || true + git config "${config_scope[@]}" commit.gpgsign false +fi diff --git a/bin/git-github-auth b/bin/git-github-auth new file mode 100755 index 0000000..ca1a3d9 --- /dev/null +++ b/bin/git-github-auth @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +set -e + +usage() { + echo "Usage: git-github-auth [remote]" >&2 +} + +mode="${1:-}" +remote="${2:-origin}" + +if [[ -z "$mode" ]]; then + usage + exit 2 +fi + +case "$mode" in +gh | https) + transport="https" + ;; +ssh) + transport="ssh" + ;; +*) + usage + exit 2 + ;; +esac + +if ! current_url=$(git config --get "remote.$remote.url" 2>/dev/null); then + current_url=$(git remote get-url "$remote" 2>/dev/null || true) +fi + +if [[ -z "$current_url" ]]; then + echo "Missing remote: $remote" >&2 + exit 1 +fi + +repo_path="${current_url%.git}" +case "$repo_path" in +https://github.com/*) + repo_path="${repo_path#https://github.com/}" + ;; +http://github.com/*) + repo_path="${repo_path#http://github.com/}" + ;; +git@github.com:*) + repo_path="${repo_path#git@github.com:}" + ;; +ssh://git@github.com/*) + repo_path="${repo_path#ssh://git@github.com/}" + ;; +github.com/*) + repo_path="${repo_path#github.com/}" + ;; +*) + echo "Cannot infer GitHub owner/repo from remote URL: $current_url" >&2 + exit 1 + ;; +esac + +IFS=/ read -r owner repo _ <<<"$repo_path" +if [[ -z "$owner" || -z "$repo" ]]; then + echo "Cannot infer GitHub owner/repo from remote URL: $current_url" >&2 + exit 1 +fi + +repo="${repo%.git}" + +case "$transport" in +https) + target_base="https://github.com/$owner/$repo" + source_base="git@github.com:$owner/$repo" + alt_source_base="ssh://git@github.com/$owner/$repo" + ;; +ssh) + target_base="git@github.com:$owner/$repo" + source_base="https://github.com/$owner/$repo" + alt_source_base="github.com/$owner/$repo" + ;; +esac + +config_scope=() +if git_dir=$(git rev-parse --git-dir 2>/dev/null) && + git_common_dir=$(git rev-parse --git-common-dir 2>/dev/null) && + [[ "$git_dir" != "$git_common_dir" ]]; then + if [[ "$(git config --bool --get extensions.worktreeConfig 2>/dev/null)" != "true" ]]; then + echo "Refusing to change shared config from a linked worktree without extensions.worktreeConfig=true" >&2 + exit 1 + fi + config_scope=(--worktree) +fi + +git config "${config_scope[@]}" --remove-section "url.https://github.com/$owner/$repo" 2>/dev/null || true +git config "${config_scope[@]}" --remove-section "url.git@github.com:$owner/$repo" 2>/dev/null || true + +git config "${config_scope[@]}" --add "url.$target_base.insteadOf" "$source_base" +git config "${config_scope[@]}" --add "url.$target_base.insteadOf" "$alt_source_base" + +echo "$remote fetch: $(git remote get-url "$remote")" +echo "$remote push: $(git remote get-url --push "$remote")" diff --git a/git/gitconfig.symlink b/git/gitconfig.symlink index 9b44f64..a0e46a8 100644 --- a/git/gitconfig.symlink +++ b/git/gitconfig.symlink @@ -59,9 +59,6 @@ [gpg "ssh"] program = /Applications/1Password.app/Contents/MacOS/op-ssh-sign -[url "git@github.com:lox/"] - insteadOf = https://github.com/lox/ - insteadOf = github.com/lox/ [credential "https://github.com"] helper = helper = !gh auth git-credential diff --git a/git/identity.zsh b/git/identity.zsh index 64d96ef..3e1f2f6 100644 --- a/git/identity.zsh +++ b/git/identity.zsh @@ -6,26 +6,5 @@ git-identities() { } git-assume() { - local identity="$1" - local name - local email - - if ! name=$(git config "identity.$identity.name") ; then - echo "Missing name for $identity" - return 1 - fi - - if ! email=$(git config "identity.$identity.email") ; then - echo "Missing email for $identity" - return 1 - fi - - git config user.identity "$identity" - git config user.name "$name" - git config user.email "$email" - - if signingkey=$(git config "identity.$identity.signingkey") ; then - git config user.signingkey "$signingkey" - git config commit.gpgsign true - fi + command git-assume "$@" } From 507192d8aff8026f280e9c5a2dfda79f8fb72c70 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Sat, 6 Jun 2026 17:02:49 +1000 Subject: [PATCH 2/2] fix: Constrain github auth rewrites --- bin/git-github-auth | 203 ++++++++++++++++++++++++++++++++------------ 1 file changed, 151 insertions(+), 52 deletions(-) diff --git a/bin/git-github-auth b/bin/git-github-auth index ca1a3d9..3ebfe74 100755 --- a/bin/git-github-auth +++ b/bin/git-github-auth @@ -6,6 +6,103 @@ usage() { echo "Usage: git-github-auth [remote]" >&2 } +parse_github_url() { + local url="$1" + local repo_path + local owner + local repo + local rest + + repo_path="${url%.git}" + case "$repo_path" in + https://github.com/*) + repo_path="${repo_path#https://github.com/}" + ;; + http://github.com/*) + repo_path="${repo_path#http://github.com/}" + ;; + git@github.com:*) + repo_path="${repo_path#git@github.com:}" + ;; + ssh://git@github.com/*) + repo_path="${repo_path#ssh://git@github.com/}" + ;; + github.com/*) + repo_path="${repo_path#github.com/}" + ;; + *) + return 1 + ;; + esac + + IFS=/ read -r owner repo rest <<<"$repo_path" + if [[ -z "$owner" || -z "$repo" || -n "$rest" ]]; then + return 1 + fi + + printf '%s/%s\n' "$owner" "$repo" +} + +target_url_for_repo() { + local repo_path="$1" + + case "$transport" in + https) + printf 'https://github.com/%s.git\n' "$repo_path" + ;; + ssh) + printf 'git@github.com:%s.git\n' "$repo_path" + ;; + esac +} + +remove_repo_rewrites() { + local repo_path="$1" + + git config "${config_scope[@]}" --remove-section "url.https://github.com/$repo_path" 2>/dev/null || true + git config "${config_scope[@]}" --remove-section "url.https://github.com/$repo_path.git" 2>/dev/null || true + git config "${config_scope[@]}" --remove-section "url.git@github.com:$repo_path" 2>/dev/null || true + git config "${config_scope[@]}" --remove-section "url.git@github.com:$repo_path.git" 2>/dev/null || true +} + +add_worktree_rewrite() { + local source_url="$1" + local repo_path + local target_url + + if [[ "$source_url" != *.git ]]; then + echo "Cannot safely rewrite non-.git URL from a linked worktree: $source_url" >&2 + echo "Normalize the shared remote URL with .git first, or run this from the main checkout." >&2 + exit 1 + fi + + if ! repo_path=$(parse_github_url "$source_url"); then + echo "Cannot infer GitHub owner/repo from remote URL: $source_url" >&2 + exit 1 + fi + + target_url=$(target_url_for_repo "$repo_path") + git config "${config_scope[@]}" --add "url.$target_url.insteadOf" "$source_url" +} + +set_direct_urls() { + local key="$1" + shift + local url + local repo_path + local target_url + + git config --unset-all "$key" 2>/dev/null || true + for url in "$@"; do + if ! repo_path=$(parse_github_url "$url"); then + echo "Cannot infer GitHub owner/repo from remote URL: $url" >&2 + exit 1 + fi + target_url=$(target_url_for_repo "$repo_path") + git config --add "$key" "$target_url" + done +} + mode="${1:-}" remote="${2:-origin}" @@ -27,59 +124,21 @@ ssh) ;; esac -if ! current_url=$(git config --get "remote.$remote.url" 2>/dev/null); then - current_url=$(git remote get-url "$remote" 2>/dev/null || true) -fi - -if [[ -z "$current_url" ]]; then - echo "Missing remote: $remote" >&2 - exit 1 -fi +fetch_urls=() +while IFS= read -r url; do + fetch_urls+=("$url") +done < <(git config --get-all "remote.$remote.url" 2>/dev/null || true) -repo_path="${current_url%.git}" -case "$repo_path" in -https://github.com/*) - repo_path="${repo_path#https://github.com/}" - ;; -http://github.com/*) - repo_path="${repo_path#http://github.com/}" - ;; -git@github.com:*) - repo_path="${repo_path#git@github.com:}" - ;; -ssh://git@github.com/*) - repo_path="${repo_path#ssh://git@github.com/}" - ;; -github.com/*) - repo_path="${repo_path#github.com/}" - ;; -*) - echo "Cannot infer GitHub owner/repo from remote URL: $current_url" >&2 - exit 1 - ;; -esac +push_urls=() +while IFS= read -r url; do + push_urls+=("$url") +done < <(git config --get-all "remote.$remote.pushurl" 2>/dev/null || true) -IFS=/ read -r owner repo _ <<<"$repo_path" -if [[ -z "$owner" || -z "$repo" ]]; then - echo "Cannot infer GitHub owner/repo from remote URL: $current_url" >&2 +if ((${#fetch_urls[@]} == 0)); then + echo "Missing remote: $remote" >&2 exit 1 fi -repo="${repo%.git}" - -case "$transport" in -https) - target_base="https://github.com/$owner/$repo" - source_base="git@github.com:$owner/$repo" - alt_source_base="ssh://git@github.com/$owner/$repo" - ;; -ssh) - target_base="git@github.com:$owner/$repo" - source_base="https://github.com/$owner/$repo" - alt_source_base="github.com/$owner/$repo" - ;; -esac - config_scope=() if git_dir=$(git rev-parse --git-dir 2>/dev/null) && git_common_dir=$(git rev-parse --git-common-dir 2>/dev/null) && @@ -91,11 +150,51 @@ if git_dir=$(git rev-parse --git-dir 2>/dev/null) && config_scope=(--worktree) fi -git config "${config_scope[@]}" --remove-section "url.https://github.com/$owner/$repo" 2>/dev/null || true -git config "${config_scope[@]}" --remove-section "url.git@github.com:$owner/$repo" 2>/dev/null || true +if ((${#config_scope[@]} > 0)); then + for url in "${fetch_urls[@]}" "${push_urls[@]}"; do + if [[ -z "$url" ]]; then + continue + fi + if [[ "$url" != *.git ]]; then + echo "Cannot safely rewrite non-.git URL from a linked worktree: $url" >&2 + echo "Normalize the shared remote URL with .git first, or run this from the main checkout." >&2 + exit 1 + fi + if ! repo_path=$(parse_github_url "$url"); then + echo "Cannot infer GitHub owner/repo from remote URL: $url" >&2 + exit 1 + fi + remove_repo_rewrites "$repo_path" + done + + for url in "${fetch_urls[@]}"; do + add_worktree_rewrite "$url" + done + + if ((${#push_urls[@]} > 0)); then + for url in "${push_urls[@]}"; do + add_worktree_rewrite "$url" + done + fi +else + for url in "${fetch_urls[@]}" "${push_urls[@]}"; do + if [[ -z "$url" ]]; then + continue + fi + if ! repo_path=$(parse_github_url "$url"); then + echo "Cannot infer GitHub owner/repo from remote URL: $url" >&2 + exit 1 + fi + remove_repo_rewrites "$repo_path" + done -git config "${config_scope[@]}" --add "url.$target_base.insteadOf" "$source_base" -git config "${config_scope[@]}" --add "url.$target_base.insteadOf" "$alt_source_base" + set_direct_urls "remote.$remote.url" "${fetch_urls[@]}" + if ((${#push_urls[@]} > 0)); then + set_direct_urls "remote.$remote.pushurl" "${push_urls[@]}" + else + git config --unset-all "remote.$remote.pushurl" 2>/dev/null || true + fi +fi echo "$remote fetch: $(git remote get-url "$remote")" echo "$remote push: $(git remote get-url --push "$remote")"