又到了季度答辩季,我连续帮几个团队的同事过了遍代码仓库,发现一个挺普遍的现象:大部分人都能把Git“用起来”,clone、add、commit、push这四板斧比谁都熟,可一旦遇到分支错乱、代码丢失、push被拒这类稍微绕一点的情况,就只能靠搜索临时抱佛脚。Git这名字听起来简单,实际上它已经是几乎所有研发团队的协作底座,理解它和机械地背命令,工作体验完全是两回事。
这篇文章想聊的,不是把Git官方文档再翻译一遍,而是从我这些年的实际踩坑和带人经验里,整理出一套关于Git的完整打法:从装好环境、日常提交,到多人协作、撤销恢复,再到典型的报错排查链路。无论你是刚装好Git还不知道怎么配的新人,还是已经写了几年代码但总觉得对Git心里没底的同学,都可以按图索骥。
1. 从一个崩溃现场聊聊为什么要认真学Git
1.1 我理解的Git本质:不是备份工具,而是时间旅行器
先讲一个我自己经历过的故事。刚工作的头一年,我在feature分支上写了一下午代码,临时要切回main分支查一个线上问题,顺手就切过去了,结果再切回feature分支的时候发现写的东西全没了。当时我冷汗都下来了,赶紧问同事,才知道原来代码只是落在工作区没提交,切分支时被“藏”起来了,用git stash或者切分支前先commit就能避免。这件小事让我意识到:Git的核心设计从来不是“把代码存起来”,而是“记录每一次变更,并允许你在不同时间线之间穿梭”。
Git把仓库分为三个区域:工作区(你正在编辑的文件)、暂存区(通过git add选中的变更)、本地仓库(通过git commit固化的一次次快照)。加上远程仓库,就构成了完整的四层流转。理解这个模型后,再去理解reset、checkout、revert这些命令的区别会容易得多——它们本质上都是在调整不同区域之间的状态。
1.2 这篇内容适合谁,能帮你解决什么
先说适合的读者。第一种是刚接触Git,下载完安装包之后不知道下一步干吗的新手;第二种是已经用了两三年,能完成日常开发,但遇到分支错乱、冲突、误删除就发怵的同学;第三种是想给团队做内部分享或制定规范的人。
这篇文章不会把Git命令按A-Z完整罗列,而是按真实的工作流组织:先讲环境准备和身份配置,再讲日常提交的高频操作,然后进入多人协作的分支与冲突管理,接着是撤销与恢复这类“后悔药”,最后用真实案例复盘几个最常见的报错排查链路。每一节都会解释“为什么这么做”,而不只是扔给你一串命令。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:从零装好Git并跑通第一个仓库
2.1 各平台安装Git的方式与避坑点
Windows用户建议直接到Git官网下载安装包,选择对应系统的64位版本。安装过程中最需要注意三个选项,很多教程一带而过,但选错后面会非常难受。
第一个是调整PATH的选项,要选“Git from the command line and also from 3rd-party software”。如果你漏选了,在CMD或PowerShell里敲git会提示找不到命令,只能在Git Bash里用,非常憋屈。
第二个是默认编辑器,默认是Vim。对不熟Vim的人来说,某次commit时不小心进了vim界面,整个人就卡住了——按i进入插入模式,输入提交信息后按Esc,再输入:wq回车才能退出。如果你装了VS Code,建议顺手选成VS Code,能省掉很多不必要的困扰。
第三个是关于行尾换行符(Line Ending)的转换策略。多人在不同操作系统之间协作时,Windows用CRLF,macOS和Linux用LF,处理不好会看到大量“整个文件被修改”的假diff。Git官方给出的建议是:Windows下checkout时转为CRLF、提交时转回LF,macOS/Linux选择checkout as-is / commit as-is。
macOS系统自带Git,可以直接在终端里用git --version确认。不过自带版本通常偏旧,我建议通过Homebrew安装新版本:
bash复制brew install git
Linux用户更简单,Debian/Ubuntu系用apt install git,CentOS/RHEL系用yum install git或dnf install git。
2.2 装完以后第一件事:配置身份信息与默认值
很多新手装完Git第一件事是急着建仓库,结果提交后发现commit记录里的作者名是一串系统生成的字符,邮箱也不对。这些信息会永久写入提交历史,后期虽然可以改写,但一旦推送到远程、多人拉取过,改起来非常麻烦。
装完Git之后,请立刻执行以下配置:
bash复制git config --global user.name "你的姓名"
git config --global user.email "你的邮箱"
git config --global init.defaultBranch main
git config --global pull.rebase false
我简单解释一下每条的意义。user.name和user.email会写入每一次commit的元数据,代码评审和问题追溯全靠它。init.defaultBranch main把git init创建的默认分支名从老旧的master改成main,避免新建仓库后在推送时还要手动改分支名。pull.rebase false是让git pull默认采用merge方式而非rebase,对于多数团队来说,merge方式产生的结果更直观,出问题的概率更低。
这里有个很多人不知道的细节:git config的作用范围分为system、global、local三层。global是当前用户全局生效,local是当前仓库内生效,优先级是local高于global。比如公司项目里需要提交到公司代码库的邮箱,个人开源项目用另一个邮箱,就可以在对应仓库目录里单独执行git config user.email "工作邮箱"覆盖全局配置。
执行完配置后,用git config --list查看所有生效的配置项,确认没问题再继续。
2.3 配置SSH Key并连接GitHub
日常和远程仓库交互有两种方式:HTTPS和SSH。HTTPS每次push可能需要输入用户名密码或Personal Access Token,SSH则通过密钥对免密认证,体验更顺滑。我个人的建议是:直接用SSH。
生成SSH Key的命令是:
bash复制ssh-keygen -t ed25519 -C "你的邮箱"
一路回车,会在~/.ssh/id_ed25519和~/.ssh/id_ed25519.pub生成私钥和公钥。私钥留在本地,公钥复制到代码托管平台。查看公钥内容:
bash复制cat ~/.ssh/id_ed25519.pub
以GitHub为例,登录GitHub后进入Settings -> SSH and GPG keys -> New SSH key,把公钥粘贴进去保存。然后验证是否连通:
bash复制ssh -T git@github.com
如果看到包含你用户名的成功提示,说明配置完成了。这里有个常见坑:如果提示Permission denied (publickey),先检查公钥是否复制完整,再确认ssh-agent是否在运行,必要时执行eval "$(ssh-agent -s)"后把私钥加入ssh-add。
2.4 创建第一个仓库并推送到远程
环境配置完毕,我们来跑通一条最完整的链路:本地初始化仓库,再推送到GitHub。
bash复制mkdir my-project && cd my-project
git init
echo "# my-project" > README.md
git add .
git commit -m "chore: init project"
git branch -M main
git remote add origin git@github.com:用户名/my-project.git
git push -u origin main
简单说明每步在做什么。git init在当前目录初始化一个Git仓库,生成隐藏的.git目录,所有版本信息都存放在这里。git branch -M main把当前分支重命名为main,避免分支名不一致。git remote add origin把本地的remote指向远程仓库,origin只是一个约定俗成的名字,你也可以改成别的。最后git push -u origin main把本地main分支推送到远程,-u参数会建立本地分支和远程分支的追踪关系,之后直接git push、git pull就能自动识别要同步的远程分支。
跑通这一步之后,你的Git环境就算完整就绪了。
3. 每天都会用到的Git命令是怎么串起来的
3.1 标准提交流程:从工作区到远程仓库
日常开发中每天在重复的,其实是一个标准四步:修改文件、暂存、提交、推送。用生活类比来讲清楚这四个状态:工作区就像你的草稿纸,正在写写画画;暂存区像“定稿区”,你把满意的段落用git add放进这个区域;本地仓库像你装订好的书,git commit表示把定稿的内容装订成册;远程仓库就是把这本册子邮寄给团队所有人,git push干的是这件事。
实际执行时,最基础的命令组合长这样:
bash复制git status # 查看当前工作区状态,养成先看再动的习惯
git diff # 查看未暂存的具体改动
git add <file> # 把文件加入暂存区
git commit -m "feat: 完成登录页表单校验"
git push # 推送到远程
我特别想强调一个习惯:不要动不动就git add .。当你一次改动涉及多个文件、多个逻辑点时,一把梭提交出来的commit会让后续review和回溯非常痛苦。更推荐的做法是分段提交:用git add精确选择某个文件,让每个commit只承载一个逻辑改动。git add -p甚至能交互式地选择同一个文件里的不同代码块,适合那种一个文件里既有重构又有新功能的场景。
3.2 分支操作:switch与merge的正确姿势
分支是Git最强大的能力,也是新手最容易绕晕的地方。我推荐从今天开始就使用git switch系列命令来管理分支,而不是继续用老旧的git checkout。原因是git checkout同时承担了“切换分支”和“恢复文件”两个职责,一不注意就会把命令敲错,导致工作区文件被意外恢复。git switch则职责单一,只管切换分支,误操作概率小很多。
常用分支命令:
bash复制git switch -c feature/login # 创建并切换到新分支
git branch -vv # 查看所有分支及追踪关系
git switch main # 切换到已有分支
git branch -d feature/login # 删除已合并的分支
git merge feature/login # 把该分支合并到当前分支
关于merge和rebase怎么选,我给一个比较务实的结论:如果你在维护自己的feature分支,且还没有推送到远程,使用rebase把主干上的新提交整合进来,能让分支历史保持线性,review时更清爽;如果分支已经推送到远程、被别人拉取过,就老老实实用merge,不要改写已经公开的历史。团队协作中,“不修改已push的历史”是一条铁律。
3.3 查看历史与定位问题:log与diff
排查问题最常用的两个命令是git log和git diff。
git log默认输出非常冗长,我平时几乎都会带参数:
bash复制git log --oneline --graph --decorate --all
这个组合会生成一张带分支图的简洁历史表,一行显示一个提交,一眼就能看清分支什么时候分叉、什么时候合并。
如果需要看某次提交改了什么内容:
bash复制git log -p -2 # 查看最近2次提交的完整差异
git show <commit-hash> --stat # 查看某次提交涉及的文件和改动量
git diff # 工作区与暂存区的差异
git diff --staged # 暂存区与最近一次提交的差异
我经常用这套命令做代码考古:发现某行代码有问题,用git log -p -- <file>看它的修改历史,再用git blame定位是谁在哪个提交里引入的,能省掉大量找原因的时间。
3.4 临时躲避:stash的使用场景
还有一个操作频率不低但很多人不了解的:git stash。它解决的问题是:当前工作区有一堆没改完的代码,但你需要紧急切换分支处理别的事情,比如线上出了一个hotfix。
bash复制git stash push -m "登录模块未完待续"
git stash list
git stash pop
git stash会把当前未提交的改动保存到一个临时栈中,让工作区恢复到干净状态,切换分支时就不会把半成品带过去。事情处理完切回来,执行git stash pop恢复现场。如果你在stash之后对代码做了别的改动,再pop可能会产生冲突,手动解决一下即可,不用慌。
4. 多人协作:分支策略、Code Review与冲突实战
4.1 不要迷信“标准流程”:三种分支策略怎么选
分支策略是个容易被神化的话题。有的团队一上来就照搬Git Flow,线上线下环境一大堆分支,结果两三百人的团队维护起来筋疲力尽。我的建议是:先看发布节奏和团队规模,再选策略,不要盲目跟风。
我给三种主流模型做了个对比:
| 模型 | 核心思想 | 适用场景 | 需要关注的问题 |
|---|---|---|---|
| Git Flow | main、develop、release、hotfix多分支并行 | 版本周期发布、需要同时维护多个版本 | 分支多、流程重,小团队会累 |
| GitHub Flow | 所有功能基于main短分支,PR合并后即可发布 | 持续交付、发布节奏快的团队 | 要求测试覆盖率高,否则main容易不稳定 |
| 极简分支 | 一条main,功能分支用完即删 | 2-5人小团队、项目早期 | 需要保护分支和CI兜底 |
选择的关键是:环境有多少套、发布频率多高、团队规模多大。没有放之四海皆准的方案,但有一条通用原则——分支存在的周期越短,合并冲突越少,团队协作越顺畅。
4.2 冲突是怎么产生的,以及一套可复现的解决流程
很多新手遇到冲突就紧张,我想先安抚一句:冲突不是你的代码写得差,而是两个人在同一段时间里改了同一文件的同一片区域,Git无法自动判断该保留谁。这是协作中的正常现象,熟练解决冲突是每个用Git的人必备的技能。
冲突出现的典型场景:你从main拉了一个分支,改了login.js的第20行,同事也改了login.js的第20行。当你要把自己的分支合并进main,或把远程main合并到你的分支时,Git发现同一行有两个不同版本,无法决定,于是标记冲突。
完整的解决流程:
- 先执行
git pull --rebase或git merge main,让本地分支拥有最新的main。 - Git会提示哪些文件冲突了,用编辑器打开,能看到类似下面的标记:
text复制<<<<<<< HEAD
你的代码
=======
同事的代码
>>>>>>> feature/同事分支
<<<<<<<到=======之间是当前所在分支的代码,=======到>>>>>>>之间是合并进来的分支代码。根据需求保留一方、两方都留,或重写成新代码,然后把三个特殊标记行删掉。- 处理完一个文件后执行
git add <file>,把该文件标记为已解决。 - 冲突全部解决后,如果是rebase模式执行
git rebase --continue,merge模式则执行git merge --continue,Git会打开编辑器让你补充一条提交信息,保存退出即可。 - 最后务必本地编译并跑一遍相关测试,再推送远程。
这里有一个我反复强调的教训:冲突解决完不要立刻push,先本地构建验证。你以为只是合并了一下代码,但合并后的逻辑可能同时用了两个人的半套实现,编译直接过不去的情况我遇到过太多次。
4.3 提交信息与Code Review的约定
团队协作的默契,一半体现在提交信息的规范上。我比较推荐Conventional Commits这套广泛使用的约定,格式是“类型: 简短描述”,常见类型包括:
feat:新功能fix:修复bugdocs:文档改动style:不影响代码逻辑的格式调整refactor:重构test:测试相关chore:构建、依赖等杂项
配套的原则是“一次提交只做一件事”。Code Review的时候,希望看到的是一个逻辑清晰、改动量可控的commit集合,而不是一个改了20个文件、包含了5个不同目的的巨型commit。
4.4 用保护分支给团队加一道保险
在GitHub或GitLab上,我强烈建议给main分支开启Branch protection规则,至少勾选:
- Require a pull request before merging
- Require status checks to pass before merging
- Require linear history(如果团队采用rebase合并)
- 禁止直接push规则
这样一来,任何改动都必须通过PR、经过CI检查和至少一位同事review才能进入main,从机制上避免了“手抖push覆盖”这类事故。
5. 撤销与恢复:Git的“后悔药”全家桶
5.1 未提交的改动:restore与reset如何选择
先说还没提交的情况。此时你改坏了文件,想回到改动前的状态。老教程会告诉你git checkout -- <file>,但更现代、语义更清晰的命令是git restore <file>。
分两种情况:
- 修改还没
git add:直接git restore <file>,工作区回到最近一次commit的状态。 - 修改已经
git add但还没commit:先git restore --staged <file>取消暂存,再git restore <file>恢复文件。
restore --staged的作用是把文件从暂存区挪回工作区,但保留内容的修改。这两个组合是“撤销暂存+撤销修改”的标准流程。
注意一个重要的边界:如果修改后的内容已经被commit了,restore就帮不上忙了,需要看下面两节的内容。
5.2 已提交但未推送:amend、reset与reflog
如果只是最近一次commit的信息写错了,或者漏提交了一个文件,用git commit --amend最方便:
bash复制git add 遗漏的文件
git commit --amend -m "修正后的提交信息"
--amend会把暂存区的改动并入上一条commit,而不是新建一条,适合“补丁式”的修正。
如果想撤销的不止一条commit,而是回到某一个历史版本,git reset就出场了。reset有三种模式,区别在于对工作区和暂存区的处理:
| 模式 | 暂存区 | 工作区 | 适用场景 |
|---|---|---|---|
--soft |
保留 | 保留 | 只想撤销commit,改动还留在暂存区 |
--mixed |
清空 | 保留 | 默认模式,撤销commit并取消暂存 |
--hard |
清空 | 清空 | 彻底回退,所有未提交改动都会被丢弃 |
用法示例:
bash复制git reset --soft HEAD~1 # 撤销最近一次commit,改动留在暂存区
git reset --mixed HEAD~1 # 撤销最近一次commit,改动回到工作区
git reset --hard HEAD~1 # 丢弃最近一次commit及所有未提交改动
特别提醒:--hard很危险,执行后未提交的改动会永久丢失。我个人的习惯是执行reset --hard前先看一眼git status确认没有值得保留的东西,或者先git stash一手。
这里必须强调一点:这三兄弟只适用于尚未push的commit。如果commit已经推送到远程,千万别用reset去“抹掉”它,原因下面会说。
5.3 已推送的提交:为什么首选revert而不是reset
假设你已经把一条commit推送到远程,后来发现它有bug,想撤销。这时候如果本地git reset --hard <旧版本>再git push --force,会发生什么?远程分支被你强行覆盖,但同事可能已经pull过你这条commit,甚至在其上继续提交,你的强制推送会把他们的提交历史搅乱,轻则一堆冲突,重则互相覆盖,这是团队协作里最让人血压升高的操作之一。
正确的做法是git revert:
bash复制git revert <commit-hash>
revert不会删除历史,而是生成一条新的commit,把指定commit的改动反向应用回去。这样历史是线性追加的,团队成员各自pull都不会有问题,也方便回滚revert本身。
一句话总结:未推送的历史可以随便改,已推送的历史要向前走,不要向后抹。
5.4 最后一道防线:reflog恢复被删的提交和分支
如果上面这些操作你都已经用了,代码看起来还是“消失”了,先别慌,Git还有一个终极后悔药:git reflog。
reflog记录的是本地仓库中HEAD指针每次移动的历史,包括reset、merge、checkout、commit等操作。即使你reset --hard到一个旧版本,刚才被“丢弃”的提交信息也还躺在reflog里,并没有被立刻清除。
bash复制git reflog
输出类似:
text复制abc1234 (HEAD -> feature/login) HEAD@{0}: commit: feat: 完成登录模块
def5678 HEAD@{1}: reset: moving to HEAD~1
ghi9012 HEAD@{2}: commit: feat: 增加注册页面
如果发现自己误删了一个分支或reset过头,只要在reflog里找到那个想要的commit hash,然后执行:
bash复制git branch recover-branch abc1234
就能把这个分支的完整历史恢复出来。这个命令我救回过不少同事的“删掉的分支”,强烈建议每个Git用户都记下来。
6. 我踩过的Git坑:完整排查链路与预防姿势
6.1 把代码提交到了错误的分支
这是一种非常常见的翻车现场。你本来应该在feature分支开发,结果在main上改了好几个小时,顺手就commit了,等发现时已经晚了。别慌,有三步可以完美解决:
- 在main的当前位置创建一个正确分支:
git branch feature/actual-work,把当前含代码的状态保留下来。 - 把main回退到commit之前:
git reset --hard HEAD~1,注意此时刚才的commit已经在feature分支上,main回退不影响代码。 - 切换到正确分支:
git switch feature/actual-work。
这个流程的本质是:先用branch保留成果,再用reset清理错误位置,最后切换。也可以用git cherry-pick把提交转移到正确分支:
bash复制git switch feature/actual-work
git cherry-pick <错误提交的hash>
git switch main
git reset --hard HEAD~1
两套方案都能解决问题,我更喜欢前者,因为它少依赖一个commit hash,操作路径更直觉。
6.2 push被拒:non-fast-forward的问题链
git push被拒时,最典型的报错是! [rejected] main -> main (non-fast-forward)。如果不理解原因,很多人第一反应是git push --force,这是最危险的误操作。被拒的本质是:远程分支有一个或多个你本地没有的提交,Git默认拒绝覆盖。一个负责任的排查链是这样:
- 先看远程最新状态:
git fetch origin - 对比本地与远程的分叉:
git log --oneline --graph --all - 把远程的新提交整合到本地:
git pull --rebase(如果你在2.2里配置了pull.rebase false,命令行参数--rebase会覆盖这个配置)。此时如果双方改了同一处,会进入冲突解决流程。 - 冲突解决完毕后
git push。
只要你的本地分支包含了远程分支的所有提交,push就能正常完成。pull --rebase是很多团队推崇的同步方式,虽然它会改写本地尚未推送的提交,但对没有push过的本地提交来说,这是完全安全的,并且能让历史更线性。
6.3 误提交大文件或敏感信息怎么处理和预防
误提交大文件的问题要防患于未然。比如不小心把几百MB的数据库dump、node_modules里的某个包、或者.env配置文件提交进了仓库,会导致仓库体积暴涨,其他人clone变得极慢。
如果还没push到远程,直接git reset到提交前,再在.gitignore里加一行规则即可。例如:
gitignore复制.env
*.log
node_modules/
dist/
如果已经push了,那就需要改写历史,典型方案有两个:git filter-branch和BFG Repo-Cleaner。但改写历史之后仓库里仍然会残留对象,直到垃圾回收才真正删除,而且所有协作者都需要重新clone一遍仓库,成本不低。
所以我的建议永远是:预防优先。在项目初始提交之前就配置好.gitignore,或者用Git LFS管理真正需要入库的大文件,远比事后清理轻松。敏感信息泄露又是另一类严重问题,一旦key或密码进了历史,哪怕删掉也没用,正确的做法是立即吊销该密钥并轮换,同时清理历史。
6.4 认证问题与几个高频报错的快速排查
最后整理几个我实际工作中反复遇到的报错和对应的排查顺序:
| 报错 | 直接原因 | 正确操作 |
|---|---|---|
Support for password authentication was removed |
GitHub等平台已不支持密码直接push | 改用SSH Key或Personal Access Token |
fatal: refusing to merge unrelated histories |
两个仓库没有共同祖先,常见于本地仓库和远端初始内容不一致 | 确认无误后git pull origin main --allow-unrelated-histories,或者统一从远程clone |
Permission denied (publickey) |
SSH公钥未配置或ssh-agent未启动 | 检查公钥配置、重启ssh-agent、确认ssh -T能认证 |
Your branch is ahead of 'origin/main' by N commits |
本地有未推送的提交 | 直接git push即可,不是错误 |
我特别想展开的是第二个报错。很多新人的做法是先在GitHub上创建仓库并勾选了“Add a README file”,然后又在本地git init并提交了本地文件,最后git remote add origin再push,就会撞上unrelated histories。解决方式是用--allow-unrelated-histories允许两段历史合并,但如果你还没在本地写太多东西,更简单的是直接把本地目录清空重新git clone,避免制造双份历史。
把上面这些内容过一遍,Git从安装、配置、日常流程到协作、撤销和排错的这些关键链路基本就有了骨架。老实说,这些操作没有哪条命令是复杂的,真正难的是在遇到问题时能冷静推断出“我现在处于哪个状态,应该把状态移动到哪个目标状态”。这也是为什么我反复强调要理解工作区、暂存区、仓库、远程这四个概念,而不是死背命令。
最后再分享一个我个人的小习惯:每次准备执行任何可能破坏性的操作(比如reset --hard、rebase、bisect)之前,先花10秒执行一句git switch -c backup/当前日期建一个备份分支,或者至少确认reflog有记录。这套成本极低的动作,帮我省掉了无数次想锤墙的瞬间。Git这个工具,你越是敬畏它背后的模型,它在关键时刻就越可靠。
