#!/usr/bin/env bash # git-sync.sh — 对指定 git 仓库做同步:pull → commit → push(分叉/冲突时中断并交回决策) # # 用法: # git-sync.sh status # fetch + 状态 + 变更摘要(拟提交说明用) # git-sync.sh sync -m "<提交说明>" # 全流程同步 # git-sync.sh pull-ff # 仅 fast-forward pull # git-sync.sh resolve merge|remote|local # 分叉后处理;remote/local 需 GIT_SYNC_CONFIRM=yes # # 退出码: 0=成功 2=参数错 3=分叉/push被拒需决策 4=冲突 5=无变更 # 6=非git仓库或无upstream 7=缺提交说明 8=破坏性操作缺确认 set -uo pipefail die() { echo "ERROR: $*" >&2; exit "${2:-2}"; } [ $# -lt 2 ] && { echo "usage: git-sync.sh [...args]" >&2; exit 2; } REPO="$1"; CMD="$2"; shift 2 [ -d "$REPO" ] || die "目录不存在: $REPO" 2 REPO="$(cd "$REPO" && pwd)" cd "$REPO" git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "不是 git 仓库: $REPO" 6 BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || true)" UPSTREAM="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)" [ -n "$UPSTREAM" ] || die "分支 $BRANCH 无 upstream(先 git push -u origin $BRANCH)" 6 ahead_behind() { local L R read -r L R < <(git rev-list --left-right --count HEAD..."$UPSTREAM" 2>/dev/null) echo "${L:-0} ${R:-0}" } status_cmd() { git fetch origin >/dev/null 2>&1 || true local AHEAD BEHIND read -r AHEAD BEHIND < <(ahead_behind) echo "repo: $REPO" echo "branch: $BRANCH (upstream: $UPSTREAM)" echo "ahead: $AHEAD behind: $BEHIND" [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -gt 0 ] && echo "STATE: DIVERGED — 需用户选择 merge / 以远程为主 / 以本地为主" local PORCELAIN UNMERGED PORCELAIN="$(git status --porcelain)" UNMERGED="$(git ls-files -u | head -1)" if [ -z "$PORCELAIN" ]; then echo "changes: 无(工作区干净)" else echo "changes: $(printf '%s\n' "$PORCELAIN" | wc -l | tr -d ' ') 项未提交" [ -n "$UNMERGED" ] && echo "STATE: UNMERGED_PATHS — 存在未解决冲突,sync 会拒绝提交" echo "--- git status --short ---" printf '%s\n' "$PORCELAIN" echo "--- diff --stat(已跟踪改动) ---" git diff --stat HEAD 2>/dev/null | tail -n +1 echo "--- 未跟踪文件 ---" git ls-files --others --exclude-standard fi exit 0 } rebase_hint() { [ "$(git config pull.rebase 2>/dev/null || true)" = "true" ] \ && echo " hint: 本仓库 pull.rebase=true — 若本地改动与远端无文件重叠(git diff --name-only HEAD origin/main 为空交集),可用: git -c pull.rebase=false pull --ff-only" >&2 } pull_ff() { local AHEAD BEHIND read -r AHEAD BEHIND < <(ahead_behind) [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ] && { echo "already up to date"; exit 0; } if [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -gt 0 ]; then echo "DIVERGED: ahead=$AHEAD behind=$BEHIND — 需用户决策后再处理(resolve)" >&2 exit 3 fi echo "==> git pull --ff-only origin $BRANCH" if ! git pull --ff-only origin "$BRANCH" 2>&1; then echo "CONFLICT: pull 失败 — 本地未提交改动与远端更新冲突,或需要 merge" >&2 rebase_hint exit 4 fi git log -1 --format='pulled: %h %s' exit 0 } sync_cmd() { local MSG [ "${1:-}" = "-m" ] && shift MSG="${1:-}" [ -n "$MSG" ] || die "sync 需要 -m 提供提交说明: git-sync.sh sync -m \"...\"" 7 # 未解决冲突禁止提交 git ls-files -u | grep -q . \ && { echo "CONFLICT: 存在未解决冲突(unmerged paths),先解决再 sync" >&2; exit 4; } git fetch origin >/dev/null 2>&1 || true local AHEAD BEHIND PORCELAIN PUSH_OUT PUSH_OK read -r AHEAD BEHIND < <(ahead_behind) if [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -gt 0 ]; then echo "DIVERGED: ahead=$AHEAD behind=$BEHIND — 需用户选择 merge / 以远程为主 / 以本地为主" >&2 exit 3 fi if [ "$BEHIND" -gt 0 ]; then echo "==> pull --ff-only origin $BRANCH" if ! git pull --ff-only origin "$BRANCH" 2>&1; then echo "CONFLICT: pull 失败 — 本地未提交改动与远端更新冲突" >&2 rebase_hint exit 4 fi fi PORCELAIN="$(git status --porcelain)" if [ -z "$PORCELAIN" ]; then echo "NO_CHANGES: 工作区干净,无需提交" exit 5 fi echo "==> git add -A" git add -A echo "==> git commit -m \"$MSG\"" git commit -m "$MSG" >/dev/null || die "commit 失败" 2 echo "==> git push origin $BRANCH" PUSH_OUT="$(git push origin "$BRANCH" 2>&1)" PUSH_OK=$? if [ $PUSH_OK -ne 0 ]; then echo "PUSH_FAILED: 远端可能有新提交,重新 status 后走决策流程" >&2 printf '%s\n' "$PUSH_OUT" >&2 exit 3 fi echo "---" echo "SYNC_OK" echo "repo: $REPO" echo "branch: $BRANCH" git log -1 --format='commit: %h (%H)' git log -1 --format='title: %s' git log -1 --format='author: %an <%ae> date: %ci' git show --stat --oneline HEAD | tail -1 | sed 's/^/stats: /' printf 'push: %s\n' "$(printf '%s\n' "$PUSH_OUT" | grep -v '^$' | tail -1)" exit 0 } resolve_cmd() { local SIDE="${1:-}" CONFIRM="${GIT_SYNC_CONFIRM:-}" case "$SIDE" in merge) echo "==> git pull --no-rebase origin $BRANCH (合并远端,两边内容都保留)" if ! git pull --no-rebase origin "$BRANCH" 2>&1; then echo "CONFLICT: 合并产生冲突 — 查看 git status 的 unmerged paths,解决后 git add + git commit,再重跑 sync" >&2 exit 4 fi git log -1 --format='merged: %h %s' ;; remote) [ "$CONFIRM" = "yes" ] || { echo "DESTRUCTIVE: 以远程为主将丢弃本地提交(git reset --hard @{u}),需 GIT_SYNC_CONFIRM=yes" >&2; exit 8; } git reset --hard "$UPSTREAM" echo "reset to $UPSTREAM: $(git log -1 --format='%h %s')" ;; local) [ "$CONFIRM" = "yes" ] || { echo "DESTRUCTIVE: 以本地为主将强制推送覆盖远端(push --force-with-lease),需 GIT_SYNC_CONFIRM=yes" >&2; exit 8; } local PUSH_OUT PUSH_OK PUSH_OUT="$(git push --force-with-lease origin "$BRANCH" 2>&1)" PUSH_OK=$? if [ $PUSH_OK -ne 0 ]; then printf '%s\n' "$PUSH_OUT" >&2 die "force push 失败" 3 fi printf '%s\n' "$PUSH_OUT" | grep -v '^$' | tail -1 ;; *) die "resolve 选项: merge|remote|local" 2 ;; esac exit 0 } case "$CMD" in status) status_cmd ;; sync) sync_cmd "$@" ;; pull-ff) pull_ff ;; resolve) resolve_cmd "${1:-}" ;; *) die "未知命令: $CMD(可用 status|sync|pull-ff|resolve)" 2 ;; esac