1. Git Pull 是什么,为什么它几乎每天都要用
1.1 它背后到底发生了什么
说实话,我第一次用 Git 的时候,git pull就是我接触到的第一个命令。那时候教程告诉我,在 GitHub 或 Gitee 上写了代码,回到本地电脑,先一行git pull把远端最新的东西拉下来,再开始干活。我一直到很晚才意识到,git pull并不是一个“原子”命令,它其实是先git fetch(把远端提交下载到本地),再执行一次git merge(把下载下来的提交合并到你当前所在的分支)。也就是:
git pull = git fetch + git merge
如果你能理解这条公式,git pull 的很多怪异行为就都能解释通了。
举个例子,你和同事同时维护一个项目,他在远端已经把某个模块重构了一遍,你本地还停留在三天前的版本。你执行git pull origin main时,流程是这样的:
- Git 先联系远程仓库(在 GitHub/Gitee 上叫
origin),把main分支上新增的提交全部拉到本地一个隐藏目录.git里; - 然后 Git 把这些新提交合并到你当前的工作分支上,同时更新工作目录里的文件;
- 如果新提交和你本地修改互不影响,这个过程是静默的;如果改了同一个文件的同一行,就进入冲突状态,需要你手动解决。
整套逻辑其实和我们平时把文件传到网盘、再从另一台设备下载最新版是一个道理。只不过 Git 把“谁改了哪几行”记得一清二楚,所以它的合并能力远超普通文件同步工具。
1.2 什么场景该用,什么场景要踩刹车
先说什么时候用:工作开始前、准备提交前、准备推送前、以及部署代码前。很多团队的习惯是早上开工第一件事先git pull,先把远端的最新代码同步到自己本地,再开始写代码。维护个人项目的人可能一周才拉一次,但只要你准备 push,push 前先 pull 几乎是纪律性动作。
为什么 push 前必须 pull?因为远端很可能在你上次拉取之后又有了别人的提交,你不先同步就直接 push,会遇到non-fast-forward的错误。本质上就是你的提交基底已经过时了,远端仓库不会好心地把你们的代码合并好再收下你的推送,它会直接拒绝,让你先把同步问题解决。
但我要提醒一句:如果你本地有大量未提交的修改,直接git pull是有点冒险的。Git 原则上不会覆盖你本地未提交的修改,但如果新拉下来的提交要改动的文件正好和你本地修改的文件重叠,它可能会拒绝合并,提示Your local changes would be overwritten by merge,或者直接让你陷入一堆需要手工处理的冲突。我自己的习惯是:pull 之前先看一眼git status,有未提交修改就先 commit 掉,或者至少 stash 一下。
还有一类场景要特别谨慎:当你在一个已经被改动很深的分支上、或者正在做代码评审的中途,这时候无脑 pull 可能会把大量陌生修改塞进你的工作区,造成信息过载。这时更适合用git fetch先看看远端发生了什么,再决定要不要合并。这个区别我后面会专门展开。
2. 日常高频:显式分支、Rebase 与老仓库合并
2.1 养成指定远端和分支的习惯
很多人用git pull不跟任何参数,因为 clone 后默认配置了跟踪分支。但我要强调,手动养成git pull origin main的习惯很有帮助。为什么要显式指定?
- 明确你要拉取的是哪个远端(可能是
origin、upstream或你自己加的备用仓库); - 明确分支名,避免本地分支名和远端不一致时拉错;
- 在没有配置 upstream 的全新分支上,不带参数的
git pull会直接报错,而带参数就能正常工作。
GitHub 新仓库默认分支已经从master改成了main,Gitee 默认分支还是master。因此你 clone 下来的默认分支可能各不相同。如果你本地有一个旧仓库,远端已经没人维护master、所有提交都去了main,那么git pull origin master拿到的可能是空数据,正确做法是git pull origin main。
分支名不一致的问题,尤其在 GitHub 和 Gitee 之间搬运仓库时特别常见。你把 GitHub 的仓库推到 Gitee,本地分支叫main,Gitee 上初始化出来的默认分支叫master,两端一合并,各种别扭。解决办法就是显式指定:git pull gitee master、git pull origin main,每次把远端名字和分支名说清楚,少踩一半的坑。
2.2 想让提交历史更干净:git pull --rebase
如果你在社区搜索 Git 实用技巧,一定会看到“尽量用git pull --rebase”的说法。原因在于默认的git pull会产生 merge commit:每次你把远端合并进本地,Git 都会记一笔Merge branch 'xxx' into xxx的提交。时间一长,日志里会出现很多分叉和合并节点,读起来像地铁线路图一样复杂。
git pull --rebase做的事情是:先把本地的提交“摘下来”。比如你有两个本地提交 A、B,远端拿下来的新提交是 C、D,那么 rebase 会把 A、B 依次重放到 C、D 的上面,最后得到一条直线:C → D → A' → B'。你的代码内容没变,但历史干净了。
什么时候用?
- 个人分支、功能分支,别人不会同时在你这个分支上工作,放心用;
- 推送前想保持历史整洁,用;
- 如果你在一个团队共享分支上工作,
--rebase后可能改变提交哈希,如果别人已经基于你原来的提交干活,就会出现麻烦。这种情况下,还是用默认的 merge 更稳妥。
我实际用下来的建议是:本地功能分支坚持git pull --rebase,主分支或发版分支老老实实 merge。不要为了追求好看的历史去动大家都依赖的分支,美观的代价可能是队友的困惑。
2.3 解决 “refusing to merge unrelated histories”
这是非常常见的新手报错,症状是执行git pull时出现:
fatal: refusing to merge unrelated histories这句话看着吓人,实际意思是:你想合并的两个提交历史没有公共祖先。最典型的场景是,你刚刚在本地git init创建了一个空仓库,然后又在 Gitee/GitHub 上手工创建了一个仓库,两边各自都有一次“初始提交”,Git 认为这俩仓库是毫无关系的独立项目,于是拒绝自动合并。
解决方案是加上--allow-unrelated-histories:
git pull origin main --allow-unrelated-histories但这里我要泼一盆冷水:这个参数是让你“强行把两个不相干的历史缝在一起”,并不是每次都正确。如果你本来就应该从远端 clone,而不是本地 init 后再去关联,那么更合适的路径是:把本地文件备份好,重新git clone仓库,再把文件拷贝进去。只有在确实需要把两个独立项目合并到同一个仓库时才用这个参数。
注意:
--allow-unrelated-histories不是灵丹妙药,它只是解除 Git 的安全检查,合并后你仍可能面对非常多冲突,因为这些文件在两个历史里都是“新增”的。务必先备份本地数据。
3. GitHub / Gitee 实操差异:协议、密钥与令牌
3.1 SSH 还是 HTTPS:先搞清楚你的 remote 地址
同样一个仓库,可以同时支持 HTTPS 和 SSH 两种方式拉取,GitHub 和 Gitee 都一样。执行git remote -v能看到当前仓库用的是哪种地址:
- HTTPS 形式:
https://github.com/user/repo.git或https://gitee.com/user/repo.git - SSH 形式:
git@github.com:user/repo.git或git@gitee.com:user/repo.git
两种方式的区别主要在于鉴权:HTTPS 需要输入用户名和密码,GitHub 现在已不支持纯密码,必须用 Personal Access Token,Gitee 也支持令牌方式;SSH 则是在本地生成一对公钥和私钥,把公钥配置到平台后,拉取推送时就不需要反复输账号密码。
选择建议:
| 对比项 | HTTPS | SSH |
|---|---|---|
| 首次配置 | 需要生成 token 或记住密码 | 需要生成密钥对、配置公钥 |
| 日常使用 | 每次要输凭证,Git 会缓存 | 免输密码,体验顺滑 |
| 安全性 | 依赖 token 有效期 | 私钥本地保存,泄露风险低 |
| 适合场景 | 临时环境、公司电脑 | 个人长期开发环境 |
我个人推荐在常用的开发机上配置 SSH。尤其是你同时使用 GitHub 和 Gitee 时,每个平台单独配置密钥,能让git pull的过程变成纯粹的“拉数据”,不用每次被凭证问题打断思路。
3.2 配置 SSH 密钥连接 Gitee 的完整步骤
在 Gitee 上如果你要免密拉取代码,流程是这样的:
本地生成密钥对(如果还没有):
ssh-keygen -t ed25519 -C "your_email@example.com"一直回车会生成到
~/.ssh/id_ed25519和~/.ssh/id_ed25519.pub。ed25519 算法目前兼容性很好,长度短安全性高;如果生产环境有老机器非要用 RSA,也可以ssh-keygen -t rsa -b 4096。把公钥内容复制到 Gitee:
cat ~/.ssh/id_ed25519.pub复制输出的整行内容,登录 Gitee 后进入「设置」→「安全设置」→「SSH 公钥」,粘贴保存。
验证连通性:
ssh -T git@gitee.com第一次连接会提示确认远端主机指纹,输入
yes,然后会看到欢迎信息,类似“Hi 用户名! You've successfully authenticated”。修改仓库地址为 SSH 形式:
git remote set-url origin git@gitee.com:user/repo.git
完成以上步骤之后,git pull、git push都不会再弹密码框。GitHub 的配置几乎一模一样,只是域名从gitee.com换成github.com。
踩过的坑:如果你配置了多个平台的多把密钥,SSH 有时会“选错钥匙”。这时可以编辑~/.ssh/config,按 Host 区分,比如:
Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_github Host gitee.com HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_gitee这样git pull时 SSH 会自动选择对应平台的密钥,不会因为 key 不匹配被远端拒之门外。
3.3 GitHub 私有仓库与令牌方式
GitHub 从 2021 年起不再接受账户密码直接推送,你必须使用 Personal Access Token。生成路径是:GitHub 头像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token。
生成时要勾选仓库访问的权限范围,至少需要repo这个 scope。然后把生成的 token 当成密码使用即可。在 HTTPS 方式下拉取私有仓库,用户名填你的 GitHub 用户名,密码填 token,而不是账户密码。
用 HTTPS + token 时,如果凭证管理器里缓存了一个错误 token,之后拉取就会反复提示认证失败。解决方式:清空本机 Git 凭据缓存,Windows 上是“控制面板 → 凭据管理器”,macOS 上是“钥匙串访问”,再用新的 token 重新拉一次。
Gitee 在登录层面也有类似机制,支持密码、令牌等方式。如果 Gitee 界面要求验证码时提示“验证码错误”,但你的验证码明明是对的,这类问题大概率是浏览器缓存或输入法导致的,换一个无痕窗口,或者直接用 SSH 方式访问仓库,能绕开绝大部分验证码相关的登录问题。
4. 分支同步、合并与提交修正
4.1 Git Fetch 和 Git Pull 的本质区别
搜索热度很高的一个问题是“git fetch 和 git pull 区别”,这里给一个最直接的解释:git fetch 是“只下载,不合并”,git pull 是“下载完还要合并”。也就是说,fetch 执行完之后,你的工作区文件一个都不会变,但对 Git 来说,它已经知道了远端的最新状态。
实际操作中怎么体会?你执行:
git fetch origin然后看:
git log --oneline origin/main就能看到远端main分支的最新提交,但你本地分支仍然在原来的位置。这时候你可以从容地查看代码变动,甚至可以用git diff HEAD origin/main来比较差异,充分了解了改动内容后,再决定git merge origin/main还是git rebase origin/main。
这个习惯对有代码洁癖的人来说非常友好,因为它把“获取信息”和“改变工作目录”两个动作拆开了。我见过很多初学者一上来就git pull,结果被大量陌生改动直接灌进工作区,一脸懵。你完全可以先 fetch 再手动 merge,效果和 pull 一样,但你能更清楚地知道自己在干什么。
4.2 分支合并:merge、rebase,还是 cherry-pick
git pull最常见的合并对象是“当前分支对应的远端分支”。但你的需求往往不只有这一种:可能你想把同事的feature/login分支拉下来看看,或者只拿某个特定提交。对应的命令分别是:
拉取某个具体分支并合并到当前分支:
git pull origin feature/login这其实等价于
git fetch origin后执行git merge origin/feature/login。只想把某个分支的最新内容拿到本地观察,不动当前工作区:
git fetch origin feature/login git checkout -b local-login origin/feature/login只想拿一个提交而不是整条分支:
git fetch origin git cherry-pick <commit-hash>
这里多说一句 cherry-pick 的使用感受。它非常适合“别人在别的分支上修了一个 bug,你要在自己分支上同步这个修复”的场景。比如有人修好了某问题并推送到了 master,你正在自己的功能分支开发,不想把整个 master 合并进来,就可以直接 cherry-pick 那个修复提交。同样的逻辑也可以用在 git pull 之后的补救上:如果你已经把远端整个合并进来,发现引入了很多无关代码,可以考虑用 cherry-pick 只挑关键的改动。
4.3 解决冲突的正确姿势
不管是git pull还是git pull --rebase,只要双方改了同一个区域,冲突就一定会出现。Git 会把冲突标记写进文件,像这样:
<<<<<<< HEAD 你的本地修改 ======= 远端带来的修改 >>>>>>> origin/main打开文件后把这个区域改成你想要的最终结果,删掉标记行,然后git add这个文件。这里有个容易搞错的点:git merge流程冲突后,解决完文件要执行git commit来结束合并过程;而git rebase流程冲突后,解决完文件执行git add后,需要执行git rebase --continue来继续重放剩余的本地提交。
如果你发现自己陷入了一个不知道如何抽身的 rebase 过程,可以使用git rebase --abort回到拉取之前的状态;merge 过程则用git merge --abort。这两个命令相当于游戏里的“回档”,能让你在冲突泥潭里随时跑路。不过要记住:已经提交过的内容不会凭空消失,冲突解决完再继续才符合多数团队的预期。
4.4 提交信息修正:git commit --amend 与拉取的关系
git commit --amend是又一个高频操作,它用来修改最后一次提交的提交信息、或者往最后一次提交里追加文件。用法很简单:
git commit --amend -m "新的提交信息"或者想把某个漏掉的文件塞进上一次提交:
git add 忘提交的文件 git commit --amend --no-edit用--no-edit会保留原来的提交信息,只是把新文件补充进去,非常方便。
但如果你在git pull之后才发现需要 amend 上一次提交,就要格外小心:你上一次提交可能已经被推送给远端了。如果别人已经看到了你那笔提交,你用 amend 重写它,会导致提交哈希变化,再次推送时必须使用强制推送git push --force-with-lease。这个命令有风险,--force-with-lease虽然比--force稍微安全一点,它会在远端还是你上次看到的状态时才会覆盖,但我依然建议:已经推送过的提交就不要再 amend 了,宁可新开一笔提交。原因很简单,重写共享历史会让其他协作者一头雾水,影响团队信任。
5. 常见报错现场与排查清单
5.1 fatal: not a git repository
这个报错几乎是每个新手都见过的,完整提示是:
fatal: not a git repository (or any of the parent directories): .git它意思就是:当前目录不是 Git 仓库,或者你的 Git 命令执行位置压根不在项目里。排查顺序如下:
- 先用
pwd看当前目录,确认是不是在项目文件夹里; - 用
ls -a看看有没有.git目录;有.git才说明这是个 Git 仓库; - 如果你原本在子目录里,Git 会自动向上找
.git,所以如果代码目录本身没初始化,才需要执行git init; - 如果你是 clone 下来的仓库,但
.git不见了,比如复制项目时漏了隐藏文件,那就只能重新 clone 或者重新关联远端。
一个实际经验:很多人喜欢复制项目文件夹,复制时把.git隐藏目录落下了。结果新文件夹里的文件全都是源代码,但 Git 不认识它。解决办法不是到处问,而是检查.git是否存在。另外,如果你把一个仓库从 GitHub 迁移到 Gitee,改了 remote 地址,留意.git/config文件里的remote "origin"配置,改错路径也会出现各种诡异拉取异常。
5.2 认证失败:Invalid username or password / Permission denied
这类报错分两类。第一类是 HTTPS 方式下的用户名或密码错误:
remote: Invalid username or password fatal: Authentication failed for 'https://gitee.com/...'GitHub 场景下基本就是 token 过期,或者把账户密码当成 token 用了。Gitee 场景下可能是密码错误或需要令牌。解决路径:重新生成 token,在 Git 询问凭据时输入正确组合;如果本地已经缓存了旧凭据,先清理再重试。
第二类是 SSH 方式下的Permission denied (publickey)。原因一般是公钥没配置到平台、或者 SSH 客户端没找到对的私钥。诊断方法:
ssh -T git@gitee.com ssh -T git@github.com看输出里是否提示你连接成功。如果公钥配置没问题,就看~/.ssh/config是否指向了正确的密钥文件。我还要提醒一件事:有些团队电脑上存在多个账号,公钥加到了这个平台,但git remote -v看到的是另一个平台的地址,也会报Permission denied。仔细核对 remote 地址,不要想当然以为“换了仓库地址就等于换了配置”。
5.3 网络慢、超时与大仓库拉不动
在 GitHub 上拉取大仓库时,偶尔会出现下载到一半中断、超时的情况。Gitee 服务器在国内,通常快很多。这里不讨论任何旁门左道,就说常规范围内能做的优化手段。
第一个建议是浅克隆,只拉取最近一次提交:
git clone --depth 1 https://github.com/user/repo.git这样下载量会小很多。但要注意,浅克隆之后git pull会有一些限制,因为本地历史不完整。如果之后想要完整历史,需要执行:
git fetch --unshallow第二个建议是调整 Git 的 HTTP 缓存区,对大文件友好一些:
git config http.postBuffer 524288000这个配置表示把 HTTP 缓冲区调大到 500MB,对某些二进制文件较大的项目,能减少RPC failed; HTTP 500类错误。
第三个建议是换到更稳定的网络环境,比如公司内网、校园网。还有一个很务实的思路:如果你只是想把 GitHub 上的项目当依赖或源码看,可以在 Gitee 上搜索对应的搬运副本,从 Gitee 拉取通常比直接从 GitHub 拉取稳定得多。很多开源项目在 Gitee 上都有搬运仓库,拉取体验会好不少。
5.4 快速排查清单汇总
| 报错 / 现象 | 可能原因 | 检查/解决动作 |
|---|---|---|
| fatal: not a git repository | 当前位置不是仓库或丢失 .git | pwd、ls -a、重新 clone 或 git init |
| Authentication failed | token/密码错误或过期 | 生成新 token、清凭据缓存 |
| Permission denied (publickey) | SSH 公钥没配或私钥不对 | ssh -T 验证、检查 config、重新添加公钥 |
| refusing to merge unrelated histories | 两个独立历史相遇 | 谨慎使用 --allow-unrelated-histories |
| non-fast-forward | 本地落后于远端 | 先 pull/rebase 再 push |
| RPC failed; curl HTTP 500 | 大文件传输问题 | 调大 http.postBuffer、用浅克隆 |
| Your local changes would be overwritten | 本地未提交修改冲突 | commit 或 stash 后再 pull |
再补充一个很多人没注意的小问题:在 Windows 上使用 Git 时,如果命令行是 cmd 而不是 Git Bash,某些交互式 rebase/冲突编辑器可能打不开。原因很简单,那些工具需要 Unix 风格的环境。建议安装 Git 时把 Git Bash 组件选上,之后操作 Git 都从 Git Bash 进,会省掉不少莫名其妙的麻烦。
5.5 我的一些操作习惯
最后分享几个我自己长期坚持的习惯,不一定适合所有人,但至少能让你在git pull上少受几次折磨。
第一,动手改代码之前必定先拉一次远端。不管是个人项目还是团队项目,这一步能最大限度避免后续冲突。哪怕你昨天刚 clone 下来,今天也值得再拉一次,因为你不知道别人是不是已经推进了几笔提交。
第二,拉取之前先git status确认工作区干净。如果不干净,我会先 commit 起一个临时提交,或者用git stash把修改藏起来,等拉取完成后再git stash pop。这个习惯帮我在大量场景里避免了“本地修改被远端覆盖”的恐慌。
第三,推送之前先拉取,而且优先用git pull --rebase。如果你所在分支只有你一个人在动,rebase 能保证推送历史是一条漂亮的直线,review 的时候体验极佳。如果发现远端已经有你完全不认识的提交,那就老老实实 merge,先把别人的代码消化再推。
第四,遇到看不懂的报错,先看git status再查资料。Git 很多报错其实都是状态问题,比如正处于 merge 中间态、rebase 中间态,或者某个 index.lock 文件残留。git status会直接告诉你“当前正处于哪个操作过程中”,这是排查一切 Git 异常的第一步。
我用 Git 这些年,git pull看起来是最简单的一条命令,但全维度真正理解它的人并不多。把 fetch、merge、rebase、remote 配置、密钥认证这些底层概念串起来之后,你在 GitHub 和 Gitee 之间搬运代码、同步分支、修复冲突,都会顺手很多。如果你现在正好卡在某一个报错上,照着上面的清单一步步排查,大概率能自己搞定。