git 个人开发工作流

1618 字
8 分钟
git 个人开发工作流

日常工作流#

feature-a 某个上游的基线分支

feature-a-xxx 基于 feature-a 的本地开发分支

多人开发时,优先在 feature-a-xxx 上提交自己的代码,定时通过 rebase 同步上游 feature-a 的改动,最后 通过 merge 方式把本地的提交 合并回上游

feature-a
----A---------B-------------C-------------------M(merge back)----
\ \ /
\ \ /
feature-a-xxx
----a1--------a2----rebase to C----a3---

本地开发时,可能会存在多次临时的提交,建议通过 squash 将本地的提交整理后再 merge 回上游分支

feature-a
----A---------B---------C---------D-------------------------------M----
\ \ \ /
\ \ \ /
feature-a-xxx
----a1----a2----rebase----a3----rebase----a4----a5----S(squash)

以下脚本主要添加了两个方便快速操作的实现

grebase 在本地开发分支上快速 rebase 上游分支

gmergebase 将本地开发分支 merge 回上游分支

Terminal window
# ============================================
# Git personal branch helpers - safe edition
# --------------------------------------------
# 约定:
# 个人开发分支 = <基线分支>-<个人后缀>
# 或 = <基线分支>/<个人后缀>
#
# 示例:
# feature-a-sephy -> feature-a
# feature-order-refactor-jack -> feature-order-refactor
# feature-a/sephy -> feature-a
#
# 提供命令:
# gbase # 输出自动识别出的基线分支
# gplan # 预览 rebase / merge 计划
# grebase # 安全 rebase 到 origin/<base>
# grebase --autostash # 工作区不干净时自动暂存后 rebase
# gmergebase # merge 当前分支回基线分支,merge 后切回当前分支
# gmergebase --push # merge 后自动 push
# gmergebase --squash # squash merge
# gmergebase --push --delete
# # merge + push + 删除已合并个人分支(本地)
# ============================================
# ---------- 基础输出 ----------
__gpb_info() { printf '[INFO] %s\n' "$*"; }
__gpb_warn() { printf '[WARN] %s\n' "$*" >&2; }
__gpb_err() { printf '[ERR ] %s\n' "$*" >&2; }
# ---------- 基础判断 ----------
__gpb_in_git_repo() {
git rev-parse --is-inside-work-tree >/dev/null 2>&1
}
__gpb_current_branch() {
git branch --show-current 2>/dev/null
}
__gpb_branch_exists_local() {
local branch="$1"
git show-ref --verify --quiet "refs/heads/$branch"
}
__gpb_branch_exists_remote() {
local branch="$1"
git show-ref --verify --quiet "refs/remotes/origin/$branch"
}
__gpb_branch_exists_any() {
local branch="$1"
__gpb_branch_exists_local "$branch" || __gpb_branch_exists_remote "$branch"
}
__gpb_all_branches() {
{
git for-each-ref --format='%(refname:short)' refs/heads
git for-each-ref --format='%(refname:short)' refs/remotes/origin
} | sed 's#^origin/##' | grep -v '^HEAD$' | awk '!seen[$0]++'
}
# 工作区是否干净(含 staged / unstaged,忽略 untracked)
__gpb_worktree_clean() {
git diff --quiet && git diff --cached --quiet
}
# 是否存在未跟踪文件
__gpb_has_untracked() {
[ -n "$(git ls-files --others --exclude-standard)" ]
}
# 当前分支是否有未推送提交
__gpb_has_unpushed_commits() {
local current upstream
current="$(__gpb_current_branch)"
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)" || return 1
[ -n "$(git rev-list "${upstream}..HEAD" 2>/dev/null)" ]
}
# 当前分支是否落后于上游
__gpb_is_behind_upstream() {
local upstream
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)" || return 1
[ -n "$(git rev-list "HEAD..${upstream}" 2>/dev/null)" ]
}
# 基线分支是否包含当前开发分支(用于删除前校验)
__gpb_is_merged_into() {
local src="$1"
local target="$2"
git merge-base --is-ancestor "$src" "$target"
}
# ---------- 基线推导 ----------
__gpb_infer_base_branch() {
local current="$1"
local best=""
local candidate=""
[ -z "$current" ] && return 1
while IFS= read -r candidate; do
[ -z "$candidate" ] && continue
[ "$candidate" = "$current" ] && continue
case "$current" in
"$candidate"-*|"$candidate"/*)
if [ ${#candidate} -gt ${#best} ]; then
best="$candidate"
fi
;;
esac
done < <(__gpb_all_branches)
if [ -n "$best" ]; then
printf '%s\n' "$best"
return 0
fi
if [[ "$current" == *-* ]]; then
local by_dash="${current%-*}"
if __gpb_branch_exists_any "$by_dash"; then
printf '%s\n' "$by_dash"
return 0
fi
fi
if [[ "$current" == */* ]]; then
local by_slash="${current%/*}"
if __gpb_branch_exists_any "$by_slash"; then
printf '%s\n' "$by_slash"
return 0
fi
fi
return 1
}
gbase() {
__gpb_in_git_repo || { __gpb_err "Not a git repository"; return 1; }
local current base
current="$(__gpb_current_branch)"
[ -n "$current" ] || { __gpb_err "Cannot determine current branch"; return 1; }
base="$(__gpb_infer_base_branch "$current")" || {
__gpb_err "Cannot infer base branch from current branch: $current"
return 1
}
printf '%s\n' "$base"
}
# ---------- 计划预览 ----------
gplan() {
__gpb_in_git_repo || { __gpb_err "Not a git repository"; return 1; }
local current base
current="$(__gpb_current_branch)"
[ -n "$current" ] || { __gpb_err "Cannot determine current branch"; return 1; }
base="$(__gpb_infer_base_branch "$current")" || {
__gpb_err "Cannot infer base branch from current branch: $current"
return 1
}
cat <<EOF
Current branch : $current
Base branch : $base
Rebase:
git fetch origin --prune
git rebase origin/$base
Merge back:
git fetch origin --prune
git checkout $base
git pull --ff-only origin $base
git merge --no-ff $current
git checkout $current
EOF
}
# ---------- 安全 rebase ----------
grebase() {
__gpb_in_git_repo || { __gpb_err "Not a git repository"; return 1; }
local autostash=0
while [ $# -gt 0 ]; do
case "$1" in
--autostash) autostash=1 ;;
*)
__gpb_err "Unknown option: $1"
return 1
;;
esac
shift
done
local current base stashed=0 stash_msg
current="$(__gpb_current_branch)"
[ -n "$current" ] || { __gpb_err "Cannot determine current branch"; return 1; }
base="$(__gpb_infer_base_branch "$current")" || {
__gpb_err "Cannot infer base branch from current branch: $current"
return 1
}
if [ "$current" = "$base" ]; then
__gpb_err "Current branch is already the base branch: $base"
return 1
fi
__gpb_info "Current branch : $current"
__gpb_info "Base branch : $base"
if ! __gpb_worktree_clean; then
if [ "$autostash" -eq 1 ]; then
stash_msg="grebase-autostash-$(date +%Y%m%d%H%M%S)"
__gpb_warn "Working tree is dirty, auto stashing changes: $stash_msg"
git stash push -u -m "$stash_msg" >/dev/null || {
__gpb_err "git stash failed"
return 1
}
stashed=1
else
__gpb_err "Working tree is dirty. Commit/stash first, or use: grebase --autostash"
return 1
fi
fi
git fetch origin --prune || {
[ "$stashed" -eq 1 ] && __gpb_warn "Your changes are stashed. Restore manually if needed: git stash list"
return 1
}
if ! __gpb_branch_exists_remote "$base"; then
__gpb_err "Remote branch origin/$base does not exist"
[ "$stashed" -eq 1 ] && __gpb_warn "Your changes are stashed. Restore manually if needed: git stash pop"
return 1
fi
if ! git rebase "origin/$base"; then
__gpb_err "Rebase failed. Resolve conflicts, then run:"
printf ' git add <files>\n'
printf ' git rebase --continue\n'
printf ' # or git rebase --abort\n'
[ "$stashed" -eq 1 ] && __gpb_warn "Auto-stash was created. Apply it after rebase is fully done."
return 1
fi
if [ "$stashed" -eq 1 ]; then
__gpb_info "Restoring stashed changes"
if ! git stash pop; then
__gpb_warn "git stash pop had conflicts or was not fully applied. Check: git stash list"
return 1
fi
fi
__gpb_info "Rebase completed successfully"
}
# ---------- 安全 merge back ----------
gmergebase() {
__gpb_in_git_repo || { __gpb_err "Not a git repository"; return 1; }
local do_push=0
local do_squash=0
local do_delete=0
while [ $# -gt 0 ]; do
case "$1" in
--push) do_push=1 ;;
--squash) do_squash=1 ;;
--delete) do_delete=1 ;;
*)
__gpb_err "Unknown option: $1"
return 1
;;
esac
shift
done
local dev base original_branch
original_branch="$(__gpb_current_branch)"
dev="$original_branch"
[ -n "$dev" ] || { __gpb_err "Cannot determine current branch"; return 1; }
base="$(__gpb_infer_base_branch "$dev")" || {
__gpb_err "Cannot infer base branch from current branch: $dev"
return 1
}
if [ "$dev" = "$base" ]; then
__gpb_err "Current branch is already the base branch: $base"
return 1
fi
__gpb_info "Dev branch : $dev"
__gpb_info "Base branch : $base"
if ! __gpb_worktree_clean; then
__gpb_err "Working tree is dirty. Please commit/stash first."
return 1
fi
if __gpb_has_untracked; then
__gpb_warn "There are untracked files in working tree"
fi
git fetch origin --prune || return 1
if __gpb_has_unpushed_commits; then
__gpb_warn "Current branch has unpushed commits"
__gpb_warn "Consider pushing first for backup"
fi
if __gpb_is_behind_upstream; then
__gpb_warn "Current branch is behind its upstream"
fi
if ! __gpb_branch_exists_any "$base"; then
__gpb_err "Base branch does not exist locally/remotely: $base"
return 1
fi
git checkout "$base" || return 1
if __gpb_branch_exists_remote "$base"; then
git pull --ff-only origin "$base" || {
__gpb_err "Failed to fast-forward local $base from origin/$base"
git checkout "$original_branch" >/dev/null 2>&1
return 1
}
fi
if [ "$do_squash" -eq 1 ]; then
__gpb_info "Running squash merge"
git merge --squash "$dev" || {
__gpb_err "Squash merge failed"
git checkout "$original_branch" >/dev/null 2>&1
return 1
}
__gpb_warn "Squash merge staged changes only; commit manually:"
printf ' git commit -m "merge %s into %s"\n' "$dev" "$base"
git checkout "$original_branch" >/dev/null 2>&1 || true
return 0
fi
__gpb_info "Running merge --no-ff"
git merge --no-ff "$dev" || {
__gpb_err "Merge failed. Resolve conflicts, then commit."
__gpb_warn "After merge is completed manually, you may switch back with: git checkout $original_branch"
return 1
}
if [ "$do_push" -eq 1 ]; then
__gpb_info "Pushing $base to origin"
git push origin "$base" || {
__gpb_err "Push failed"
git checkout "$original_branch" >/dev/null 2>&1 || true
return 1
}
fi
if [ "$do_delete" -eq 1 ]; then
if __gpb_is_merged_into "$dev" "$base"; then
__gpb_info "Deleting merged local branch: $dev"
git branch -d "$dev" || {
__gpb_warn "Failed to delete local branch: $dev"
}
else
__gpb_warn "Skip deleting $dev because it is not fully merged into $base"
fi
fi
__gpb_info "Switching back to original branch: $original_branch"
git checkout "$original_branch" >/dev/null 2>&1 || {
__gpb_warn "Failed to switch back automatically. Please run: git checkout $original_branch"
return 0
}
__gpb_info "Merge back completed successfully"
}
# ---------- 可选快捷别名 ----------
alias grb='grebase'
alias gmb='gmergebase'

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

git 个人开发工作流
https://blog.sephy.top/posts/git-个人开发工作流/
作者
虾米
发布于
2026-03-27
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
虾米
coder
分类
标签
站点统计
文章
61
分类
4
标签
52
总字数
64,663
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.13.9
文章许可
CC BY-NC-SA 4.0