Git是目前最流行的分布式版本控制系统,几乎每个程序员都在使用。但是,很多人对Git的使用还停留在基础层面:git add、git commit、git push、git pull,遇到冲突就头疼,遇到复杂场景就不知所措。

其实,Git的功能非常强大,掌握一些高级技巧,能大大提升开发效率,让版本管理更加优雅和高效。2016年,Git已经成为版本控制的绝对主流,GitHub、GitLab、Bitbucket等平台都基于Git,掌握Git高级技巧是每个程序员的必备技能。

今天就来深入讲解Git的高级技巧,包括rebase变基、cherry-pick精选提交、stash暂存、交互式rebase、reset回退、reflog恢复,以及常见的Git工作流。

一、rebase(变基)

1. 什么是rebase

rebase(变基)是Git中另一种合并分支的方式。与merge不同,merge会创建一个新的合并提交,而rebase会把当前分支的提交"移动"到目标分支的最新提交之后,形成一条线性的提交历史。

举个例子,假设你从master分支切出一个feature分支,在feature分支上做了3次提交(C4、C5、C6),同时master分支上也有了新的提交(C7、C8):

master:   C1 -- C2 -- C3 -- C7 -- C8
                    \
feature:             C4 -- C5 -- C6

如果用merge合并:

git checkout feature
git merge master

结果会创建一个新的合并提交C9:

master:   C1 -- C2 -- C3 -- C7 -- C8
                    \               \
feature:             C4 -- C5 -- C6 -- C9

如果用rebase变基:

git checkout feature
git rebase master

结果会把C4、C5、C6"移动"到C8之后,形成线性历史:

master:   C1 -- C2 -- C3 -- C7 -- C8
                                        \
feature:                                 C4' -- C5' -- C6'

注意:C4'、C5'、C6'是新的提交,虽然内容和C4、C5、C6一样,但commit hash不同。

2. rebase vs merge

对比项mergerebase
提交历史保留分支结构,有合并提交线性历史,无合并提交
安全性安全,不改变已有提交会改变提交历史,已推送的分支慎用
冲突处理一次解决所有冲突逐个提交解决冲突,可能多次冲突
适用场景公共分支、多人协作个人分支、整理提交历史
可读性能看到分支合并关系历史清晰简洁,像一条直线

3. 什么时候用rebase

  • ✅ 个人开发分支,想保持提交历史线性整洁
  • ✅ 把master的最新更新同步到feature分支,避免分叉
  • ✅ 合并前整理提交历史,让PR/MR更清晰
  • ❌ 已经推送到远程的公共分支(会改变历史,导致其他人冲突)
  • ❌ 多人协作的分支(其他人可能基于旧的提交开发)

黄金法则:不要对已经推送到公共仓库的分支执行rebase。rebase只用于本地未推送的分支,或者你确定只有自己一个人使用的分支。

4. rebase冲突处理

rebase过程中如果遇到冲突,Git会暂停,让你解决冲突:

# 解决冲突后
git add <冲突文件>
git rebase --continue  # 继续rebase

# 如果想放弃rebase
git rebase --abort  # 回到rebase之前的状态

# 如果想跳过当前提交
git rebase --skip

注意:rebase可能会在多个提交上遇到冲突,需要逐个解决,这比merge一次性解决所有冲突更麻烦。这也是很多人不喜欢rebase的原因之一。

5. 交互式rebase(git rebase -i)

交互式rebase是Git中最强大的功能之一,可以让你重新编辑、合并、删除、重新排序提交。

# 对最近3次提交进行交互式rebase
git rebase -i HEAD~3

执行后会打开编辑器,显示类似内容:

pick a1b2c3d 提交1:添加登录功能
pick d4e5f6g 提交2:修复登录bug
pick h7i8j9k 提交3:优化登录性能

# 命令:
# p, pick = 使用该提交
# r, reword = 使用该提交,但修改提交信息
# e, edit = 使用该提交,但暂停以便修改
# s, squash = 使用该提交,但合并到前一个提交
# f, fixup = 类似squash,但丢弃该提交的信息
# x, exec = 执行命令

你可以修改每一行前面的命令,实现:

  • reword:修改提交信息
  • squash:把多个提交合并成一个(保留所有提交信息)
  • fixup:把多个提交合并成一个(丢弃被合并的提交信息)
  • edit:暂停rebase,修改提交内容
  • drop:删除某个提交(直接删除该行)
  • 重新排序:调整行的顺序

例如,把3个提交合并成1个:

