学 Git 这件事,我从最开始的“用文件后缀区分版本”进化到“所有项目都先git init”,中间踩了不少坑,也慢慢把零零碎碎的命令串成了自己能讲清楚的一套体系。这篇东西与其说是教程,不如说是一份真实的学习记录:Git 安装与配置、日常高频命令、免密登录、常见报错和几个容易被忽视的底层参数。如果你刚接触 Git 或者用了很久却总是“会用但说不清”,这篇文章应该能帮你把知识补完整。我尽量少讲抽象理论,多放可以直接实测的命令和场景。
1. 为什么每个人都值得系统学一遍 Git
1.1 版本管理到底在解决什么问题
先聊一个最基础的问题:为什么要用版本管理工具?以我个人的经历来说,最直观的痛点是“改坏了想回退却找不到原来的版本”。很多没有版本管理习惯的人,会把项目目录变成这个样子:
project ├─ 论文终版.doc ├─ 论文终版_改2.doc ├─ 论文终版_改3_真不改了.doc ├─ 论文终版_改4_再也不改了.doc └─ 论文终极版_最终.doc这种命名方式有两个致命问题:第一,你想回到三天前的状态,只能靠手动备份,晚一步就追悔莫及;第二,多个人同时改一个文件时,根本没有冲突解决机制,只能靠微信传来传去。Git 的核心价值就体现在这里,它把你的项目变成一本可以随时翻阅的账本,每一次提交都记录了“谁在什么时间、改了什么、为什么改”,并且支持任意回溯到任何一个历史节点。
版本管理概念曾经被认为只有程序员才需要,但实际上文档写作、设计稿管理、数据分析脚本都可以用 Git 管理。我自己甚至用 Git 管理过配置文件目录和笔记目录,效果相当不错。真正的分水岭不是“你会不会用某个软件”,而是你有没有建立起“提交点、分支点、回退点”这套思维。
1.2 Git 和传统版本管理工具的差异
在我学习 Git 之前,还短暂接触过 SVN(Subversion)。SVN 采用集中式版本控制,服务器是唯一权威,所有提交都要先连上服务器;Git 则采用分布式版本控制,每个开发者本地都有一份完整的仓库历史。这个差异造成了几个实际体验上的区别:
- 断网也能提交。Git 的提交发生在本地,不需要和服务器交互;SVN 一旦连不上服务器,连 commit 都做不到。
- 分支成本极低。Git 的分支本质是指向提交的指针,创建、切换基本是瞬间完成;SVN 的分支往往要复制整个目录,慢且容易出问题。
- 历史就是财富。Git 把每一次提交都串联成有向无环图,你可以在本地自由探索、合并、重写历史;SVN 的历史管理则要笨重得多。
Git 的分布式特性一开始可能让人不太适应,尤其是第一次接触“本地仓库”和“远程仓库”这两个概念的人。但一旦理解了“提交在本地、推送在远程、拉取是同步”,整个协作模型就清晰了。后面我会用完整的实战流程,把这句话展开讲清楚。
2. 从零搭建 Git 工具链:下载、安装与初始化配置
2.1 全平台安装 Git 的三种方式
我最早在 Windows 上接触 Git 时,直接下载了 Git for Windows 的安装包。这个安装包内置了 Git Bash、Git GUI 和命令行工具,属于“全家桶”式安装。安装时有一个比较容易踩坑的选项是“调整 PATH 环境变量”,建议选择Git from the command line and also from 3rd-party software,这样我可以在 PowerShell、CMD 甚至 IDE 的终端里直接用git命令,而不必特意打开 Git Bash。
macOS 上最简单的方式其实是安装 Xcode Command Line Tools,终端输入git --version时会自动弹出安装提示。如果不想用系统自带的版本,或者需要较新的 Git,我一般用 Homebrew:
brew install gitLinux 平台则优先走系统包管理器。Ubuntu/Debian 用sudo apt install git,CentOS/RHEL 用sudo yum install git。装完后统一验证一下版本:
git --version安装本身不是难点,真正的坑在于后续的换行符、编码、编辑器默认设置。这些问题如果等到团队协作、多人提交时才暴露,排查起来相当痛苦,所以建议安装之后马上做一次系统配置。
2.2 全局配置 user.name、user.email 与默认编辑器
Git 的每一次提交都会记录作者信息,所以安装完成后要立刻设置全局用户名和邮箱。我见过很多新手跳过这一步,结果提交时 Git 自动从系统用户名猜测出类似user@DESKTOP-ABC123这样的信息,提交记录看起来像乱码一样。
常见的配置指令如下:
git config --global user.name "Your Name" git config --global user.email "you@example.com"--global表示这份配置作用于当前用户的所有仓库。如果某个项目需要单独身份(比如公司仓库用公司邮箱),可以在进入该项目目录后去掉--global再执行一遍,仓库级配置会覆盖全局配置。
还要设置一个顺手的默认编辑器,否则执行git commit但没有-m参数时,Git 会调用系统默认编辑器,比较难退出。我习惯设置为 VS Code:
git config --global core.editor "code --wait"如果你更喜欢 Vim,就设置成vim。这个细节看似无关紧要,但在你第一次执行git commit忘记带-m时,能救你一命。毕竟当年我第一次在 Vim 里不知道怎么保存退出,卡了好几分钟才查明白要按:wq。
顺手再配一个高亮颜色和一个更直观的日志格式:
git config --global color.ui true git config --global alias.lg "log --oneline --graph --all --decorate"有了git lg这个别名之后,查看分支拓扑图就非常直观了。
2.3 Git 免密配置:SSH Key 与 credential helper
几乎所有人都会被 Git 的账号密码验证折磨过。每次 push 都要输入账号密码,既不安全也浪费时间。配置免密的核心思路有两条路:SSH 协议免密和 HTTPS 凭据缓存。
SSH 方式是更推荐的方案。首先生成密钥对:
ssh-keygen -t ed25519 -C "you@example.com"生成过程中会询问保存路径和 passphrase,直接回车使用默认路径(~/.ssh/id_ed25519)即可。如果不设置 passphrase,就意味着持有该私钥的人不需要密码就能访问仓库,要注意保管好私钥文件。然后把公钥内容复制到代码托管平台的 SSH Keys 设置里:
cat ~/.ssh/id_ed25519.pub最后验证连接是否成功:
ssh -T git@github.com如果你用的是 Gitee、GitLab 或者公司内网 GitLab,把域名换掉即可。验证通过后,把远程仓库地址改成 SSH 格式(形如git@github.com:user/repo.git),再 push 时就不再需要输入任何密码了。
如果项目形式比较特殊,只能用 HTTPS 方式,那可以启用 Git 自带的凭据存储工具:
git config --global credential.helper store这样第一次输入账号密码后,Git 会明文把凭据写入~/.git-credentials。安全性要求不高的环境可用,但注意不要把这份文件提交到仓库或泄露给他人。macOS 用户还可以配置osxkeychain,Windows 用户可以选择manager-core,即 Windows 凭据管理器来存储,体验更好一些。
3. 把 Git 命令拆成一套能记住的工作流
3.1 单人开发用得最频繁的本地命令
我见过很多速查表把 Git 命令铺得密密麻麻,实际用起来反而让人害怕。如果先把“单人开发最常用的命令”掌握牢,后面的协作概念会自然延伸出来。我的教学思路是从一条完整链路开始的:
git init # 在项目目录初始化仓库 git status # 查看工作区状态 git add . # 把所有改动加入暂存区 git commit -m "feat: 完成登录模块" # 把暂存区内容提交成历史记录 git log --oneline # 查看提交历史很多新手混淆工作区、暂存区和版本库这三个概念。工作区就是你写代码时看到的目录;暂存区是提交前的“待确认区域”,相当于购物车;版本库是已经提交的历史,相当于已支付的订单。git add是把选中的货物放进购物车,git commit才是真正结账打包。
单人项目里还有两个常用命令不能漏,一个是git diff,用来查看未暂存的改动;另一个是git diff --staged,用来查看已暂存但未提交的内容。养成提交前先git diff的习惯,可以避免把调试用的临时代码顺手提交进去。我这么些年下来最深的感受是:提交信息要写得像“给未来同事发消息”一样清楚,而不是“fix bug”这种没法追溯的废话。项目里后来强制要求提交信息遵循feat、fix、docs、refactor这些约定,回溯历史时效率明显高很多。
3.2 分支与合并:让多条开发线并存
分支是 Git 里最值得深入理解的概念。一句话解释:分支就是指向某个提交的可移动指针。默认分支一般叫master或main,你可以随时从某个提交上开出新分支。
创建并切换分支最常用的一行命令是:
git checkout -b feature/login这行命令等价于先git branch feature/login再git checkout feature/login。从 Git 2.23 开始,官方更推荐用git switch和git restore来区分“切换分支”和“恢复文件”两个语义:
git switch -c feature/login # 创建并切换 git switch main # 切换回主分支切到新分支后,你就可以放心大胆地改造代码,因为 main 分支不会受影响。等新功能开发完毕,需要把 feature 分支合并回主分支:
git switch main git merge feature/login如果两个分支各自修改了同一个文件的同一块区域,Git 无法自动判定哪个版本是正确的,就会产生合并冲突。我刚开始遇到冲突时觉得很棘手,其实冲突格式非常固定,文件中会出现类似下面的标记:
<<<<<<< HEAD 当前分支的内容 ======= 另一个分支的内容 >>>>>>> feature/login手动修改后重新执行git add和git commit,合并就完成了。关于冲突的详细处理方案,我在后面专门写一节。
分支用多了,还要注意“清理”问题。合并完的分支保留着容易让分支列表变得混乱,建议及时删除:
git branch -d feature/login如果某个分支确定不再需要、且未合并的内容也不想要了,用大写-D强制删除。我在公司里见过有人因为保留了几十个别名已不可考的分支,吓得都不敢随意切换。建议按“功能粒度开分支,按周粒度合主分支”的节奏来维护。
3.3 远程仓库协作:clone、pull、push 与 fetch
本地分支和远程分支之间是什么关系,是我学 Git 时困惑最久的点。理解这条路之后就会豁然开朗:
git clone git@github.com:user/project.git # 把远程仓库完整复制到本地 git pull origin main # 拉取远程最新内容并合并到当前分支 git push origin main # 把本地提交推送到远程git pull实际上是git fetch加git merge的组合。fetch只是把远程仓库的最新状态下载到本地“远程跟踪分支”上,比如origin/main,并不会动你当前的工作区。而pull在 fetch 完自动执行 merge,所以如果本地有未提交的改动,pull 时可能因为合并冲突而中断。
协作中的常见操作程序是这样的:
1. git pull origin main 拉最新代码 2. git switch -c feature/xxx 新建自己的功能分支 3. 开发完成后 git add、git commit 提交 4. git push -u origin feature/xxx 推到远程自己的分支上 5. 在代码托管平台上发起合并请求 6. 审核通过后合并到主分支,删除远程分支我第一次用git push -u origin feature/xxx时,一直不理解-u是干什么的。它其实是--set-upstream的缩写,作用是建立本地当前分支和远程分支的关联关系。建立之后,后续直接执行git push或git pull就不用再带远程分支名了。如果你之前已经用git clone拉过仓库,主分支的 upstream 关系会自动建立,所以很多人没接触过-u也很正常。
远程协作中还有一个高频场景:别人已经推送了新内容到 main,而你在本地 main 上也有新提交,这时直接git pull大概率会生成一条“分叉再合并”的记录,如果团队希望历史保持线性,可以用git pull --rebase代替。关于 rebase 的用法和风险,我放到后面的避坑章节展开讲。
3.4 撤销与回滚:reset、revert、checkout 三种场景
“改错了想撤销”是入门用户最关心的问题。Git 的撤销方法有好几条路径,分别对应不同的状态,很多人记不住是因为没有把场景分类。我把它们拆成三类来看:
第一类,文件还没git add,只想把工作区的改动丢弃:
git restore README.md等价于旧的写法git checkout -- README.md。这条命令不可逆,所以执行前先确认自己确实不要这些改动了。
第二类,文件已经git add,想把暂存区清空但保留工作区改动:
git restore --staged README.md这个操作很常用,比如你习惯性git add .后发现把.env文件也加进去了,就可以用这行命令把文件“移出购物车”。
第三类,提交已经发生了,但提交错了想回退。如果只想改最近一次的提交信息,用git commit --amend;如果想完全回退到之前某个提交,属于“修改历史”的重操作,需要区分 reset 和 revert 两种思路。
git reset是把当前分支指针移回某个历史提交,有三种模式:
git reset --soft HEAD~1 # 回到上一个提交,但保留暂存区内容 git reset --mixed HEAD~1 # 回到上一个提交,保留工作区但清空暂存区 git reset --hard HEAD~1 # 彻底回到上一个提交,工作区和暂存区都丢弃--hard模式最容易造成代码丢失,我建议在不确定时先加git stash或手动备份。重要分支最好不要随意reset --hard,因为如果改动已经被推送到远程,其他人拉取时会非常头疼。
git revert的思路是生成一个“反向提交”,用一个新的提交把某次历史提交的效果抵消掉。它不会改动历史,因此适合用于已经推送到远程的公共分支:
git revert 8f3a2b1如果你在团队协作中误提交了不该提交的内容,优先考虑revert而不是reset,因为前者的影响范围是增量式的,其他成员执行git pull就能安全获得修复。
4. 隐藏在日常命令背后的参数细节
4.1 解密 git -c 临时配置参数的执行逻辑
经常有人在 IDE 或第三方工具的日志里看到类似这样的命令:
git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks status第一次见这种命令的人通常会以为这是自定义脚本,其实它只是在调用 git 时临时追加了几个配置项。-c <key>=<value>的作用是在本次命令执行期间覆盖配置,比全局改git config更安全,因为不会把临时参数写入任何配置文件。
这条长命令里的三个参数分别很有讲究。
diff.mnemonicprefix=false:控制git diff输出时文件名前面的前缀。默认情况下,Git 会分别用a/和b/表示改动前后的两个文件,比如diff --git a/README.md b/README.md。设置了mnemonicprefix后则可能用index/、worktree/等更“助记”的前缀对不同场景做区分。但某些工具和解析器并不关心这种区别,反而希望始终统一用a/和b/,所以显式设置为false,保证输出格式稳定,方便做后续的文本解析或补丁处理。
core.quotepath=false:与中文文件名展示有关。Git 在默认情况下会采用类似 C 语言转义的方式来输出超出 ASCII 范围的特殊字符,所以你会看到中文文件名显示为一串\346\265\213\350\257\225.txt之类的八进制转义序列。设置core.quotepath=false后,Git 会直接输出原始 UTF-8 字符,中文文件名可以在终端里正常显示。这个参数不仅 IDE 会加,我建议在所有仓库里也全局配置上:
git config --global core.quotepath false--no-optional-locks:Git 在运行一些只读命令(如git status)时,为了性能会尝试刷新索引文件,这个刷新过程会获取索引文件上的可写锁。可问题是,当 IDE 正同时监听文件变化时,这种隐式写入可能带来锁竞争。加上--no-optional-locks后,Git 会跳过这类非必需的锁获取,从而减少与其它进程的冲突,让命令执行更“只读”。
这条长命令之所以被很多 GUI 工具使用,本质上是因为它希望调用时不污染用户的全局配置,并且能够获得稳定、可解析的输出结果。如果你想在命令行里复现这套行为,直接在原命令前加上这些参数就行。理解了它,你再看到 IDE 里那些密密麻麻的日志,就不会觉得它神秘了。
4.2 .gitignore、中文路径与换行符的隐藏坑
新手容易在项目初始化时忽视.gitignore,结果误把node_modules、target、__pycache__、.idea等目录提交进仓库。一旦提交进去,即使之后在.gitignore里加上规则,这些目录也不会自动被移除,因为.gitignore只管“未跟踪文件”。
所以建议用模板生成一套基础忽略规则。GitHub 官方维护了一份常用.gitignore模板库,可以直接选择对应语言的模板下载。另外有两个通用规则值得记住:
# 忽略所有 .env 文件 .env # 忽略日志目录 logs/ # 取反:保留 logs 目录下的 .gitkeep !logs/.gitkeep换行符问题在 Windows 和 macOS/Linux 混合开发的团队中极其常见。Windows 的换行是 CRLF(回车+换行),Linux/macOS 使用 LF(只换行)。如果 Git 不统一处理,你会看到明明只改了一行代码,git diff却显示整个文件都被修改了。
推荐的做法是让 Git 在提交时自动把 CRLF 转成 LF,在检出时按系统类型还原:
git config --global core.autocrlf input # macOS/Linux git config --global core.autocrlf true # Windows团队项目还可以在仓库根目录放一个.gitattributes文件,显式指定文本文件的换行规则,这样比每个成员都依赖本机配置更保险。如果以前已经提交过大量 CRLF 文件,需要执行一次规范化操作,这个过程比较繁琐且容易引发大面积 diff,建议规划好专门的时间处理。
4.3 用别名和 IDE 集成提升操作效率
我还在 “终端上一个个敲命令” 的阶段时,效率并不高。后来把最常用的几个命令配成了别名,输入成本下降一截。除了前面提到过的git lg,我常用的还有:
git config --global alias.st "status -sb" git config --global alias.ci "commit" git config --global alias.co "checkout" git config --global alias.br "branch -vv" git config --global alias.unstage "restore --staged ." git config --global alias.last "log -1 HEAD --stat"需要注意的是,不要把所有命令都做成别名,因为换一台机器、没有同步配置时会很不适应。至少保留对裸命令的理解能力,别名只是锦上添花。
IDE 的 Git 集成现在也相当成熟。以 VS Code 为例,左侧的源代码管理面板可以完成暂存、提交、推送、冲突解决等操作;JetBrains 全家桶内置的 Git 工具甚至能可视化地展示分支拓扑。我个人的习惯是日常“查看状态、diff、添加文件”用 IDE 或 GUI,涉及 reset、rebase、cherry-pick 等需要精确控制的操作仍然用终端。GUI 能降低入门门槛,但无法替代对命令本质的理解。
5. 实战问题排查与避坑心得
5.1 解决合并冲突的标准步骤
冲突发生后,git status会标记出both modified状态的文件。我处理冲突的标准流程如下:
1. git status 查看哪些文件冲突 2. 逐个打开冲突文件,搜索 <<<<<<< 标记 3. 看懂两边的意图,保留需要的代码,删除冲突标记 4. git add 每个已解决的文件 5. git commit 完成合并第一次处理冲突时,容易犯两个错误。一是看到冲突标记就慌乱,想着“哪个版本新就全部保留”,结果把两边的代码重复拼在一起;二是没有先理解上下文就盲目删除代码。处理冲突必须结合业务逻辑判断,不要为了“消除冲突提示”而草率解决了事。
如果冲突发生在大量文件里,而且你其实并不需要合并进来,可以随时终止合并:
git merge --abort这会回到合并前的状态,为你争取更多思考时间。我在实际工作中还会借助 IDE 的可视化 diff 工具处理冲突,左侧显示当前分支,右侧显示合并进来的分支,中间是合并结果,比直接在文本编辑器里看标记直观很多。
5.2 detached HEAD:游离头指针是怎么发生的
“游离头指针”这个术语听起来很恐怖,我第一次看到时一度以为仓库坏了。它发生的典型场景是你直接执行了git checkout 8f3a2b1,即直接切换到了一个具体的提交而不是分支名。Git 会提示你当前不处于任何分支上,这种状态就叫 detached HEAD,其实和仓库损坏无关。
在 detached HEAD 状态下做出的提交,本质上是“悬空”的,没有任何分支引用它们。如果你此时切回别的分支,新提交可能会因为失去引用而逐渐被 Git 的垃圾回收机制清除。但 Git 从 2.23 开始,在切换时如果检测到游离头指针下的未推送提交,会给出较明确的提示,不会像早期版本那样容易造成数据丢失。
如果希望在游离状态下保留当前改动,第一时间从当前位置创建一个新分支:
git switch -c rescue-branch这样新提交就有了分支引用,不会再丢失。日常开发中尽量避免直接 checkout 一个 commit 来“看代码”,改成用git switch或直接查看提交内容更安全。
这里还要提一个容易和 detached HEAD 混淆的场景:你 checkout 了远程分支。git checkout origin/feature/xxx看到的是“远程状态”的快照,也不适合在上面提交。需要基于远程分支开发时,应该在自己的本地分支上操作:
git switch feature/xxx # 如果本地已存在 git switch -c feature/xxx origin/feature/xxx # 如果本地不存在后者会自动建立本地分支与远程分支的追踪关系。
5.3 merge 和 rebase 到底应该选哪个
这是 Git 社区最容易被争论的话题之一。merge 保真历史,会保留“分叉-汇合”的真实脉络;rebase 重写历史,会让提交记录看起来像一条直线。从结果看两者都是把另一个分支的改动合并过来,但历史的组织方式不同。
rebase 的操作如下:
git switch feature/login git rebase main这条命令的效果是把feature/login分支上独有的提交“重新播放”到main分支的最新提交之上。提交内容不变,但提交的基点变了,所以提交哈希也会变。好处是后续合回 main 时历史非常干净,坏处是如果有人已经基于你本地旧的 feature 分支拉了新的分支,rebase 会让他非常痛苦。
我的取舍原则有两句话:
- 还没推送到远程的分支,想整理历史尽量用 rebase;
- 已经推送到远程、且有多人共同基于该分支开发的分支,一律用 merge 拒绝 rebase。
如果团队约定使用 rebase 工作流,推荐使用 pull 时的--rebase方式:
git pull --rebase origin main这样本地提交会被先“放到一边”,等远程更新合并后再“放回来”,不会生成多余的 merge 提交。rebase过程中也可能遇到冲突,逐文件冲突解决后执行:
git add . git rebase --continue如果发现 rebase 风险太高,也可以随时git rebase --abort回到 rebase 之前的状态。
5.4 误删、误提交、误 reset 的挽救方法
“后悔药”是 Git 最宝贵的特性之一。我到现在还依赖这么几条救命命令。
误删文件后还没提交,可以用:
git restore deleted-file.txt已经提交过、后来又删除并提交了,想恢复某个历史版本的文件:
git checkout <commit-hash> -- deleted-file.txt误执行了git reset --hard发现代码丢了,不用担心,只要提交在历史里存在过,就还能找回来。先用git reflog查看 HEAD 的移动记录:
git reflogreflog 会记录每一次 HEAD 的变动,包括 reset、checkout、merge 等。找到你执行git reset --hard之前的那个提交哈希,然后:
git reset --hard <commit-hash>或者把它作为新分支拉出来。reflog 里的记录默认保留 90 天,所以只要你发现得早,几乎都能恢复。
误把敏感文件(比如密钥)提交并推送到远程仓库,这种情况下“删除提交”是不够的,因为历史里还能翻出来。正确做法是先用工具确认哪些历史提交包含该文件,然后配合平台支持的历史清理功能处理,或者用git filter-repo重写历史。这条路径操作门槛较高,更需要的是防范意识:提交前确认.gitignore、提交前用git status观察跟踪文件、不要把密钥放进仓库目录。
5.5 日志乱码、中文文件名异常等细节问题
中文环境下的 Git 乱码,通常由两个配置引起。一个是前面提到的core.quotepath=false,解决文件名被转义成八进制的问题;另一个是提交信息中的中文乱码,可以通过调整 log 输出编码解决:
git config --global i18n.logoutputencoding utf-8 git config --global i18n.commitencoding utf-8在 Windows 的 Git Bash 上还可能出现git status输出乱码,通常是终端代码页和 UTF-8 不一致导致。Windows Terminal 或 VS Code 内置终端一般没有这个问题。
我自己曾经因为换行符问题在一周内多次提交了大量无效 diff,后来靠.gitattributes才彻底解决:
* text=auto *.js text eol=lf *.md text eol=lf *.bat text eol=crlf这套规则会告诉 Git,对于*.js、*.md这类文件统一使用 LF,对于 Windows 批处理文件使用 CRLF。团队内推这种方式,远比每个成员各自配置core.autocrlf可靠。
最后再分享一个小技巧:不要一遇到问题就想“把整个仓库删了重新克隆”。Git 的设计本身就鼓励尝试和恢复,大部分错误的成本都比你想象中低。把git reflog当作仓库的“操作时间线”,把git status当作你的“当前状态仪表盘”,基本可以覆盖绝大多数意外场景。
学 Git 的曲线确实有一点陡,但如果按“本地提交 → 分支合并 → 远程协作 → 历史改写”这条主线走,每一步都能落在实际操作上,而不是背命令。等你把某一次“救回误删分支”的经历做成案例,你就真正掌握它了。