181 lines
6.5 KiB
Bash
Executable File
181 lines
6.5 KiB
Bash
Executable File
#!/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 |