Files
atlas/git-sync/scripts/git-sync.sh

181 lines
6.5 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# git-sync.sh — 对指定 git 仓库做同步:pull → commit → push(分叉/冲突时中断并交回决策)
#
# 用法:
# git-sync.sh <repo> status # fetch + 状态 + 变更摘要(拟提交说明用)
# git-sync.sh <repo> sync -m "<提交说明>" # 全流程同步
# git-sync.sh <repo> pull-ff # 仅 fast-forward pull
# git-sync.sh <repo> 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 <repo> <status|sync|pull-ff|resolve> [...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 <repo> 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