写git命令的文章太多了,但大部分要么是命令字典式的罗列,背完就忘,要么只讲命令不讲原理,换个场景就不会用了。我这些年用git踩过不少坑,也帮同事处理过各种奇奇怪怪的问题,最后发现真正管用的不是死记硬背多少命令,而是把git的几个核心机制搞明白,剩下的都是推理。这篇就把我日常最高频、最容易出问题、以及关键时刻能用到的git操作做个汇总,配套原理讲解和实战注意事项,照着来能省下不少上网搜索的时间。
1. 整体思路与底层机制:先理解数据流,再记命令
1.1 git的“三个区”模型是理解一切操作的地基
很多人学git上来就敲命令,add、commit、push循环两周看似会用了,一遇到reset、rebase、cherry-pick就懵。根子在于没搞懂git的数据到底是怎么流动的。把git的数据流动想清楚,至少八成命令你能自己推理出作用。
git的数据流转涉及四个关键区域:
- 工作区(Working Directory):你电脑上实际可见、可编辑的文件目录。
- 暂存区(Staging Area / Index):一个临时保存区,记录你准备纳入下一次提交的文件快照,你可以把它想象成购物车。
- 本地仓库(Local Repository):在你机器上存储所有历史提交记录的.git目录,每次提交就是这个仓库中一个不可变的数据快照节点。
- 远程仓库(Remote Repository):部署在服务器上的公共版本库,多人协作进行push和pull的目的地。
日常操作的数据流是:工作区通过git add把内容放入暂存区,暂存区通过git commit把内容生成为本地仓库中的一个提交,本地仓库通过git push推送到远程仓库。反向流则是git pull或git fetch,把远程仓库内容取回本地。
这个模型的关键点在暂存区。它意味着提交不是直接“抓取”工作区当前状态,而是只提交你明确放入暂存区的内容。
注意:这是git和SVN最大的体验差异之一。SVN的commit就是直接把本地已修改的文件全部提交,而git需要你先把要提交的内容“选中”到暂存区。好处是你可以把一次开发中的不同文件改动拆分成多个逻辑独立的提交,坏处是新手经常忘掉add导致提交了旧版本。
1.2 对象模型与分支本质:为什么git是“内容寻址”的
git底层本质上是一个内容寻址文件系统。当你执行git add时,git会把文件内容做哈希(SHA-1),生成一个41位的十六进制对象存入.git目录。每次commit则生成一个包含父提交哈希、作者信息、提交说明和一棵目录树(tree)哈希的commit对象。
这种设计带来了几个日后会经常觉得“真香”的特性:
- 提交不可变:一旦生成,提交对象的内容就不能被修改,所谓“修改历史”其实是创建了新的提交对象并替换引用。
- 分支本质是指针:一个分支名只是指向某个提交对象的可变指针,而你内心想象的“分支是分叉的线”其实是git基于提交的父子关系画出来的图。
- HEAD是当前指针:它指向当前分支名(而不是直接指向提交),所以切换分支就是移动HEAD的指向。
理清这些之后,你再看git merge、git rebase、git reset时就会意识到:日常操作的本质就是移动指针、创建提交、重写引用,而不是“神奇地改动文件”。
在实际排查问题的时候,这个模型也特别有用。遇到“文件状态不对”“分支历史莫名其妙的多了个提交”这类问题,先别慌,顺着数据流检查:改动在工作区还是暂存区?本地仓库和远程仓库在哪里分叉?HEAD现在指向哪个提交?想清楚这个链条,问题位置基本就能定位到七八成。
2. 初始化与基础提交:每天必用的高频操作
2.1 init与clone的细节:从零开始正确建仓
git init
在已有工程目录里初始化仓库,git init之后再配置用户信息:
git init git config user.name "your name" git config user.email "your_email@example.com"有几个实际工作中的细节值得注意:
- 不推荐直接
git init后立刻在新克隆环境提交。建议先准备一个合理的.gitignore,把语言构建产物、依赖目录、本地配置文件排除,否则后续提交会把你不想入库的文件全带进来。 - 初始化时的分支名取决于git版本和全局配置。新版git默认
main,老版本默认master。如果希望统一,可以设置git config --global init.defaultBranch main。在团队协作中,分支名不一致会带来不必要的困扰,最好统一约定。 - init后可以马上
symbolic-ref查看HEAD实际指向的分支:git symbolic-ref HEAD。
git clone
克隆远程仓库的常规用法是git clone <repo_url>,但在实际场景里更建议加上必要的参数:
# 浅克隆:只拉取最近一次提交,适合大型仓库快速预览或CI环境 git clone --depth 1 <repo_url> # 指定分支克隆 git clone -b <branch_name> --single-branch <repo_url> # 克隆后启用稀疏检出,只检出需要的子目录 git clone --filter=blob:none --sparse <repo_url>浅克隆可以大幅节省拉取时间,但对于需要完整历史进行开发协作的仓库不适合。--filter=blob:none配合--sparse能在不拉取全部分支文件内容的情况下保留完整提交结构,适合在超大代码仓库中只想关注某个子模块的场景。
2.2 add与commit:提交的正确姿势与心智模型
git add的层级
git add不是单纯“把文件加入版本管理”,它的本质是把当前工作区中的文件快照写入暂存区。因此有以下几个高频用途:
git add <file> # 精确添加单文件 git add . # 添加当前目录下所有改动(注意会忽略.gitignore) git add -u # 只添加已跟踪文件的修改和删除,不添加新文件 git add -A # 添加所有改动,包括删除和新文件 git add -p # 交互式按块暂存,只提交某个文件中的部分修改git add -p是我强烈推荐习惯性使用的一个命令。它会把文件里的改动拆成多个“hunk”,让你选择y(暂存)、n(不暂存)、s(拆成更小块)、e(手动编辑)等。实际操作中,经常遇到一个文件里同时包含两处bug修复、一处代码格式化,用-p就可以拆成两个语义清晰的提交,后续排查与回滚都方便得多。
.gitignore语法上核心就是几条规则:
# 注释:注释行。*.log:匹配任意目录下所有.log文件(但注意,如果前缀包含/,比如/build,则只匹配根目录下的build目录)。logs/:匹配任意层级名为logs的目录。- 常见坑:如果某文件已经被git跟踪,中途再写进.gitignore就不会生效,需要
git rm --cached <file>将文件移出跟踪后再依赖ignore规则。
git commit
# 常规提交 git commit -m "fix: 修复登录接口超时问题" # 如果暂存区没有内容但工作区有已跟踪文件的修改,可以用 -a 直接暂存并提交 git commit -am "docs: 更新接口文档" # 修改最后一次提交信息,或补充漏提交的文件 git commit --amend -m "新的提交说明" git add <file> && git commit --amend --no-edit.gitignore与commit细节,有几条实操中掉过坑的经验:
- 提交信息不要只写“fix bug”,要写清楚“修了什么、影响什么模块”,这直接关系到日后用
git log搜索历史时能否快速定位。 --amend只适合修改尚未推送到远程的提交。如果已经push,amend会创造一个与远程不同哈希的提交,后续push必须使用--force-with-lease,这会覆盖远端历史,影响同事工作。
# 日常提交前养成查看状态的习惯 git statusgit status是信息量最大的命令,没有之一。它会明确告诉你当前分支领先/落后远程多少提交,哪些文件处于staged状态,哪些未被暂存。实际使用中,我的固定流程是:
git status # 查看整体状态 git diff --stat # 查看改了几行、涉及哪些文件 git diff --cached # 查看即将提交的内容 git add <file> git commit -m "type: 说明信息"2.3 status、log与diff的实用技巧
git status
看状态时最忌讳的是一眼扫过,然后凭感觉add。建议每次认真看三行:第一行当前分支、第二行有没有未暂存变更、第三行有没有未跟踪文件。熟练后,这能避免大量“我明明改了为什么不生效”“怎么提交了旧文件”的疑问。
git log
# 简洁单行历史 git log --oneline # 图形化查看分支历史 git log --graph --oneline --all # 查看指定文件的历史 git log --follow --oneline <file> # 按作者和日期过滤 git log --author="某开发者" --since="2024-01-01" --until="2024-12-31" --onelinegit log配合几个参数能快速回答“这个改动是谁在哪次提交引入的”,配合git blame还能定位到具体行。
git diff
# 工作区与暂存区比较 git diff # 暂存区与HEAD比较(即将提交的内容) git diff --cached # 两个提交之间的差异 git diff <commit1> <commit2>做代码审查时我最常用的是git diff --stat一眼看改动规模,再针对具体文件看详情。
3. 分支管理与合并策略:多人协作的“交通规则”
3.1 分支创建、切换、删除与跟踪
分支是git里最划时代的设计。它轻量得惊人,创建分支几乎瞬间完成,因为本质上就是创建一个指针。
# 查看分支 git branch # 本地分支,前面带*的是当前分支 git branch -a # 本地加远程跟踪分支 git branch -vv # 本地分支与远程跟踪分支的对应关系 # 创建与切换 git branch <branch_name> # 基于HEAD创建 git checkout -b <branch_name> # 创建并切换(老派写法) git switch -c <branch_name> # 创建并切换(git 2.23+推荐写法) # 删除分支 git branch -d <branch_name> # 安全删除,只允许删除已合并分支 git branch -D <branch_name> # 强制删除,未合并也删 # 重命名 git branch -m <old_name> <new_name>特别说明一下git switch。新版git把checkout拆成了两条语义清晰的命令:switch只负责切换分支,restore只负责恢复文件。在实际使用中,我强烈建议用switch来切分支,因为它不会像git checkout一样带有一堆历史包袱,误操作风险更低。
分支跟踪关系是很重要的概念。跟踪分支是本地分支与远程分支的映射关系。当你git push -u origin branch_name时,本地分支就绑定到远程分支上,此后git pull/git push不再需要手动指定远程和分支名。
查看本地分支与远程分支对应关系,一个命令:
git branch -vv如果显示[origin/main]表示已跟踪,没有则说明这是本地独有分支。
3.2 merge的两种模式与冲突处理
合并分支的本质是把另一条分支的提交“并入”当前分支。git merge <branch>会尝试把目标分支的提交和当前分支的提交整合起来。
fast-forward(快进合并)
如果当前分支是目标分支的上游祖先,git会把当前分支指针直接向前移动,不产生新提交。比如从main分出feature分支,main没有新提交,feature完成后合并回main,就是fast-forward。
three-way merge(三方合并)
如果两个分支在分叉之后各自都有新提交,git会找出两个分支的共同祖先提交,再把三方(共同祖先、当前分支、目标分支)的内容做合并,最终生成一个新提交。
A---B---C feature / D---E---F main三方合并的常见问题是同一个文件的同一行被两个分支同时做了不同修改,git无法自行判断,只能把冲突标记写进文件,等待人工处理。
冲突解决的标准步骤:
# 合并时报冲突,先看冲突文件 git status # 编辑文件,搜索 <<<<<<<、=======、>>>>>>> 标记 vim <conflict_file> # 修改完成后 git add <conflict_file> git commit -m "merge: 合并feature分支到main"冲突解决有个容易忽略的点:当你看到<<<<<<< HEAD到=======之间是当前分支内容,=======到>>>>>>> branch之间是待合并分支内容。千万别直接整段删掉,要逐段判断保留哪边,还是两边都要保留。
实战中还有一个好用的辅助思路:在解决大文件冲突前,先看一遍git diff中对冲突块的标注,很多冲突实际上只有几处关键逻辑需要人判断,其余click“accept theirs”或“accept ours”即可,如果用的是VS Code这类带图形界面的工具,这个操作很顺手。
3.3 rebase:重写提交历史的利与弊
rebase的核心逻辑:把一个分支上的所有提交,重新“嫁接”到另一个分支的顶端。它的内部实现是:找到当前分支与目标分支的共同祖先,把当前分支的提交逐一摘下来,在目标分支的最新提交之上重新应用一遍,生成一系列全新提交。
# 将当前分支的提交重放到 main 分支顶端 git rebase main # 交互式rebase,可以合并提交、修改信息、删除提交 git rebase -i HEAD~3rebase -i HEAD~3进入交互界面后,会出现类似下面的列表:
pick a1b2c3 提交信息1 pick d4e5f6 提交信息2 pick g7h8i9 提交信息3常用操作是:将squash与上一个pick合并为一个提交,reword修改提交说明,drop删除提交。调整提交顺序也能直接改行序。
merge与rebase的选择是我个人认为git操作里最体现工程理念的地方。我的经验和很多团队的工作流结论基本一致:
- 公共分支(比如main/dev/release):多用
merge,保留历史分叉和合并点,这样每个发布节点、每个业务合并的上下文都是清晰的。 - 个人功能分支(feature/xxx):多用
rebase,把开发过程中的零碎提交整理成一个或多个逻辑完整的提交,让最终合并进主干时历史像一条直线,方便后续查看演进。
用rebase有一个明确不能打破的禁忌:无论如何,不要rebase已经推送到远程并且别人可能拉取过的分支,这相当于改写公共历史,会把人家的仓库搞成一团乱麻。如果已经不小心做了,唯一的补救方式就是和团队沟通清楚,让大家重新基于你的新历史同步。
3.4 合并策略的工程经验
实际工作中,完整的一个功能从开发到合入的流程,我是这样走的:
# 1. 从最新主干上分出新分支 git switch main git pull git switch -c feature/user-auth # 2. 开发过程中,经常把主干的最新更新合并进来,减少后期冲突 git fetch origin git rebase origin/main # 3. 功能完成,整理提交 git rebase -i HEAD~n # 合并琐碎提交 # 4. 合并回主干,强制保留合并记录(避免fast-forward) git switch main git pull git merge --no-ff feature/user-auth -m "merge: 用户认证功能"用--no-ff可以强制产生一个合并提交,即使当前分支可以fast-forward。这么做的好处是保留“有一个明确的合并节点”这个上下文,回滚时可以一眼找到“哪个合并引入了这个功能”。
4. 远程仓库协作:多人项目的核心日常
4.1 remote管理与fetch、pull、push的区分
git remote是管理远程仓库连接的入口:
git remote -v # 查看远程地址 git remote add origin <repo_url> # 添加远程源 git remote remove origin # 移除 git remote set-url origin <new_url> # 修改地址(比如公司仓库迁移了域名)多人协作中git是一个“分布式”工具,但绝大多数团队仍然以某个中心仓库(origin)作为“官方真值”。这就产生了一个经典问题:git pull到底是什么。
其实git pull是两条命令的合体:git fetch+git merge。很多问题就出在fetch和merge被大家当成了一个词来理解,一旦pull出现意外,就完全不知道如何排查。
更稳健的做法是分开执行:
# 1. 从远程抓取更新,不改变工作区,只是把远程分支的最新状态记录到本地 git fetch origin # 2. 查看本地分支与远程分支的差距 git log --oneline HEAD..origin/main # 3. 确认没有意外后,再合并 git merge origin/mainfetch不改变工作区,意味着你可以“安全地”把远程的新提交引入到本地仓库的引用中,观察差异,决定后续操作。pull则直接改变工作区和历史,遇到冲突时处理起来更被动。
4.2 push的注意事项与force-with-lease
push是本地提交推送到远程。日常操作中最容易踩雷的是推送被拒绝、以及force推送。
推送被拒绝通常是因为远程分支有本地没有的提交。此时不要养成习惯直接git push --force,而是先git pull --rebase或git fetch && git rebase origin/main,把自己的提交重放到远程最新提交之上,再推送。
如果要强制推送,务必使用--force-with-lease而非--force:
git push --force-with-lease origin main--force会无条件覆盖远程分支引用,把远程历史重置到本地状态,即使远程已经有了别人新推送的提交也会被抹掉。--force-with-lease则会在推送前检查远程分支是否仍然是你上次fetch到的状态,如果期间有别人推送过,它会拒绝推送并让你先fetch下来看情况。
这个参数的区别在实际团队协作中保命级别的重要。遇到过不止一次,有人用git push --force把同事刚刚推送的一个提交整个抹掉了。--force-with-lease至少会拦住这种意外。
设置上游分支:
# 第一次推送新分支时设置跟踪关系 git push -u origin feature/login删除远程分支的两种方式:
git push origin --delete feature/old git push origin :feature/old4.3 fork工作流:如何保持与上游仓库同步
团队协作如果采用fork模式(自己账号下有一个远程副本,另一个官方仓库是上游),最常见的操作是保持fork仓库与上游同步。
# 添加上游远程(只需一次) git remote add upstream <official_repo_url> # 拉取上游更新 git fetch upstream # 将上游main合并到本地main git switch main git merge upstream/main # 推送回自己的远程fork git push origin main还有一种保持功能分支与上游同步的常见姿势:直接git rebase upstream/main,把功能分支的提交重放到最新上游之上,然后git push --force-with-lease。这样可以让PR的diff非常干净,只包含你自己真正引入的改动。
4.4 远程跟踪分支的清理与状态查看
长时间协作后,本地仓库会残留一堆已经删除的远程分支的跟踪引用。查看和清理:
# 清理远程已经不存在的分支引用 git remote prune origin # 或者fetch时同时清理 git fetch --prune # 查看远程分支与本地分支的对应 git branch -rgit branch -vv如果显示[origin/feature-login: gone],说明远程分支已经删除,本地可以放心git branch -d删掉对应的本地分支。
5. 撤销与拯救:告别“删库跑路”
5.1 reset:回退的三种模式与原理
git reset可以回退commit历史,它通过在三个粒度之间移动引用来实现撤销:--soft、--mixed(默认)、--hard,分别对应我们回退到“哪个位置”:
--soft:仅重置HEAD指向某个提交,暂存区和工作区都保持不变。相当于“撤销commit,但保留改动并暂存”。--mixed(默认):重置HEAD并清空暂存区,但工作区改动保留。相当于“撤销commit和暂存,但保留改动为未暂存状态”。--hard:重置HEAD并清空暂存区,同时把工作区也强制覆盖为目标提交内容。这是真正的“回滚”,操作前必须想清楚。
# 撤掉最近一次commit,但保留所有改动到暂存区 git reset --soft HEAD~1 # 撤掉最近一次commit,改动保留但回到未暂存状态 git reset HEAD~1 # 强制回到某个提交,清空所有未提交改动(危险) git reset --hard <commit_id>实际使用中,reset --hard需要异常谨慎。我见过多起“reset --hard后代码找不回来”的悲剧,但其中大部分人其实忘了git还有reflog这个救命稻草。
注意:如果
reset发生在push之前,一切都可逆转。如果已经push到远程,并且有其他人在此基础上提交,reset就不太合适了,应该用revert。
5.2 revert:安全的“反向提交”
git revert会生成一个与目标提交内容完全相反的新提交,实现撤销而不改写历史。这是唯一适合用于“已推送的公共分支”的撤销方式。
# 撤销某个提交(生成一个新提交) git revert <commit_id> # 撤销一个历史范围内的多个提交 git revert <oldest>..<newest>值得注意的细节:
- revert还可能产生冲突。比如你revert的提交正好和当前分支上其他提交改动重叠,这时按正常冲突解决流程处理即可。
- revert一个合并提交需要指定
-m参数:git revert -m 1 <merge_commit_id>,表示撤销合并提交并保留哪条侧分支的历史。
5.3 stash:临时保存工作区现场
开发中经常遇到这种情况:正在feature分支改到一半,突然要切到其他分支修复一个紧急bug。如果直接git switch,git会阻止切换(有未提交的冲突改动),此时git stash就是现场保护伞。
git stash # 暂存已跟踪文件的改动 git stash push -m "说明" # 带说明的暂存 git stash -u # 连未跟踪文件一起暂存 git stash list # 查看暂存列表 git stash pop # 恢复最近一次暂存并删除记录 git stash apply # 恢复暂存但保留记录(用于多次暂存时挑某一个) git stash drop # 丢弃某条暂存 git stash show -p stash@{0} # 查看某条暂存的具体diff使用stash需要注意:
- stash默认不会包含未跟踪文件,如果改了还没add的新文件,要用
-u才收纳进去。 stash pop恢复时如果工作区已经有冲突改动,会恢复失败并保留stash记录,先解决工作区冲突再pop。- 长时间不使用stash记录会堆成山,建议每次stash都带
-m备注,并在两周内清理掉不用的记录。
5.4 cherry-pick:把指定提交移植到当前分支
git cherry-pick是将某个提交的改动应用到当前分支,并生成一个新提交。典型场景:线上紧急修复了一个bug(在hotfix分支),需要把同一个修复带到main分支;或者feature分支中某个提交和当前分支完全无关。
git cherry-pick <commit_id> # 多个提交 git cherry-pick <commit1> <commit2> # 不自动提交,暂存到暂存区后人工确认 git cherry-pick -n <commit_id>如果有冲突,和merge冲突解决流程一致,解决后git cherry-pick --continue收尾。如果中途想放弃,用git cherry-pick --abort。
5.5 reflog:终极后悔药
git reflog记录了HEAD引用所有移动的历史,包括reset、merge、rebase、checkout、commit等。哪怕你把一个分支reset --hard到了很久以前,“丢失”的提交仍然存在于对象数据库中,只要你知道哈希,就能恢复。
git reflog # 输出示例 # a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to a1b2c3d # e4f5a6b HEAD@{1}: commit: fix: 登录逻辑 # 1a2b3c4 HEAD@{2}: commit: feat: 用户模块恢复丢失的提交:
git reset --hard e4f5a6b或者基于它创建新分支:
git branch recover-branch e4f5a6breflog记录默认保留90天,如果被恢复对象超过这个期限并且没被引用,就真的没了。所以万一发现reset错了,第一反应应该是马上git reflog,而不是到处喊救。
5.6 clean:清理工作区垃圾文件
git clean用于删除未跟踪文件。直接运行会直接删除,所以务必先看会删除什么:
git clean -n # 列出将要被删除的未跟踪文件(dry run) git clean -f # 删除未跟踪文件 git clean -fd # 删除未跟踪文件和目录 git clean -fdx # 连被.gitignore忽略的文件也一并删除-n是dry-run显示预览,确认无误后再加-f执行。实际操作中,我最常用git clean -nd预览整个工作区,再决定要不要清理,起码不会手滑删掉本地配置。
5.7 恢复单个文件:checkout/restore的正确打开方式
很多人在“文件被误改了,想恢复”时脑子一热就用git checkout -- .,这个命令会把工作区所有已跟踪文件覆盖成HEAD版本。危险之处在于,它会把你还没有提交的所有本地改动一次性抹掉。
正确姿势是:
# 恢复某个文件到最近一次提交版本 git restore <file> # 恢复某个文件到指定提交版本 git restore --source=<commit_id> -- <file> # 放弃暂存区的改动(即取消某个文件的add) git restore --staged <file>git restore --staged是最常用的“撤销add”操作。比如不小心把所有文件都add了,但只想提交其中一部分,用git restore --staged .把整个暂存区清空,再重新逐个add。
6. 常见问题与排查技巧实录:踩坑后的解题思路
6.1 状态速查表
实际工作中,我总结了一个高频问题速查表,按操作类型分类,方便快速定位:
| 操作目标 | 推荐命令 | 说明与坑位 |
|---|---|---|
| 撤销工作区文件修改 | git restore <file> | 注意不要误加.或--hard导致全部覆盖 |
| 取消add暂存 | git restore --staged <file> | 等价于老命令git reset HEAD <file> |
| 撤销最近一次commit保留改动 | git reset --soft HEAD~1 | 可配合--amend重写信息 |
| 撤销已push的公共提交 | git revert <commit_id> | 生成反向提交,不改写历史 |
| 合并或rebase过程放弃操作 | git merge --abort/git rebase --abort | 中止恢复到操作前状态 |
| 查看文件某行的修改人 | git blame <file> | 能定位到具体commit |
| 二分查找引入bug的提交 | git bisect start && git bisect bad / good | 配合脚本自动标记 |
| 搜索历史提交信息 | git log --grep="关键词" | 比肉眼翻日志靠谱很多 |
| 搜索文件内容 | git grep "关键词" <commit_id> | 在历史快照中搜索字符串 |
6.2 detached HEAD(游离HEAD)的处理
切到某个具体commit而不是分支名时,git会进入detached HEAD状态。此状态下做的提交不属于任何分支,切走后容易丢失。
# 触发方式 git checkout <commit_id>如果在detached HEAD下做了改动,想保留,立刻用git switch -c <new_branch>创建分支把提交“接住”。如果什么都不做,直接切走,这些提交会被reflog记录但不容易发现。最好在进入detached HEAD后第一时间检查git status来确认状态。
6.3 push被拒绝后的标准解法
遇到“failed to push some refs”错误时,我见过不少人直接git push --force把问题糊弄过去,其实标准解法比很多人想象得简单:
# 把远程新提交合并到本地 git pull --rebase origin main # 或者手动fetch后查看差异再合并 git fetch origin git log --oneline HEAD..origin/main git rebase origin/main # 解决冲突后,再push git pushpull --rebase对比pull(merge)的优势在于:它不会产生一个多余的“merge远程分支”提交,提交历史更干净。但它同样会改写本地提交的哈希,对于公共分支同样要谨慎。
6.4 误删文件或误改分支名后的救援路径
误删未commit文件:
# 未add的文件误删,无药可救 # 如果刚刚add过,仍然可以从暂存区恢复 git restore --staged --worktree <file>误删文件且已经commit过:
# 从HEAD恢复 git restore <file> # 从指定历史commit恢复 git restore --source=<commit_id> -- <file>误改分支名但想改回来:
git branch -m <old_name> <new_name>如果真的分支被删了,使用git reflog找回原commit,然后基于哈希创建新分支。
6.5 合并冲突中文件状态的判别
合并冲突时git status会直接显示Unmerged paths,文件会有冲突标记,常见的三个状态缩写:
UU:两个分支都改了同一文件的同一区域,存在冲突。AA:两个分支都新增了同名文件,需要手动取舍。AU:当前分支新增但另一侧没有,git可以自动处理,一般不会报冲突。
git ls-files -u会列出冲突文件及其三个阶段的编号(1为共同祖先、2为当前分支、3为另一分支),可以用git show :2:filegit show :3:file查看各版本内容,这在复杂冲突的场景中很实用。
6.6 我的几个实用alias,提升日常操作效率
长期经验告诉我,给高频命令配置alias能显著减少输入错误并提高效率。这里分享一份我日常在用的:
git config --global alias.st status git config --global alias.co checkout git config --global alias.br branch git config --global alias.ci commit git config --global alias.lg "log --oneline --graph --decorate --all" git config --global alias.unstage "reset HEAD --" git config --global alias.last "log -1 HEAD --stat" git config --global alias.df "diff" git config --global alias.dfs "diff --staged"配置后,git lg就是漂亮的彩色分支图,git last能快速查看最后一次提交的改动统计。
7. 实操总结与个人经验分享
如果只让我给一句话,我会说:不要用SVN时代的心智模型去使用git,一切命令都基于数据流和指针去思考,大部分问题能直接推理。
围绕这个认知,有两类实践让我感受最深:
第一类是“提交历史也是项目资产”。我把提交信息当作文档的一部分来写,一个提交只做一件事,改一行格式和修一个bug分别提交。用rebase -i把零碎提交合并成逻辑完整的单元,用cherry-pick把需要的修复单独移植到发布分支。这样的历史,后来索引问题的效率是几何级提升。很多新人不在意这条,觉得“反正高版本信息都在”,但当你在三个月后凭提交信息快速定位线上问题根因时,就会感谢当初自己的克制。
第二类是“凡是涉及强制操作的,先备份再行动”。我在reset --hard、git clean -f、push --force之前,一定会先看一眼git status和git reflog记录,有必要的话先git branch backup_xxx创建一个备份分支。多这一个动作,可能就不会发生“误删整个工作区”“推掉了同事的提交”这类事故。
最后再分享一个非常实用的小技巧:如果你不确定某条命令会带来什么效果,可以找一个不重要的临时目录,git init后随便放几个文件,把命令跑一遍,观察输出。git的所有机制都是可见的,多跑几次就熟悉了,没必要直接在生产仓库里冒险实验。这种“实验式学习法”是我当年掌握git最顺手的方法,你可以试试。