pick a1b2c3d 提交1:添加登录功能
squash d4e5f6g 提交2:修复登录bug
squash h7i8j9k 提交3:优化登录性能

保存后,Git会让你编辑合并后的提交信息,最终生成一个包含所有更改的提交。

交互式rebase是整理提交历史的利器,让你的提交历史清晰、有意义。

二、cherry-pick(精选提交)

1. 什么是cherry-pick

cherry-pick可以把另一个分支上的某个(或某几个)提交"挑选"出来,应用到当前分支。不同于merge会合并整个分支,cherry-pick只合并指定的提交。

使用场景:

  • 只需要某个分支上的一个或几个提交,不需要整个分支
  • 把bug修复从master分支应用到release分支
  • 把某个功能提交从feature分支应用到另一个feature分支
  • 误提交到错误分支,把提交cherry-pick到正确分支

2. 基本用法

# 把某个提交应用到当前分支
git cherry-pick <commit-hash>

# 把多个提交应用到当前分支
git cherry-pick <commit1> <commit2> <commit3>

# 把一个范围的提交应用到当前分支(不包含start,包含end)
git cherry-pick <start>..<end>

# 包含start的范围
git cherry-pick <start>^..<end>

例如,把feature分支上的abc1234提交应用到当前master分支:

git checkout master
git cherry-pick abc1234

3. cherry-pick冲突处理

cherry-pick过程中如果遇到冲突:

# 解决冲突后
git add <冲突文件>
git cherry-pick --continue  # 继续

# 放弃cherry-pick
git cherry-pick --abort

# 跳过当前提交
git cherry-pick --skip

4. 只应用更改,不自动提交

# -n 或 --no-commit:只应用更改到工作区,不自动提交
git cherry-pick -n <commit-hash>
# 然后你可以修改后手动提交
git commit -m "自定义提交信息"

5. cherry-pick注意事项

  • cherry-pick会生成新的提交(commit hash不同),不是移动原提交
  • 不要对已经推送的公共分支cherry-pick后又rebase,会导致历史混乱
  • 如果需要把多个提交从一个分支移到另一个分支,且这些提交是连续的,考虑用rebase --onto
  • cherry-pick后,原分支上的提交仍然存在,不会被删除

三、stash(暂存)

1. 什么是stash

stash可以把当前工作区的修改暂时"储藏"起来,让工作区恢复干净,等以后再恢复。

使用场景:

  • 正在开发新功能,突然需要切换到其他分支修复bug,但当前修改还不想提交
  • 切换分支时,工作区有未提交的修改会导致冲突,先stash再切换
  • 临时需要保存当前工作状态,去做其他事情

2. 基本用法

# 暂存当前工作区的修改(包括已add和未add的,但不包括未跟踪的新文件)
git stash

# 暂存时添加说明,方便以后识别
git stash save "正在开发登录功能,临时暂存"

# 暂存包括未跟踪的新文件(-u 或 --include-untracked)
git stash -u

# 暂存所有文件(包括忽略的文件,-a 或 --all)
git stash -a

3. 查看stash列表

# 查看所有stash
git stash list
# 输出示例:
# stash@{0}: On feature/login: 正在开发登录功能,临时暂存
# stash@{1}: On master: 修复首页样式

# 查看某个stash的详细内容
git stash show stash@{0}
git stash show -p stash@{0}  # 显示diff详情

4. 恢复stash

# 恢复最近的stash,并从stash列表中删除
git stash pop

# 恢复指定的stash,并从列表中删除
git stash pop stash@{1}

# 恢复stash,但不从列表中删除(可以多次恢复)
git stash apply
git stash apply stash@{1}

5. 删除stash

# 删除指定的stash
git stash drop stash@{0}

# 清空所有stash
git stash clear

6. 从stash创建分支

如果stash的修改和当前分支冲突太多,可以从stash创建一个新分支:

# 从stash创建新分支并恢复stash
git stash branch new-branch-name stash@{0}

7. stash注意事项

  • stash是全局的,不绑定分支,可以在任何分支恢复
  • stash默认不包含未跟踪的新文件,需要加-u参数
  • stash可以堆叠,最新的stash是stash@{0}
  • pop会删除stash,apply不会删除,根据需要选择
  • 建议用save添加说明,否则stash列表只有分支名,不好识别

四、reset(回退)

1. 什么是reset

reset可以把当前分支的HEAD指针移动到指定提交,同时可以选择如何处理工作区和暂存区的修改。

2. reset的三种模式

