add a reference updater tool #20
@@ -29,6 +29,7 @@ in
|
||||
./python.nix
|
||||
./ghostty.nix
|
||||
./updater.nix
|
||||
./update-ref.nix
|
||||
];
|
||||
|
||||
options.athenix.sw = {
|
||||
|
||||
416
sw/update-ref.nix
Normal file
416
sw/update-ref.nix
Normal file
@@ -0,0 +1,416 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
pkgs.python3
|
||||
(pkgs.writeShellScriptBin "update-ref" ''
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[31m'; YEL='\033[33m'; NC='\033[0m'
|
||||
die() { printf "''${RED}error:''${NC} %s\n" "$*" >&2; exit 2; }
|
||||
warn() { printf "''${YEL}warning:''${NC} %s\n" "$*" >&2; }
|
||||
|
||||
# ---- Must be in a git repo (current dir) ----
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "This directory is not a git project"
|
||||
CUR_REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
# ---- Athenix checkout ----
|
||||
ATHENIX_DIR="$HOME/athenix"
|
||||
ATHENIX_REPO=""
|
||||
ATHENIX_BRANCH=""
|
||||
|
||||
# ---- Git automation options for CURRENT repo ----
|
||||
COMMIT_MSG=""
|
||||
PUSH_SPEC=""
|
||||
|
||||
# ---- Push + mode controls ----
|
||||
PUSH_SET=0 # user explicitly set push true/false
|
||||
DO_PUSH=0 # final push decision
|
||||
MODE_FORCE="" # "", "local", "remote" (from -l/-r)
|
||||
|
||||
# ---- Required subcommand ----
|
||||
TARGET="" # user=<username> OR system=<device-type>:<hostkey>
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
update-ref [--athenix-repo=PATH|URL|-R PATH|URL] [--athenix-branch=BRANCH|-b BRANCH]
|
||||
[-m "msg" | --message "msg"]
|
||||
[-p[=false] [remote[=URL]] | --push[=false] [remote[=URL]]]
|
||||
[--make-local|-l] [--make-remote|-r]
|
||||
user=<username> | system=<device-type>:<hostkey>
|
||||
|
||||
push default:
|
||||
- determined from the existing builtins.fetchGit url in the target entry:
|
||||
remote url -> push=true
|
||||
file/local -> push=false
|
||||
- if the entry doesn't exist: push=false unless --make-remote
|
||||
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
is_remote_url() {
|
||||
# https://, http://, ssh://, or scp-style git@host:org/repo
|
||||
printf "%s" "$1" | grep -qE '^(https?|ssh)://|^[^/@:]+@[^/:]+:'
|
||||
}
|
||||
|
||||
derive_full_hostname() {
|
||||
devtype="$1"
|
||||
hostkey="$2"
|
||||
if printf "%s" "$hostkey" | grep -q '-' || printf "%s" "$hostkey" | grep -q "^$devtype"; then
|
||||
printf "%s" "$hostkey"
|
||||
elif printf "%s" "$hostkey" | grep -qE '^[0-9]+$'; then
|
||||
printf "%s" "$devtype$hostkey"
|
||||
else
|
||||
printf "%s" "$devtype-$hostkey"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_existing_fetch_url() {
|
||||
# args: mode file username key
|
||||
python3 - "$1" "$2" "$3" "$4" <<'PY'
|
||||
import sys, re, pathlib
|
||||
|
||||
mode = sys.argv[1] # user | system
|
||||
path = pathlib.Path(sys.argv[2])
|
||||
username = sys.argv[3] # user mode
|
||||
key = sys.argv[4] # system mode: exact string key
|
||||
|
||||
text = path.read_text()
|
||||
|
||||
def find_fetch_block_user(u):
|
||||
m = re.search(r'(?s)\n\s*' + re.escape(u) + r'\.external\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
def find_fetch_block_system(k):
|
||||
m = re.search(r'(?s)\n\s*"' + re.escape(k) + r'"\s*=\s*builtins\.fetchGit\s*\{(.*?)\n\s*\};', text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
def extract_url(block):
|
||||
if not block:
|
||||
return ""
|
||||
m = re.search(r'url\s*=\s*"([^"]+)"\s*;', block)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
block = None
|
||||
if mode == "user":
|
||||
block = find_fetch_block_user(username)
|
||||
else:
|
||||
block = find_fetch_block_system(key)
|
||||
|
||||
print(extract_url(block))
|
||||
PY
|
||||
}
|
||||
|
||||
# ---- Parse args ----
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
user=*|system=*)
|
||||
[ -z "$TARGET" ] || die "Only one subcommand allowed (user=... or system=...)"
|
||||
TARGET="$1"
|
||||
shift
|
||||
;;
|
||||
--athenix-repo=*)
|
||||
ATHENIX_REPO="''${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-R)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
ATHENIX_REPO="$2"
|
||||
shift 2
|
||||
;;
|
||||
--athenix-branch=*)
|
||||
ATHENIX_BRANCH="''${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-b)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
ATHENIX_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-m|--message)
|
||||
[ "$#" -ge 2 ] || usage
|
||||
COMMIT_MSG="$2"
|
||||
shift 2
|
||||
;;
|
||||
|
||||
-p|--push)
|
||||
PUSH_SET=1
|
||||
DO_PUSH=1
|
||||
# optional next token remote or remote=URL (only if it doesn't look like another flag/subcommand)
|
||||
if [ "$#" -ge 2 ] && printf "%s" "$2" | grep -qvE '^(user=|system=|--|-l$|-r$)'; then
|
||||
PUSH_SPEC="$2"
|
||||
shift 2
|
||||
else
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
-p=*|--push=*)
|
||||
PUSH_SET=1
|
||||
val="''${1#*=}"
|
||||
case "$val" in
|
||||
false|0|no|off) DO_PUSH=0 ;;
|
||||
true|1|yes|on|"") DO_PUSH=1 ;;
|
||||
*) die "Invalid value for --push: $val (use true/false)" ;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
|
||||
--make-local|-l)
|
||||
MODE_FORCE="local"
|
||||
shift
|
||||
;;
|
||||
--make-remote|-r)
|
||||
MODE_FORCE="remote"
|
||||
shift
|
||||
;;
|
||||
|
||||
-h|--help) usage ;;
|
||||
*) die "Unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$TARGET" ] || die "Missing required subcommand: user=<username> or system=<device-type>:<hostkey>"
|
||||
|
||||
# ---- Validate athenix checkout ----
|
||||
[ -d "$ATHENIX_DIR" ] || die "$ATHENIX_DIR does not exist"
|
||||
git -C "$ATHENIX_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "$ATHENIX_DIR is not a git project"
|
||||
if [ -z "$ATHENIX_BRANCH" ]; then
|
||||
ATHENIX_BRANCH="$(git -C "$ATHENIX_DIR" rev-parse --abbrev-ref HEAD)"
|
||||
fi
|
||||
|
||||
# ---- Determine target file + identifiers ----
|
||||
FILE=""
|
||||
MODE=""
|
||||
USERNAME=""
|
||||
DEVTYPE=""
|
||||
HOSTKEY=""
|
||||
|
||||
case "$TARGET" in
|
||||
user=*)
|
||||
MODE="user"
|
||||
USERNAME="''${TARGET#user=}"
|
||||
[ -n "$USERNAME" ] || die "user=<username>: username missing"
|
||||
FILE="$ATHENIX_DIR/users.nix"
|
||||
;;
|
||||
system=*)
|
||||
MODE="system"
|
||||
RHS="''${TARGET#system=}"
|
||||
printf "%s" "$RHS" | grep -q ':' || die "system=... must be system=<device-type>:<hostkey>"
|
||||
DEVTYPE="''${RHS%%:*}"
|
||||
HOSTKEY="''${RHS#*:}"
|
||||
[ -n "$DEVTYPE" ] || die "system=<device-type>:<hostkey>: device-type missing"
|
||||
[ -n "$HOSTKEY" ] || die "system=<device-type>:<hostkey>: hostkey missing"
|
||||
FILE="$ATHENIX_DIR/inventory.nix"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -f "$FILE" ] || die "File not found: $FILE"
|
||||
|
||||
# ---- Determine existing fetchGit url in the entry (for push default) ----
|
||||
EXISTING_URL=""
|
||||
ENTRY_EXISTS=0
|
||||
|
||||
if [ "$MODE" = "user" ]; then
|
||||
EXISTING_URL="$(extract_existing_fetch_url user "$FILE" "$USERNAME" "")"
|
||||
[ -n "$EXISTING_URL" ] && ENTRY_EXISTS=1 || true
|
||||
else
|
||||
FULL="$(derive_full_hostname "$DEVTYPE" "$HOSTKEY")"
|
||||
EXISTING_URL="$(extract_existing_fetch_url system "$FILE" "" "$HOSTKEY")"
|
||||
if [ -n "$EXISTING_URL" ]; then
|
||||
ENTRY_EXISTS=1
|
||||
elif [ "$FULL" != "$HOSTKEY" ]; then
|
||||
EXISTING_URL="$(extract_existing_fetch_url system "$FILE" "" "$FULL")"
|
||||
[ -n "$EXISTING_URL" ] && ENTRY_EXISTS=1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- Default push based on existing entry url unless user explicitly set it ----
|
||||
if [ "$PUSH_SET" -eq 0 ]; then
|
||||
if [ "$ENTRY_EXISTS" -eq 1 ] && is_remote_url "$EXISTING_URL"; then
|
||||
DO_PUSH=1
|
||||
else
|
||||
# entry missing or local/file url
|
||||
if [ "$MODE_FORCE" = "remote" ]; then
|
||||
DO_PUSH=1
|
||||
else
|
||||
DO_PUSH=0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If forcing local and user didn't explicitly demand push, push defaults off.
|
||||
if [ "$MODE_FORCE" = "local" ] && [ "$PUSH_SET" -eq 0 ]; then
|
||||
DO_PUSH=0
|
||||
fi
|
||||
|
||||
# ---- Dirty check prompt ----
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
warn "This branch has untracked or uncommitted changes. Would you like to add, commit''${DO_PUSH:+, and push}? [y/N] "
|
||||
read -r ans || true
|
||||
case "''${ans:-N}" in
|
||||
y|Y|yes|YES)
|
||||
git add -A
|
||||
if ! git diff --cached --quiet; then
|
||||
if [ -n "$COMMIT_MSG" ]; then
|
||||
git commit -m "$COMMIT_MSG"
|
||||
else
|
||||
git commit
|
||||
fi
|
||||
else
|
||||
warn "No staged changes to commit."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "Proceeding without committing. (rev will be last committed HEAD.)"
|
||||
;;
|
||||
esac
|
||||
elif [ -n "$COMMIT_MSG" ]; then
|
||||
# clean tree but message provided: nothing to do
|
||||
:
|
||||
fi
|
||||
|
||||
# ---- Push current repo if requested ----
|
||||
PUSH_REMOTE_URL=""
|
||||
if [ "$DO_PUSH" -eq 1 ]; then
|
||||
if [ -n "$PUSH_SPEC" ]; then
|
||||
if printf "%s" "$PUSH_SPEC" | grep -q '='; then
|
||||
REM_NAME="''${PUSH_SPEC%%=*}"
|
||||
REM_URL="''${PUSH_SPEC#*=}"
|
||||
[ -n "$REM_NAME" ] || die "--push remote-name=URL: remote-name missing"
|
||||
[ -n "$REM_URL" ] || die "--push remote-name=URL: URL missing"
|
||||
|
||||
if git remote get-url "$REM_NAME" >/dev/null 2>&1; then
|
||||
git remote set-url "$REM_NAME" "$REM_URL"
|
||||
else
|
||||
git remote add "$REM_NAME" "$REM_URL"
|
||||
fi
|
||||
|
||||
git push -u "$REM_NAME" "$CUR_BRANCH"
|
||||
PUSH_REMOTE_URL="$REM_URL"
|
||||
else
|
||||
REM_NAME="$PUSH_SPEC"
|
||||
git push -u "$REM_NAME" "$CUR_BRANCH"
|
||||
PUSH_REMOTE_URL="$(git remote get-url "$REM_NAME")"
|
||||
fi
|
||||
else
|
||||
# default: push to upstream, error if none
|
||||
if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
||||
die "No upstream is set. Set a default upstream with \"git branch -u <remote>/<remote_branch_name>\""
|
||||
fi
|
||||
git push
|
||||
UPSTREAM_REMOTE="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} | cut -d/ -f1)"
|
||||
PUSH_REMOTE_URL="$(git remote get-url "$UPSTREAM_REMOTE")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- Always get current HEAD hash (after optional commit/push) ----
|
||||
CUR_REV="$(git -C "$CUR_REPO_ROOT" rev-parse HEAD)"
|
||||
|
||||
# ---- Determine url to write into fetchGit (respecting make-local/make-remote) ----
|
||||
if [ "$MODE_FORCE" = "local" ]; then
|
||||
FETCH_URL="file://$CUR_REPO_ROOT"
|
||||
elif [ "$MODE_FORCE" = "remote" ]; then
|
||||
if [ "$DO_PUSH" -eq 1 ]; then
|
||||
FETCH_URL="$PUSH_REMOTE_URL"
|
||||
elif [ "$ENTRY_EXISTS" -eq 1 ] && [ -n "$EXISTING_URL" ] && is_remote_url "$EXISTING_URL"; then
|
||||
FETCH_URL="$EXISTING_URL"
|
||||
else
|
||||
CUR_ORIGIN="$(git remote get-url origin 2>/dev/null || true)"
|
||||
[ -n "$CUR_ORIGIN" ] && is_remote_url "$CUR_ORIGIN" || die "--make-remote requires a remote url (set origin or use -p remote=URL)"
|
||||
FETCH_URL="$CUR_ORIGIN"
|
||||
fi
|
||||
else
|
||||
if [ "$DO_PUSH" -eq 1 ]; then
|
||||
FETCH_URL="$PUSH_REMOTE_URL"
|
||||
else
|
||||
FETCH_URL="file://$CUR_REPO_ROOT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- Rewrite athenix file ----
|
||||
python3 - "$MODE" "$FILE" "$FETCH_URL" "$CUR_REV" "$USERNAME" "$DEVTYPE" "$HOSTKEY" <<'PY'
|
||||
import sys, re, pathlib
|
||||
|
||||
mode = sys.argv[1]
|
||||
path = pathlib.Path(sys.argv[2])
|
||||
fetch_url = sys.argv[3]
|
||||
rev = sys.argv[4]
|
||||
username = sys.argv[5]
|
||||
devtype = sys.argv[6]
|
||||
hostkey = sys.argv[7]
|
||||
|
||||
text = path.read_text()
|
||||
|
||||
def mk_fetch_block(url, rev):
|
||||
return (
|
||||
'builtins.fetchGit {\n'
|
||||
f' url = "{url}";\n'
|
||||
f' rev = "{rev}";\n'
|
||||
' submodules=true;\n'
|
||||
' }'
|
||||
)
|
||||
|
||||
fetch_block = mk_fetch_block(fetch_url, rev)
|
||||
|
||||
def full_hostname(devtype, hostkey):
|
||||
if hostkey.startswith(devtype) or "-" in hostkey:
|
||||
return hostkey
|
||||
if hostkey.isdigit():
|
||||
return f"{devtype}{hostkey}"
|
||||
return f"{devtype}-{hostkey}"
|
||||
|
||||
if mode == "user":
|
||||
m = re.search(r'(?s)(athenix\.users\s*=\s*\{)(.*?)(\n\s*\};)', text)
|
||||
if not m:
|
||||
raise SystemExit("error: could not locate `athenix.users = { ... };` block")
|
||||
|
||||
head, body, tail = m.group(1), m.group(2), m.group(3)
|
||||
|
||||
key_re = re.compile(r'(?s)(\n\s*' + re.escape(username) + r'\.external\s*=\s*)builtins\.fetchGit\s*\{.*?\};')
|
||||
if key_re.search(body):
|
||||
body = key_re.sub(lambda mm: mm.group(1) + fetch_block + ';', body)
|
||||
else:
|
||||
body = body + f'\n {username}.external = {fetch_block};\n'
|
||||
|
||||
new_text = text[:m.start()] + head + body + tail + text[m.end():]
|
||||
path.write_text(new_text)
|
||||
sys.exit(0)
|
||||
|
||||
elif mode == "system":
|
||||
block_re = re.compile(r'(?s)(\n\s*' + re.escape(devtype) + r'\s*=\s*\{)(.*?)(\n\s*\};)')
|
||||
m = block_re.search(text)
|
||||
if not m:
|
||||
raise SystemExit(f"error: could not locate `{devtype} = {{ ... }};` block in inventory.nix")
|
||||
|
||||
head, body, tail = m.group(1), m.group(2), m.group(3)
|
||||
|
||||
candidates = [hostkey, full_hostname(devtype, hostkey)]
|
||||
candidates = list(dict.fromkeys(candidates))
|
||||
|
||||
replaced = False
|
||||
for k in candidates:
|
||||
entry_re = re.compile(r'(?s)(\n\s*"' + re.escape(k) + r'"\s*=\s*)builtins\.fetchGit\s*\{.*?\};')
|
||||
if entry_re.search(body):
|
||||
body = entry_re.sub(lambda mm: mm.group(1) + fetch_block + ';', body)
|
||||
replaced = True
|
||||
break
|
||||
|
||||
if not replaced:
|
||||
body = body + f'\n "{hostkey}" = {fetch_block};\n'
|
||||
|
||||
new_text = text[:m.start()] + head + body + tail + text[m.end():]
|
||||
path.write_text(new_text)
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
raise SystemExit("error: unknown mode")
|
||||
PY
|
||||
|
||||
printf "updated %s (athenix branch: %s)\n" "$FILE" "$ATHENIX_BRANCH" >&2
|
||||
printf " url = %s\n" "$FETCH_URL" >&2
|
||||
printf " rev = %s\n" "$CUR_REV" >&2
|
||||
'')
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user