#!/bin/bash # chv-sync:轻量 VM 里的 Gitea 双向同步(用户工作区口径)。 # # 主角是用户,不是 agent:用户在 Gitea 上的仓库在 VM 启动时 # clone 到 ~/workspace,用完再同步回 Gitea;VM 是用户的工作区, # Gitea 是权威远端。同步口径沿用 Git Workspace Platform 已定 # 的规则:分叉或冲突时失败关闭,不强制覆盖、不自动 rebase、 # 不静默丢提交。 # # 凭证绝不进镜像。启动时由 seed 盘投递 /etc/chv/guest-env # (0600,root 所有),格式(KEY=VALUE,每行一项): # GITEA_URL=https://gitea.tose.top # GITEA_USER= # GITEA_TOKEN=<专用 API token,仅此次测试/此次 VM 有效> # SYNC_REPO= # SYNC_BRANCH=<可选,默认分支> # GITEA_EMAIL=<可选,commit 身份邮箱;不给则留镜像占位> # boot(root,cloud-init runcmd 调)读 guest-env,把 token 写进 # /home/chv/.git-credentials(0600,chv 所有,git credential # store 标准机制),再以 chv 身份 clone 到 ~/workspace/<仓名>。 # pull/push/status 以 chv 身份跑,只用已落盘的 credential,不 # 再读 guest-env;token 不会出现在 git remote URL 或镜像里。 # # 用法: # chv-sync boot # 启动时 clone(已 clone 则静默退出,不自动 pull) # chv-sync pull [仓目录] # 脏树拒绝;只接受 fast-forward,分叉即失败 # chv-sync push [仓目录] # 普通 push;无 upstream 时 push -u origin HEAD; # # 非 fast-forward 即失败,绝不 --force # chv-sync status [仓目录] # 本地状态 + 最近提交 # 仓目录省略时取 $WORKSPACE 下唯一的 git 仓库;多个时必须显式给。 set -euo pipefail GUEST_ENV="${CHV_GUEST_ENV:-/etc/chv/guest-env}" WORKSPACE="${CHV_WORKSPACE:-/home/chv/workspace}" USER_HOME="${CHV_USER_HOME:-/home/chv}" SYNC_USER="${CHV_USER:-chv}" # privsep worker(feat-chv-muse-p6):socket 在时 pull/push/status # 改走 worker(类型化请求),不在时走旧的直接路径 + 警告(过渡兼容, # 旧镜像退役后改为无 worker 即 fail-closed)。 WORKER_SOCK="${CHV_GITEA_WORKER_SOCK:-/run/chv-gitea/worker.sock}" WORKER_CLIENT="${CHV_GITEA_WORKER_CLIENT:-/usr/local/bin/chv-sync-worker-client}" die() { echo "chv-sync: $*" >&2; exit 1; } info() { echo "chv-sync: $*"; } # 试走 worker:返回 42 = worker 不在(调用方走旧路径);其它返回 # 值即 worker 的退出码(调用方直接退出,不再回落重跑)。 try_worker() { local action="$1"; shift [ -S "$WORKER_SOCK" ] || return 42 [ -x "$WORKER_CLIENT" ] || return 42 "$WORKER_CLIENT" "$action" "$@" } # 解析仓目录:显式给则用它;否则取 WORKSPACE 下唯一的 git 仓库。 resolve_repo() { if [ -n "${1:-}" ]; then [ -d "$1/.git" ] || die "不是 git 仓库:$1" printf '%s' "$1" return 0 fi local found=() local d for d in "$WORKSPACE"/*/; do [ -d "${d}.git" ] && found+=("${d%/}") done if [ "${#found[@]}" -eq 1 ]; then printf '%s' "${found[0]}" return 0 fi die "WORKSPACE 下 git 仓库不是唯一(${#found[@]} 个),请显式给仓目录" } cmd_boot() { # 无 guest-env = 本次启动不同步任何仓,静默成功(VM 照常用)。 [ -f "$GUEST_ENV" ] || { info "无 $GUEST_ENV,本次启动不同步"; exit 0; } # shellcheck disable=SC1090 set -a; . "$GUEST_ENV"; set +a : "${GITEA_URL:?guest-env 缺 GITEA_URL}" : "${GITEA_USER:?guest-env 缺 GITEA_USER}" : "${GITEA_TOKEN:?guest-env 缺 GITEA_TOKEN}" : "${SYNC_REPO:?guest-env 缺 SYNC_REPO}" [ "$(id -u)" = "0" ] || die "boot 须以 root 运行(cloud-init runcmd)" local host="${GITEA_URL#https://}" host="${host#http://}" host="${host%%/*}" local name="${SYNC_REPO##*/}" local dir="$WORKSPACE/$name" mkdir -p "$WORKSPACE" chown "$SYNC_USER:$SYNC_USER" "$WORKSPACE" # token 只落盘进用户家目录的 credential store(0600), # 不进 git remote URL、不进镜像、不进日志(此处不打印 token)。 local creds="$USER_HOME/.git-credentials" printf 'https://%s:%s@%s\n' "$GITEA_USER" "$GITEA_TOKEN" "$host" > "$creds" chmod 0600 "$creds" chown "$SYNC_USER:$SYNC_USER" "$creds" if [ -d "$dir/.git" ]; then info "已 clone:$dir(不自动 pull,改动由用户显式同步)" exit 0 fi [ -e "$dir" ] && die "$dir 已存在但不是 git 仓库,请人工处理" local clone_args=(clone) [ -n "${SYNC_BRANCH:-}" ] && clone_args+=(--branch "$SYNC_BRANCH") clone_args+=("$GITEA_URL/$SYNC_REPO.git" "$dir") runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git "${clone_args[@]}" runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" config user.name "$GITEA_USER" if [ -n "${GITEA_EMAIL:-}" ]; then runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" config user.email "$GITEA_EMAIL" fi info "已 clone $SYNC_REPO 到 $dir,HEAD=$(runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" rev-parse --short HEAD)" } cmd_pull() { local rc=0 try_worker pull "${1:-}" || rc=$? [ "$rc" != "42" ] && exit "$rc" echo "chv-sync: worker 不在,走旧的直接路径(过渡兼容)" >&2 local dir dir="$(resolve_repo "${1:-}")" [ -z "$(git -C "$dir" status --porcelain)" ] \ || die "工作树有未提交改动,拒绝 pull(先 commit 或 stash)" git -C "$dir" pull --ff-only \ || die "pull 不是 fast-forward(本地与远端已分叉),请人工处理,不自动 rebase/merge" info "已同步:$dir HEAD=$(git -C "$dir" rev-parse --short HEAD)" } cmd_push() { local rc=0 try_worker push "${1:-}" || rc=$? [ "$rc" != "42" ] && exit "$rc" echo "chv-sync: worker 不在,走旧的直接路径(过渡兼容)" >&2 local dir dir="$(resolve_repo "${1:-}")" if git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then git -C "$dir" push \ || die "push 被拒绝(远端已有新提交,先 pull 再 push,不 --force)" else git -C "$dir" push -u origin HEAD \ || die "push 被拒绝(远端已有新提交,先 pull 再 push,不 --force)" fi info "已 push:$dir HEAD=$(git -C "$dir" rev-parse --short HEAD)" } cmd_status() { local rc=0 try_worker status "${1:-}" || rc=$? [ "$rc" != "42" ] && exit "$rc" echo "chv-sync: worker 不在,走旧的直接路径(过渡兼容)" >&2 local dir dir="$(resolve_repo "${1:-}")" git -C "$dir" status -sb git -C "$dir" log --oneline -5 } case "${1:-}" in boot) shift; cmd_boot "$@" ;; pull) shift; cmd_pull "$@" ;; push) shift; cmd_push "$@" ;; status) shift; cmd_status "$@" ;; *) echo "用法:chv-sync boot|pull|push|status [仓目录]" >&2; exit 2 ;; esac