git操作注意事项
多人开发同一分支
远程有别人的新提交,本地也有自己的提交,这时 git pull 和 git pull --rebase 结果不一样。
远程:a --- b --- c
本地:a --- b --- d
git pull 走 merge,会多生成一个合并节点 M,M 有两个父(c 和 d),c、d 都保留,历史是分叉再合流:
a --- b --- c
\ \
d ---- M
git pull --rebase 是把你的 d 摘下来,接到 c 后面重放成 d',历史是一条直线:
a --- b --- c --- d'
注意 rebase 重放的只是你本地多出来的提交,远程已有的 c 不会被改写。两个人改到同一处时,rebase 一样会冲突,解决完 git add,再 git rebase --continue;不想继续就 git rebase --abort。
共享分支上 rebase 完不要随手强推,原因后面说。
只想看看远程有没有新东西
本地其实有两套指针:qa 是你自己的分支,origin/qa 是「上次同步时远程在哪」的备忘录。
git fetch 只更新 origin/qa 这类备忘录,不动你当前分支,也不动工作区:
bash
git fetch
git log qa..origin/qa // 看看远程多了什么
git pull = fetch + 把远程合进当前分支,会改你的代码。想先看一眼,用 fetch。
不要长期在共享分支上开发
直接在 qa 上改、提交、推送,短期省事,代价是:半成品会污染别人拉到的代码;多人挤在一个分支尖上,冲突更密;几个需求的提交混在一起,想撤其中一个很难;也没有清晰的评审边界。
推荐流程:
bash
git checkout qa && git pull
git checkout -b feature/xxx
// 开发、提交
git checkout qa && git pull
git merge feature/xxx
git push
一个需求一个分支,依赖升级、顺手改的无关代码不要塞进同一个 feature。
合分支时冲突
冲突出现后,仓库处在「merge 进行中」的状态。这时不要再去 git pull --rebase:merge 和 rebase 都是需要占用仓库状态的操作,Git 一次只允许进行一个,多半直接报错,硬搞状态更乱。
只有两条路:
bash
// 继续合
// 改完冲突文件
git add <文件>
git commit
// 放弃这次合
git merge --abort
放弃优先用 merge --abort,语义清楚。git reset --hard HEAD 多数时候也能回到 merge 前,但 --hard 会连未提交的改动一起丢,容易误伤。
看懂冲突文件标记
<<<<<<< HEAD
当前分支的版本
=======
合进来那边的版本
>>>>>>> feature
HEAD 那侧是你现在所在的分支,不是「以前的版本」;下面那侧是被合入或被挑入的内容。
改完文件(把标记删干净)之后必须收尾,不能直接 push:
bash
git add <文件>
git commit // merge
git cherry-pick --continue // cherry-pick
git rebase --continue // rebase
只要别人分支上的某一次提交
不想合整条分支,只要其中一个提交,用 cherry-pick。它做的是:算出那次提交相对它父提交的差异,打到你当前分支上,再生成一个新提交。
qa: A --- B --- C
feature: A --- B --- D --- E --- F
git cherry-pick E
qa: A --- B --- C --- E'
原来的 E 还在 feature 上,F 不会跟着过来。冲突的原因是这块补丁跟当前代码对不上:
bash
git add <文件>
git cherry-pick --continue
// 或
git cherry-pick --abort
如果被挑的提交依赖同分支更早的改动,只挑一个可能编译不过,要么一起挑,要么确认它是独立的。
撤掉已经推到共享分支的提交
已经 push、别人也拉过了,就用 revert,不要 reset 之后强推。
revert 不是把旧提交从历史里删掉,而是再推一个新提交,把那部分改动反做回去。旧节点仍在历史里,只是分支最新的内容不再包含它。这样别人正常 pull 就能同步,不会对不上。
merge 提交要指定主线,光写 hash 一般会被拒绝:
bash
git revert <普通提交hash>
git revert -m 1 <merge提交hash>
-m 1 表示以第一个父(执行 merge 时你所在的那侧)为正线,撤掉这次合并从另一侧引进来的改动,不是把两边的提交内容全清掉。另外,一条分支被 merge 后又 revert,之后再合同一条分支容易合不进来,需要先 revert 那个 revert。
有未提交改动却要临时切分支
bash
git stash // 未跟踪的新文件用 git stash -u
git checkout 其他分支
// 处理完
git checkout 原分支
git stash pop
pop 会取回并删掉这条记录,apply 会保留,怕冲突就先 git stash list 看一眼再决定。
改上一次提交的说明或漏加的文件
bash
git add 漏掉的文件
git commit --amend
amend 不是新增一次提交,而是把最后一次重新打包成一个新提交,hash 会变:
amend 前:A --- B --- C
amend 后:A --- B --- C'
没 push 过随便用。已经 push 过再 amend,本地就和远程分叉了,普通 push 会被拒,只能强推 —— 所以这招留给自己一个人用的分支。
强推被拒,或者不该强推
普通 push 只允许快进:远程分支尖是你历史里的祖先,你在后面接着推。amend、rebase 这类改写历史的操作会让本地和远程分叉,push 就会被拒。
bash
git push --force // 直接覆盖远程
git push --force-with-lease // 远程若被别人推过,就拒绝这次覆盖
--force-with-lease 的「拒绝」指推送失败、远程保持原样,避免把同事刚推的提交盖掉。被拒之后先 git fetch 看看远程到哪了,再决定怎么处理。
共享分支不要强推。别人已经基于旧历史干活,你一覆盖,他们的提交在分支上就消失了,全员历史对不上。共享分支要撤代码,用 revert。
reset --hard 之后提交没了
分两种情况:
- 已经 commit、只是没 push:能救。
--hard只是把分支尖挪走了,提交对象通常还在。 - 从没 commit 过的工作区改动:基本救不回来。
bash
git reflog // 找到 reset 之前的那次提交
git branch recover <hash> // 先拉一条分支保住它
reflog 记录的是本机 HEAD 的移动轨迹,所以「看起来没了」的多数情况都能捞回来。
功能分支同步最新代码:rebase 换底座
feature 拉出来之后,共享分支一直在往前走,你的底座就旧了。基于过时代码开发,越晚合回去冲突越多。
bash
git checkout feature
git rebase qa
rebase 前:
qa: A --- B --- C --- F --- G
feature: A --- B --- C --- D --- E
rebase 后:
feature: A --- B --- C --- F --- G --- D' --- E'
结果不是「把代码换成 qa」,而是以最新 qa 为底,再把你的提交重放一遍,你的功能还在。git pull --rebase 是同一件事,只是底座换成远程最新。
也可以在 feature 上 git merge qa 来同步,能用,就是会多出合并节点,历史更岔。
HEAD、切分支、reset 的区别
HEAD 表示「你现在在哪」,一般是 HEAD -> 分支 -> 提交。
git switch feature:换的是 HEAD 挂在哪条分支上,两条分支的尖都不动。git reset --hard <hash>:不换分支,而是把当前分支的尖挪到指定提交,--hard连工作区一起改。
一句话:切分支换的是「我站在哪条线上」,reset 挪的是「我这条线指向哪」。
checkout 一个 hash 之后的 detached HEAD
git checkout <hash> 之后 HEAD 直接指向某个提交,不挂任何分支,这就是 detached HEAD。看旧版本、排查问题没问题,但在上面继续提交就危险:这些提交没有分支指着,一切走就很难找。
要保住就先建分支:
bash
git switch -c recover-wip
工作区、暂存区、本地仓库
工作区 --git add--> 暂存区 --git commit--> 本地仓库 --git push--> 远程
commit 只到本地仓库,push 才上远程。回退对应的两条:
bash
git restore <文件> // 丢弃工作区改动
git restore --staged <文件> // 撤出暂存区,改动仍留在工作区
git checkout -- <文件> 是 restore 的旧写法,别和切分支的 checkout 混。
一句话分清 abort、reset、revert
git merge --abort:取消正在进行的合并,回到合之前。git reset --hard:把当前分支尖和工作区强行拨到某个提交,本地未推送时用,会丢未提交内容。git revert:用一个新提交反做掉旧改动,历史保留,已推送的共享分支用它。
快进合并(fast-forward)
如果你的分支没有新提交,只是落后于目标分支,合并时 Git 不需要造合并节点,直接把分支尖往前移,这叫快进:
合并前:
qa: A --- B --- C --- D
我的分支: A --- B
合并后:我的分支尖直接移到 D,不产生新节点
两边都有新提交时就不能快进,要么产生 merge 节点,要么先 rebase。push 被拒的报错里常出现的 non-fast-forward,说的就是这件事。
文件已经提交过,再加 .gitignore 没用
.gitignore 只对未被跟踪的文件生效。已经提交进仓库的文件,加规则也照样被跟踪。
bash
git rm --cached <文件> // 从版本控制移除,本地文件保留
git commit -m "stop tracking"
配置文件、本地环境文件建议一开始就写进 .gitignore。已经推上去的密钥要先轮换,删文件不等于历史里没有。
冲突时的三方:base、ours、theirs
Git 合并是三方比较:共同祖先(base)、当前分支(ours)、合入的一侧(theirs)。两边相对 base 改了不同地方,Git 能自动合;改了同一处,就交给你决定,也就是冲突标记的由来。
bash
git checkout --ours <文件> // 直接要当前分支的版本
git checkout --theirs <文件> // 直接要合入那侧的版本
整文件二选一才用这两条,多数冲突还是要手动合成一版。注意 rebase 时 ours 和 theirs 的含义是反过来的:此时「当前」是被重放到的那个底座。