模式HEAD移动暂存区(index)工作区(working tree)说明
--soft不变不变只移动HEAD,修改保留在暂存区,相当于撤销commit但保留add
--mixed(默认)重置为指定提交不变移动HEAD,暂存区重置,修改保留在工作区,相当于撤销commit和add
--hard重置为指定提交重置为指定提交移动HEAD,暂存区和工作区都重置,彻底丢弃修改,危险!

3. 常用场景

# 撤销最近一次commit,但保留修改在暂存区(可以重新commit)
git reset --soft HEAD~1

# 撤销最近一次commit和add,保留修改在工作区(可以重新add和commit)
git reset --mixed HEAD~1
# 或(默认就是mixed)
git reset HEAD~1

# 彻底撤销最近一次commit,丢弃所有修改(危险!)
git reset --hard HEAD~1

# 回退到指定提交
git reset --hard <commit-hash>

# 撤销git add(把暂存区的文件放回工作区)
git reset HEAD <file>
# 或
git restore --staged <file>

4. reset注意事项

  • --hard会彻底丢弃工作区修改,不可恢复(除非用reflog),使用前确认
  • 不要对已经推送到远程的分支执行reset --hard,会导致历史不一致
  • 如果已经推送,需要用git revert(创建反向提交)而不是reset
  • reset --soft常用于合并多个提交:reset --soft到某个提交,然后重新commit

五、reflog(引用日志)

1. 什么是reflog

reflog记录了HEAD和分支引用的所有变化历史,包括commit、reset、rebase、merge等操作。即使你用reset --hard"删除"了提交,reflog中仍然有记录,可以恢复。

reflog是Git的"后悔药",几乎所有操作都可以通过reflog恢复。

2. 基本用法

# 查看HEAD的reflog
git reflog

# 查看指定分支的reflog
git reflog show master

# 输出示例:
# abc1234 (HEAD -> master) HEAD@{0}: commit: 添加登录功能
# def5678 HEAD@{1}: reset: moving to HEAD~1
# ghi9012 HEAD@{2}: commit: 修复bug
# ...

3. 用reflog恢复误操作

场景1:不小心用reset --hard删除了提交,想恢复:

git reflog  # 找到删除前的commit hash,假设是abc1234
git reset --hard abc1234  # 恢复

场景2:rebase过程中出错,想回到rebase前:

git reflog  # 找到rebase前的状态,假设是HEAD@{5}
git reset --hard HEAD@{5}

场景3:误删了分支,想恢复:

git reflog  # 找到分支被删除前的commit hash
git branch <branch-name> <commit-hash>  # 重建分支

4. reflog注意事项

  • reflog默认保留90天(可通过gc.reflogExpire配置)
  • reflog是本地的,不会推送到远程
  • 定期执行git gc会清理过期的reflog
  • 只要reflog中还有记录,几乎所有操作都可以恢复,所以误操作后不要慌,先看reflog

六、其他实用技巧

1. 查看提交历史

# 简洁的单行显示
git log --oneline

# 图形化显示分支合并关系
git log --oneline --graph --all

# 显示最近N次提交
git log -n 10

# 按作者筛选
git log --author="张三"

# 按时间筛选
git log --since="2016-01-01" --until="2016-06-01"

# 搜索提交信息
git log --grep="登录"

# 显示某个文件的修改历史
git log -p <file>

# 显示每次提交的统计信息
git log --stat

2. 查看差异

# 查看工作区和暂存区的差异
git diff

# 查看暂存区和最新提交的差异
git diff --cached
# 或
git diff --staged

# 查看工作区和最新提交的差异
git diff HEAD

# 查看两个提交之间的差异
git diff <commit1> <commit2>

# 查看某个文件的差异
git diff <file>

# 只显示变更的文件名
git diff --name-only

3. 撤销修改

# 撤销工作区的修改(恢复到最新提交状态)
git checkout -- <file>
# 或(Git 2.23+)
git restore <file>

# 撤销暂存区的修改(放回工作区)
git reset HEAD <file>
# 或
git restore --staged <file>

# 撤销某次提交(创建一个反向提交,安全,适合已推送的分支)
git revert <commit-hash>

4. 标签管理

# 查看所有标签
git tag

# 创建轻量标签
git tag v1.0.0

# 创建附注标签(推荐,包含标签信息、创建者、日期等)
git tag -a v1.0.0 -m "版本1.0.0发布"

# 给指定提交打标签
git tag -a v1.0.0 <commit-hash> -m "版本1.0.0"

# 查看标签信息
git show v1.0.0

# 推送标签到远程
git push origin v1.0.0

# 推送所有标签
git push origin --tags

