前言
上一篇文章Git协作流程介绍了多人协作时,一个功能从开发到合并的大致流程。
真正写项目时,最容易让人头疼的往往不是提交代码,而是各种异常情况:
pull冲突merge冲突push被拒绝- 提交错了
- 分支搞乱了
- 改了一半代码但突然要切分支
这些问题刚遇到时会很吓人,尤其是终端里突然出现一堆英文提示,很容易不知道下一步该敲什么。
这篇就不讲太多Git原理了,主要记录一些常见问题的处理方法。目标很简单:遇到问题时,知道先看什么,再执行什么命令。
为什么会产生冲突
先看一个最常见的场景:
你修改 login.js↓同事也修改 login.js↓你执行 git pull↓Git 无法自动决定保留哪份代码↓产生冲突Git能自动合并很多修改。
比如你改了login.js,同事改了README.md,这两个文件互不影响,Git一般能自动合并。
再比如你和同事都改了login.js,但你改的是第10行,同事改的是第80行,Git大多数时候也能自动合并。
真正容易冲突的是:
同一个文件同一个位置被不同人改了不同内容这时Git不知道应该保留你的版本,还是保留同事的版本,所以它会停下来,让你手动处理。
冲突本身不是Git坏了,而是Git不敢替你乱选。
merge冲突
假设你在main分支,想把feature-login分支合并进来:
git merge feature-login如果两边修改了同一个位置,可能会看到类似提示:
CONFLICT (content): Merge conflict in login.jsAutomatic merge failed; fix conflicts and then commit the result.这时先不要慌,先看状态:
git statusgit status会告诉你哪些文件处于冲突状态。
打开冲突文件后,一般会看到这种内容:
<<<<<<< HEAD本地内容=======另一分支内容>>>>>>> feature-login这里的含义是:
<<<<<<< HEAD到=======之间,是当前分支的内容=======是分隔线=======到>>>>>>> feature-login之间,是待合并分支的内容>>>>>>> feature-login表示这些内容来自feature-login
处理冲突时,不是简单删除某一边就完事,而是要根据实际需求改成最终正确的代码。
处理步骤一般是:
- 打开冲突文件
- 删除
<<<<<<<、=======、>>>>>>>这些冲突标记 - 保留或合并正确内容
- 保存文件
- 重新加入暂存区
- 提交合并结果
命令是:
git add login.jsgit commit如果有多个冲突文件,就都处理完后再git add:
git add 文件1 文件2git commitpull为什么会冲突
很多人会觉得奇怪:
git pull不是拉代码吗,为什么也会冲突?
原因是git pull并不是单纯下载代码,它本质上等于:
git pull = git fetch + git merge也就是说,git pull会先把远程的新提交下载下来,然后再把远程分支合并到你当前分支。
既然它内部做了merge,那当然也可能产生merge冲突。
所以遇到pull冲突时,处理方式和merge冲突基本一样:
git status# 手动修改冲突文件git add 冲突文件git commit如果你不确定哪些文件冲突了,永远先看:
git status这个命令会告诉你当前Git卡在哪一步。
rebase冲突
有些团队或个人习惯用rebase来同步代码,比如:
git pull --rebase或者:
git rebase mainrebase也可能冲突。
冲突文件里的标记看起来也差不多,还是这种形式:
<<<<<<< HEAD当前内容=======你的提交内容>>>>>>> 提交ID处理文件的方式一样:打开文件,删掉冲突标记,改成最终正确内容。
不同的是,rebase处理完冲突后,不是执行git commit,而是:
git add 文件名git rebase --continue如果后面还有冲突,就继续处理,再继续:
git add 文件名git rebase --continue如果这次rebase越处理越乱,想放弃,可以:
git rebase --abort这个命令会取消本次rebase,回到rebase之前的状态。
简单记:
merge 冲突解决完:git add -> git commitrebase 冲突解决完:git add -> git rebase --continuepush被拒绝
推送时看到这种提示也很常见:
failed to push some refs或者提示远程分支比本地更新。
常见原因是:
远程仓库已经有新提交但你本地还没有这些提交所以Git拒绝直接推送比如你和同事都在同一个分支上开发。同事先推送了代码,你本地还没拉取他的提交,这时你直接git push,Git就可能拒绝。
比较常见的解决方式是:
git pull --rebasegit push意思是先把远程的新提交拿下来,再把你的提交放到后面,然后重新推送。
如果git pull --rebase过程中出现冲突,就按前面rebase冲突的方式处理:
git status# 手动修改冲突文件git add 文件名git rebase --continuegit push不要一看到push失败就立刻强制推送。强制推送可能会覆盖别人已经推上去的提交,尤其是在公共分支上很危险。
临时保存修改
有时你代码写到一半,突然要切到别的分支修bug。
这时如果直接切分支,Git可能会提示:
Please commit your changes or stash them before you switch branches.意思是:你当前还有未提交修改,先提交或者临时保存起来。
如果这些代码还没写完,不适合提交,可以用stash临时保存。
保存当前修改:
git stash查看保存列表:
git stash list恢复最近一次保存:
git stash poppop会恢复修改,并把这条stash记录从列表里删除。
如果只想恢复但保留stash记录,可以用:
git stash apply删除某条stash:
git stash drop如果有多条stash,可以指定名字:
git stash pop stash@{0}git stash drop stash@{0}stash适合临时救急,不建议长期把重要代码放在里面。重要修改最好还是及时提交到分支上。
撤销未提交的修改
如果文件改坏了,还没有提交,可以用restore恢复。
恢复单个文件:
git restore 文件名恢复当前目录下所有未提交修改:
git restore .注意,这个操作会丢失工作区修改。
执行前最好先看一下:
git diff确认这些修改真的不要了,再执行restore。
取消暂存
如果文件已经git add了,但还没commit,想把它从暂存区拿出来:
git restore --staged 文件名这个命令只是取消暂存,不会删除文件内容。
比如你执行了:
git add .结果发现把不该提交的文件也加进去了,就可以:
git restore --staged 文件名然后重新选择真正要提交的文件。
回退提交
如果已经提交了,但发现提交错了,可以用reset。
先查看提交历史:
git log --oneline比如最近一次提交写错了,想撤销这个commit,但代码还想保留:
git reset --soft HEAD~1--soft的意思是:
撤销 commit保留代码修改保留暂存状态如果想撤销提交,并且让修改回到未暂存状态,可以用:
git reset --mixed HEAD~1--mixed是git reset默认模式,通常可以不写。
最危险的是:
git reset --hard HEAD~1它的意思是:
撤销 commit代码修改也一起删除所以--hard一定要慎用。执行前最好确认这次提交里的代码真的不要了。
如果提交已经推送到远程,并且别人可能已经基于它继续开发,就不要随便reset --hard再强推,这很容易影响别人。
使用revert撤销提交
多人协作时,更推荐用revert撤销已经提交的内容。
命令是:
git revert 提交IDrevert和reset的区别可以简单理解成:
reset:把历史往回挪revert:新增一个反向提交比如某个提交添加了三行代码,revert会生成一个新的提交,把这三行代码删掉。
它不会删除历史记录,所以更适合多人协作。
简单建议:
- 自己本地还没推送的提交,可以考虑
reset - 已经推送到远程的提交,优先考虑
revert - 公共分支上不要随便
reset --hard
reflog:Git的后悔药
如果你误删了提交,或者reset过头了,可以先看reflog。
git reflog输出大概像这样:
abc123 HEAD@{0}: reset: moving to HEAD~1def456 HEAD@{1}: commit: 添加登录页面git log主要看当前分支能看到的提交历史。
git reflog记录的是HEAD移动过的位置。很多误操作之后,提交虽然从普通日志里看不到了,但还能在reflog里找到。
找到想恢复的提交ID后,可以:
git reset --hard def456注意,reset --hard会让当前代码回到那个提交对应的状态,执行前确认工作区没有要保留的修改。
如果不想直接回退,也可以先新建一个分支保住现场:
git switch -c recover-branch def456这样更稳一点。
强制推送
强制推送命令是:
git push --force它的风险很大,因为它可能覆盖远程分支历史。
比如远程分支上有同事刚推送的提交,你本地没有这些提交,直接--force可能会把同事的提交顶掉。
如果确实需要强制推送,更推荐:
git push --force-with-lease简单理解:
--force:直接用本地历史覆盖远程历史--force-with-lease:如果远程分支被别人更新过,就拒绝覆盖--force-with-lease不是完全没有风险,但比--force更安全。
日常建议:
- 自己的功能分支,确认没人共用时,可以谨慎使用
- 公共分支,比如
main、develop,不要随便强推 - 不确定时,先问团队成员,不要直接覆盖远程
常见问题
commit说明写错了怎么办
如果只是最近一次提交说明写错了,还没推送,可以:
git commit --amend它会修改最近一次提交。
也可以直接写新说明:
git commit --amend -m "新的提交说明"如果这个提交已经推送到远程,修改后通常需要强制推送。多人协作时要谨慎。
push到错误分支怎么办
先不要继续操作,先确认当前分支和提交历史:
git branchgit log --oneline如果只是把提交推到了自己的错误分支,可以把正确提交cherry-pick到目标分支,再在错误分支上用revert撤销。
如果推到了公共分支,建议不要自己直接reset --hard加push --force,先和团队确认处理方式。
删除错分支怎么办
如果只是删除了本地分支,可以先用reflog找提交:
git reflog找到提交ID后重新建分支:
git switch -c 分支名 提交ID如果远程分支还在,也可以直接重新拉:
git fetch origingit switch -c 分支名 origin/分支名reset过头了怎么办
先看:
git reflog找到reset之前的提交ID,然后恢复:
git reset --hard 提交ID如果当前工作区还有想保留的修改,不要立刻--hard,可以先stash或者新建分支保存现场。
merge和rebase冲突有什么区别
冲突文件的处理方式差不多,都是:
打开文件删除冲突标记保留正确内容git add区别在最后一步:
merge 冲突:git commitrebase 冲突:git rebase --continue如果不想继续:
git merge --abortgit rebase --abort分别用于取消当前的merge或rebase。
总结
遇到Git冲突时,可以先按这条线处理:
冲突↓git status↓编辑冲突文件↓git add↓git commit / git rebase --continue不要一看到冲突就乱敲命令。先用git status看清楚Git现在卡在哪一步,再决定是commit、rebase --continue,还是abort。
常见场景可以记这个表:
| 场景 | 命令 |
|---|---|
| 查看状态 | git status |
| 临时保存 | git stash |
| 恢复修改 | git restore 文件名 |
| 取消暂存 | git restore --staged 文件名 |
| 回退提交 | git reset |
| 安全撤销 | git revert 提交ID |
| 查看历史 | git log --oneline |
| 找回误删提交 | git reflog |
| 继续rebase | git rebase --continue |
| 取消rebase | git rebase --abort |
Git很多问题看起来吓人,其实大部分都有固定处理流程。只要提交前多看status和diff,公共分支上少用危险命令,出了问题先别急着强推,基本都能处理回来。