# 删除本地标签
git tag -d v1.0.0

# 删除远程标签
git push origin --delete v1.0.0

5. 远程仓库管理

# 查看远程仓库
git remote -v

# 添加远程仓库
git remote add origin <url>

# 修改远程仓库URL
git remote set-url origin <new-url>

# 删除远程仓库
git remote remove origin

# 拉取远程更新但不合并
git fetch origin

# 拉取并合并(= fetch + merge)
git pull origin master

# 推送到远程
git push origin master

# 推送并设置上游分支(以后可以直接git push)
git push -u origin master

# 查看远程分支
git branch -r

# 删除远程分支
git push origin --delete <branch-name>

七、Git工作流

1. Git Flow

Git Flow是最经典的Git工作流,适合有明确版本发布周期的项目。

分支模型:

  • master:生产环境代码,随时可发布
  • develop:开发主分支,集成最新开发功能
  • feature/*:功能分支,从develop切出,开发完成后合并回develop
  • release/*:发布分支,从develop切出,用于发布前的测试和bug修复,完成后合并到master和develop
  • hotfix/*:热修复分支,从master切出,修复生产环境bug,完成后合并到master和develop

优点:分支职责清晰,适合版本发布明确的项目 缺点:分支多,流程复杂,不适合快速迭代和持续部署

2. GitHub Flow

GitHub Flow是更简单的工作流,适合持续部署的项目。

流程:

  1. 从master切出feature分支
  2. 在feature分支上开发和提交
  3. 提交Pull Request(PR)
  4. 代码审查和讨论
  5. 合并到master
  6. 部署

优点:简单、灵活,适合快速迭代和持续部署 缺点:没有明确的版本概念,依赖自动化测试和部署

3. GitLab Flow

GitLab Flow介于Git Flow和GitHub Flow之间,结合了两者的优点,有环境分支(pre-production、production)但流程更简单。

4. 如何选择

  • 有明确版本发布周期(如桌面软件、App)→ Git Flow
  • 持续部署、快速迭代(如Web应用、SaaS)→ GitHub Flow
  • 介于两者之间 → GitLab Flow
  • 个人项目、小团队 → 简单的分支模型即可,不必拘泥于流程

八、常见问题与解决方案

1. 提交信息写错了怎么办?

# 修改最近一次提交的信息
git commit --amend -m "新的提交信息"

# 如果已经推送,需要强制推送(慎用)
git push --force

2. 把不该提交的文件提交了怎么办?

# 从Git中移除但保留本地文件
git rm --cached <file>
echo "<file>" >> .gitignore
git commit -m "移除误提交的文件"

# 如果是敏感信息(如密码),需要彻底从历史中清除
# 使用git filter-branch或BFG Repo-Cleaner

3. 合并冲突太多怎么办?

# 放弃合并,回到合并前
git merge --abort

# 使用可视化工具解决冲突
git mergetool

# 只接受某一方的版本
git checkout --ours <file>  # 接受当前分支
git checkout --theirs <file>  # 接受合并进来的分支

4. 大文件导致仓库臃肿怎么办?

# 查看大文件
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sort -k3 -n -r | head -20

# 使用Git LFS管理大文件
git lfs install
git lfs track "*.psd"
git add .gitattributes

5. .gitignore不生效怎么办?

.gitignore只能忽略未跟踪的文件,如果文件已经被Git跟踪,需要先移除:

git rm -r --cached .
git add .
git commit -m "重新应用.gitignore"

总结

Git是一个功能强大的版本控制系统,掌握高级技巧能大大提升开发效率。本文讲解了:

  1. rebase:变基,保持线性提交历史,注意不要对公共分支使用
  2. cherry-pick:精选提交,只合并需要的提交
  3. stash:暂存,临时保存工作区修改
  4. reset:回退,三种模式(soft/mixed/hard)区别使用
  5. reflog:引用日志,Git的"后悔药",几乎所有误操作都能恢复
  6. 工作流:Git Flow、GitHub Flow、GitLab Flow,根据项目特点选择

Git的学习曲线比较陡,但是一旦掌握,会成为你开发中不可或缺的工具。建议多练习、多使用,遇到问题多查文档和reflog。记住:Git几乎所有操作都是可恢复的,所以不要害怕尝试。

最后,推荐几个学习Git的好资源:

  • Pro Git(免费电子书,Git官方推荐)
  • Git Cheat Sheet(Git速查表)
  • Learn Git Branching(交互式Git学习网站)
  • Git官方文档

希望本文能帮助你更深入地理解和使用Git,让版本管理更加高效和优雅。