我从第一次用Git到把它变成每天最顺手的工具,中间踩过不少莫名其妙的坑。最开始的印象就是命令行窗口里一堆看不懂的报错,什么fatal: not a git repository、failed to push some refs,甚至有一次合并分支把同事的代码弄丢了,靠reflog才救回来。这篇博文就围绕Git基本操作展开,从安装、配置、提交、分支、远端协作到常见问题排查,把每一步为什么要这么做、实际怎么操作、有哪些坑,都按我自己的经验讲一遍。适合刚接触Git的新手,也适合用了一两年但遇到问题还全靠搜索引擎的开发者。看完你能把日常80%的Git操作理顺,遇到报错也知道先去查什么。
1. 内容整体设计与思路拆解
1.1 Git到底在解决什么问题
很多人学Git之前,都经历过没有版本控制的痛苦。写毕业论文的时候文件夹里塞满论文_最终版.doc、论文_打死也不改版.doc、论文_最终版2.doc,每次改动都复制一份新文件,生怕哪次改坏了回不去。多人协作更崩溃,两个人同时改同一个文件,最后只能通过聊天工具互传“我改好了你覆盖一下”,结果覆盖掉了对方的工作成果。
Git解决的就是这个问题:它给文件的每一次变化都拍一张快照,记录改动的人、时间和内容差异,并且允许你在任意历史节点之间自由穿梭、分叉、合并。关键的是,Git的快照机制和传统的“增量备份”不一样。
Git内部用三类对象来管理数据:blob对应文件内容,tree对应目录结构,commit对应一次提交的快照。每次提交,Git把整个项目当时的状态保存下来,而不是只保存“改了哪些文件”。所以Git的提交本质上是一个链式结构,每个commit都指向父提交,形成了一个DAG(有向无环图)。这也是为什么Git切换分支、回退版本特别快——它只是移动指针,不需要对比文件算出补丁。
理解这个模型之后,你会发现很多Git命令其实是在操作三类东西:提交节点、分支指针、暂存区内容。后续所有操作,都围绕这三样展开。
1.2 为什么选择Git而不是SVN
在Git流行之前,SVN是主流。两者的核心差异在于架构:SVN是集中式版本控制,所有历史记录都存在中央服务器上,提交代码必须联网;Git是分布式版本控制,每个人本地都有一份完整的仓库历史,提交动作在本地完成,不需要连接服务器。
集中式和分布式带来的体验差别非常大。用SVN的时候,哪怕只是想看一个月前的某一行代码,只要断网或者服务器挂了,你什么都干不了。Git则完全不受影响,飞机上、地铁里都能提交、看历史、建分支——这些操作全部在本地完成,联网只是为了push和pull。
还有一个被很多人忽略的优势:分支成本。SVN创建分支本质上是把目录复制一份,成本高、合并麻烦;Git创建分支只是创建一个指针,只需要几十毫秒。这个差异决定了工作流的不同。Git能把“每修一个bug就建一个分支”变成日常习惯,因为开销实在太小了。
如果你所在的项目现在还在用SVN,迁移到Git之后最大的变化不是命令变了,而是协作模式变了:每个人都有一个完整仓库,不会因为中央服务器故障而阻塞开发,代码评审、持续集成也能更好地和分支模型配合。
1.3 学习路径怎么设计最合理
我发现初学者最容易犯的错误是拿着命令列表死记硬背,今天记git checkout -b,明天记git reset --hard,全背下来但不知道这些命令在解决什么问题,遇到报错依然不会处理。
我的建议是先建立心智模型,再按“本地操作 -> 远端协作 -> 问题排查”的顺序学习。
第一步是理解三个区域:工作区(你正在编辑的目录)、暂存区(准备提交的文件清单)、版本库(已经提交的历史快照)。git add是把工作区的改动放进暂存区,git commit是把暂存区内容固化成版本库里的一个新提交。日常95%的命令都是在调整这三个区域之间的状态。
第二步熟练本地操作:初始化仓库、提交、查看历史、建分支、合并分支、回退。这些操作不依赖任何服务器,可以随时练习,也不会给别人造成困扰。
第三步再学远端协作:clone、push、pull、解决冲突。有了本地基础,你才知道push失败时为什么要先pull,才知道origin/main这种远程跟踪分支的含义。
第四步是积累排错经验:把常见报错整理成自己的速查表,遇到问题能快速定位是配置问题、网络问题还是操作顺序问题。这部分的经验会写在后面的章节里。
2. 环境准备:安装与初始化配置
2.1 不同操作系统下的安装方式
Windows用户最省事的方式是去Git官网下载安装包,一路点Next装完。但有三个选项要特别注意,不要无脑默认。
第一个是“调整PATH环境变量”,默认选项是“Git from the command line and also from 3rd-party software”,这个保持默认就好。如果选成了“Use Git from Bash only”,你在CMD或PowerShell里敲git会找不到命令,还要手动配环境变量,纯属给自己找麻烦。
第二个是“选择默认编辑器”,默认是Vim。很多新手在commit时不小心打开了Vim,然后不知道怎么退出,卡在窗口里。建议在安装时把这改成VS Code或Notepad++,也可以在安装后执行git config --global core.editor "code --wait"来改。如果装完了才发现用的是Vim,记住:按Esc,输入:wq回车,就能保存退出。
第三个是“换行符转换方式”,这个坑很大,安装时默认选项是“Checkout Windows-style, commit Unix-style line endings”,也就是autocrlf=true。这个选项会让Git在检出文件时把LF改成CRLF,提交时再把CRLF转回LF。对纯Windows团队来说问题不大,但跨平台协作时容易造成“整个文件都被标记为修改”的假象,这个问题我在后面的排查章节专门讲。
macOS用户推荐用Homebrew安装:brew install git。这条命令会自动安装最新版并配置好PATH。Linux用户可以分不同发行版用apt、yum或者dnf安装,比如Ubuntu上是apt install git。
安装完成后验证一下:
git --version能输出版本号(比如git version 2.23.0.windows.1)就说明安装成功了。如果公司网络下载慢,可以用国内镜像站或者包管理器安装,装完务必对一下安装包的签名或SHA256,不要随便从不明来源下载可执行文件。
2.2 第一次配置必须做四件事
Git安装完成后不能直接干活,必须先做身份配置。Git每次提交都会记录“谁在什么时候改了这些内容”,如果没配置用户名和邮箱,commit会被拒绝,或者生成一串看不出作者的记录。
最基本的配置是这两条:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"关于邮箱,有个很多人不知道的点:如果是往GitHub、Gitee这样的平台推代码,提交邮箱最好和平台账号的邮箱一致,这样提交记录才能关联到你的账号,否则提交会变成“幽灵提交”——代码合进去了,但贡献者列表里找不到你,个人提交图表也不增长。
第三件事是设置默认分支名。旧版Git默认分支叫master,新版已经改成main。为了避免每台机器行为不一致,建议显式指定:
git config --global init.defaultBranch main第四件事是配置换行符处理。Windows上设置git config --global core.autocrlf true,macOS/Linux上设置git config --global core.autocrlf input。这条配置的意思是:Windows检出时把LF转成CRLF,提交时转回LF;macOS/Linux检出时保持LF。这样能最大化避免跨平台的行尾混乱。如果团队项目里已经有.gitattributes文件,那么以文件里的规则为准,命令行的global配置会被覆盖。
2.3 配置文件与查看方式
Git的配置分成三个层级:system(系统级)、global(用户级)、local(仓库级)。优先级从低到高是system < global < local,也就是说local配置能覆盖global配置,这个和覆盖机制是逐层叠加的。
实际项目里,我见过很多奇怪的问题都是配置冲突导致的。比如某个人全局配置了错误的邮箱,项目里又单独配了一个正确的邮箱,然后他提交代码时系统到底用哪个?答案是用local的。所以排查配置问题时要分别查看三层配置。
查看所有生效配置的命令:
git config --list --show-origin--show-origin会告诉你每一行配置来自哪个文件,非常实用。只想看某项配置可以这样:
git config user.name git config --global user.email我还喜欢配几个别名,能显著提升日常效率:
git config --global alias.st status git config --global alias.co checkout git config --global alias.cm commit git config --global alias.br branch git config --global alias.lg "log --oneline --graph --decorate --all"配了别名之后,git st、git cm -m "xxx"写起来顺手很多。但团队协作时别强制别人用你的别名,别名只是本地个人偏好,换一台机器就要重新配。
3. 核心实操:本地仓库的基本操作流程
3.1 从零到第一次提交的完整过程
我们先走一遍新手必做的流程:把一个普通文件夹变成Git仓库,然后提交第一个版本。
mkdir my-project cd my-project git initgit init执行成功后,会创建一个隐藏的.git目录,这里面存放着所有历史数据。很多人第一次看到.git文件夹不知道它是干嘛的,解释一下:你的项目代码文件是你的工作区,.git目录是版本数据库,二者互不干扰。如果你删掉了.git,代码文件还在,但所有历史记录全部丢失,这个操作不可逆。
如果你想从别人已经建好的仓库开始,则用clone而不是init:
git clone https://github.com/xxx/xxx.gitclone会自动完成三件事:下载历史数据、创建当前分支的工作文件、配置远程跟踪分支origin/main。注意clone之后不需要手动git init,再去init反而会破坏原有配置。
初始化之后,随便创建几个文件,然后查看仓库状态:
git status此时文件处于“未跟踪”状态。Git现在知道这个文件存在,但还没有把它纳入版本管理。要让文件进入暂存区:
git add README.md git add src/ # 添加整个目录 git add . # 添加当前目录所有改动git add .是新手最爱的命令,但注意它会把当前目录下所有改动(包括变动的、新增的、删除的)全部加入暂存区。如果项目里刚好有一个不想提交的临时文件,就容易被误加。所以更稳妥的做法是git add明确指定的文件或目录,或者先通过.gitignore把不相关的文件排除掉。
暂存完成后要提交:
git commit -m "feat: 初始化项目,添加项目说明"-m是简短提交信息。如果不带-m,Git会打开默认编辑器让你填写多行提交信息。提交成功后会输出一行摘要,包含分支名、提交哈希、变更的文件数。
第一次提交做完之后,你对Git的信任感就建立起来了。后续每次改动都是同一个节奏:改文件 ->git add->git commit。哪怕改动只有一行,也要提交,这是Git使用中最重要的习惯。
3.2 补漏神器:git commit --amend怎么用
有一次我提交完代码,发现少加了一个文件。当时想的是“再提交一次不就行了”,结果留下了两条提交记录,第二条还写着“fix typo”。这样的记录在别人看历史时是噪音。其实Git提供了一个专门解决这类问题的命令:git commit --amend。
amend的作用是“修改最近一次提交”。它不会生成新提交,而是把当前暂存区里的内容合并进上一个提交,替换掉旧的提交对象。
使用场景主要有三个:
第一,漏提交了文件。比如你刚提交了index.html,但忘记提交style.css,现在补上:
git add style.css git commit --amend -m "feat: 添加首页样式"注意,-m里最好带一份完整、正确的提交信息,因为amend之后旧信息会被替换;如果你只是想改信息没改代码,可以用git commit --amend -m "新的提交信息"。
第二,提交信息写错了字。比如把“feat”写成了“fat”,改一下就行。
第三,把几次小提交合并成一次。比如你写功能时做了三次提交:“改了一部分”、“改完功能”、“补一个忘了的变量”,在推送到远端之前可以用amend合并:
git commit --amend --no-edit--no-edit表示沿用上一条提交信息,这样修改不会打开编辑器。
但这里有两条铁律必须记住。第一条:amend只能用于还没有推送到远端的提交。如果你已经git push过了,远端已有旧提交,此时git commit --amend会生成一个不同的哈希,强行push会被拒绝,强制push则会把同事基于旧提交的改动全部打乱。第二条:amend本质上是“删除旧提交、创建新提交”,你以为只改了提交信息,其实哈希、时间戳全变了。万一搞坏了,用git reflog能找到旧提交并恢复,这一点在第5章排查部分展开。
3.3 分支操作:创建、切换、合并与冲突处理
分支是Git最强大的能力,也是一堆报错的来源。前面说了,Git分支本质上只是指向某个提交对象的指针。创建分支就是复制一个指针,几乎零成本。
创建并切换分支的常用命令:
git branch feature/login # 创建分支但不切换 git checkout -b feature/login # 创建并切换(旧版) git switch -c feature/login # 创建并切换(新版推荐)现在Git官方推荐用switch和restore替代容易混淆的checkout。checkout这个命令有太多含义,新手常常搞混:git checkout feature是切分支,git checkout -- file是丢弃文件修改,git checkout -b是建分支。语义不清就会用错。新命令把职责拆开了:switch只管分支,restore只管文件恢复。
切换分支后,工作区的文件会跟着变化,这就是很多人第一次用Git时被吓到的地方:切到另一个分支,为什么文件不见了?其实文件没丢,只是每个分支的快照内容不同。Git在你切换分支时,会用目标分支的最新快照去更新工作目录。
合并分支有两种常见情形。第一种是快进合并(fast-forward),当目标分支领先于当前分支、且没有分叉时,Git直接把当前分支的指针移动到目标分支指向的节点上,不产生新的合并提交。第二种是三方合并,当两个分支都有各自的提交时,Git需要生成一个合并提交,把两条线焊在一起。
合并命令:
git switch main git merge feature/login如果合并时两个分支改了同一个文件的同一行,Git无法自动判断保留谁的版本,就会报冲突。冲突文件会被打上标记,里面是<<<<<<<开头的当前分支内容和>>>>>>>开头的另一分支内容。正确做法是手动编辑文件,把不需要的标记和内容删掉,保存后:
git add 冲突文件 git commit -m "merge: 合并feature/login分支"初学者遇到冲突容易慌,其实只要记住一个原则:冲突不是错误,它只是一个“需要人类做决策”的信号。Git已经把所有能自动合并的都合好了,剩下需要你用编辑器打开文件,看看保留哪边、调整成什么样子,最后add和commit就行。
还有一个实用技巧:合并大改动之前,先看一眼两个分支差了多少:
git log --oneline main..feature/login这能看到feature分支上但main上没有的提交。如果差了几十个提交,合并时冲突概率会很大,建议先清理代码、拆分成小合并,或者先跑一次git merge --no-commit做预演,确认没问题再正式合并。
4. 远端协作:clone、push、pull与认证
4.1 添加远程仓库并理解origin
本地仓库和远端仓库通过remote关联。remote是一个别名,指向远端仓库的URL。
查看当前关联的远端:
git remote -vclone下来的仓库会自动有一条origin,指向你clone的地址。如果是自己先建的本地仓库,再想推到远端,需要手动添加:
git remote add origin https://github.com/yourname/repo.git我见过不少人把git remote add origin和git remote set-url origin搞混。前者是首次添加,后者是修改已有地址。还有一种情况是本地仓库已经关联了一个远端的仓库A,但他想跟仓库B关联,又懒得删除旧配置,直接再add origin,结果报错error: remote origin already exists。正确做法是先删再添:
git remote remove origin git remote add origin https://github.com/yourname/repo.git这里顺便讲一下origin/main这种奇特的名字。它叫远程跟踪分支,是你“上一次和远端通信时,远端main分支的指向”。git fetch或者git pull后,origin/main会更新。本地的main是你在当前机器上的分支,origin/main是远端状态的本地镜像,两者可以短暂不一致——这正是分布式版本控制的正常状态。
4.2 SSH密钥配置与免密原理
连接远端有两种常用协议:HTTPS和SSH。HTTPS每次push都要输入用户名密码(除非配置了凭据缓存),SSH则通过密钥对免密认证。
SSH配置步骤:
ssh-keygen -t ed25519 -C "你的邮箱"一路回车,会生成一对密钥:私钥在~/.ssh/id_ed25519,公钥在~/.ssh/id_ed25519.pub。私钥留在本地,公钥贴到代码托管平台的SSH Keys设置里,然后用这条命令测试:
ssh -T git@github.com如果配置成功,会返回类似Hi yourname! You've successfully authenticated的信息。注意这里用的用户名是git,和你的Git账号名无关,这是SSH协议层面的固定账号。
常见误区有两个。第一个是把私钥内容贴到了平台上,私钥一旦泄露等同于密码泄露,必须立刻删除重新生成。第二个是平台返回“Key already in use”,说明你之前已经把这个公钥添加到另一个账号了——一个公钥只能关联一个账号,想多账号使用就得生成多把密钥并通过~/.ssh/config配置。
需要多账号协作时,可以参考这样的~/.ssh/config配置片段:
Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work Host gitee-personal HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_personal配置后clone地址要改成对应别名,比如git clone git@github-work:yourname/repo.git。这种按Host区分密钥的方式比较清晰,相比每次切换账号都要改全局配置要省心得多。
4.3 push、pull、fetch:别盲目pull
很多人在远端协作时,指令就是“先pull再push,冲突就搜谷歌”。其实把这三个动作搞清楚,很多问题都能避免。
git fetch只做一件事:把远端的最新提交下载到本地,并更新origin/main等远程跟踪分支。它不会动你的工作区,也不会产生合并。
git pull等于fetch + merge,它把远端提交拉下来,并自动合并到当前分支。
git push是把本地提交推送到远端分支。
推荐流程是先把fetch和merge分开看。遇到大改动或者不确定远端有没有新提交时,别直接pull,而是:
git fetch origin git log --oneline main..origin/main这能看到远端有而本地没有的提交。看一眼货物内容再决定怎么合并,比闭着眼睛pull出冲突之后哭要强。
push被拒绝是最常见的远端问题,报错一般是:
! [rejected] main -> main (fetch first) error: failed to push some refs原因很简单:远端main分支上有你在本地没有的提交,Git不敢直接覆盖别人的历史。这时候你有两条路。第一条是git pull --rebase,把本地提交“垫”到远端提交之上,历史是一条直线;第二条是git pull --no-rebase,生成一个合并提交把两条线合并。
团队协作时建议统一策略。我所在的项目组约定主干分支用rebase保持整洁,功能分支随便merge。如果你不确定团队约定,先问清楚再动手,这种操作会改写提交历史,推上去之后很难收拾。
push时第一次需要设置上游分支:
git push -u origin feature/login-u的意思是把本地feature/login和远端origin/feature/login关联起来,之后这条分支上直接git push、git pull就不需要再带参数了。
4.4 清除账号密码与其他认证问题
HTTPS配合缓存在日常使用中最常遇到的麻烦是“密码存错了或者想换账号”。比如你之前用A账号push,现在公司换了B账号,Git凭据管理器里存的还是A的,push时提示认证失败或者一直用旧账号。
Windows凭据管理器存储的Git账号,可以通过“控制面板 -> 用户账户 -> 凭据管理器 -> Windows凭据”里删除对应的git:https://github.com条目,也可以敲命令:
git credential-manager reject https://github.commacOS用户可以在“钥匙串访问”里搜索github.com删除对应条目。
如果连缓存都不想要了,可以显式关闭凭据存储:
git config --global --unset credential.helper还有一个经常被忽略的情况:网上教程让用git config --global credential.helper store来免密,这个方案虽然简单,但会把用户名密码明文存在~/.git-credentials文件里,安全性很差。如果你用的是自己的电脑,建议用系统自带的凭据管理器;如果用共享机器,干脆就别缓存,每次输入密码也花不了几秒钟。
认证失败的另一个常见原因是SSH端口被网络环境拦截,导致ssh: connect to host github.com port 22: Connection timed out。这种情况可以考虑改用HTTPS协议clone,或者在~/.ssh/config里为对应Host配置443端口:
Host github.com HostName ssh.github.com Port 443 User git这是我处理过多次的真实案例:一台Windows机器在公司网络下SSH连不上,配置完443端口后恢复。如果港口的网络策略禁止,那就只能用HTTPS了。
4.5 git worktree:一个处理多个分支的隐藏利器
搜“git worktree”的人通常是在一个仓库里遇到“想切分支但手头改动还没提交完”、“想同时在两个分支上干活”的痛点。
worktree允许你在同一个仓库下创建多个工作目录,每个目录对应不同分支,并且互不干扰。比如你正在feature分支开发,突然需要修复线上main分支的紧急bug,传统做法是stash、切换分支、修复、再切回来,手忙脚乱。用worktree可以直接开一个新的目录:
git worktree add ../hotfix -b fix/urgent main这样项目的../hotfix目录就对应着fix/urgent分支,你可以在这个目录里改代码、提交,另一个目录的工作完全不受影响。查看、清理工作树:
git worktree list git worktree remove ../hotfixworktree还有一个好处是并行构建验证。比如两个分支改了不同模块,可以各开一个工作目录同时编译。不过要注意,同一时间同一个分支只能在一个工作目录里检出,因为Git不允许两个工作树操作同一个分支,这是设计上的保护,防止两份工作区把分支状态搞乱。
5. 常见问题与排查技巧实录
5.1 常见报错速查表
这些年我整理了一份高频报错对照表,每次遇到都能快速定位,这里分享出来。
| 报错信息 | 触发原因 | 解决思路 |
|---|---|---|
fatal: not a git repository (or any of the parent directories): .git | 当前目录不是Git仓库,或父目录都不在仓库内 | 确认是否执行过git init;用git rev-parse --show-toplevel查看仓库根目录 |
fatal: refusing to merge unrelated histories | 两个仓库没有共同祖先(比如两个独立init的仓库) | 确认确实要合并后,使用git merge --allow-unrelated-histories |
error: failed to push some refs | 远端有本地没有的提交 | 先git fetch,然后pull或rebase再push |
Please commit your changes or stash them before you merge | 切换分支/合并时工作区有未提交改动且和目标分支冲突 | 先commit,或者git stash暂存,操作完再git stash pop |
ssh: connect to host github.com port 22: Connection timed out | SSH端口被网络拦截 | 改用HTTPS或配置SSH 443端口 |
warning: LF will be replaced by CRLF | 换行符转换规则触发 | 按团队规范统一配置core.autocrlf或.gitattributes |
这个表格不是让你背,而是遇到问题先确认自己在哪一层:是“当前目录没有仓库”(初始化问题)、还是“合并历史冲突”(数据模型问题)、还是“网络认证失败”(环境问题),不同层的排查手段完全不同。
5.2 提交发错分支怎么办
这个问题每个新手都碰到过:在main分支上改了一堆代码,其实应该在feature分支上开发。提交已经做了,怎么“搬”过去?
正确步骤是:
# 1. 记录当前提交到临时分支 git branch temp # 2. 把main分支回退到上一个提交 git reset --soft HEAD~1 # 3. 切到真正要提交的分支 git switch feature/login # 4. 把改动提交过来 git commit -m "feat: 正确的提交说明"这里的关键是--soft。git reset --soft HEAD~1的含义是:把main分支的指针回退一个提交,但保留所有改动到暂存区。也就是说,代码、暂存状态都还在,只是提交节点撤销了。对比一下:--mixed(默认)会回退指针并保留工作区改动但清空暂存区;--hard会回退指针并彻底丢弃改动,这个命令极其危险,执行前一定要确认你不需要那些代码。
还有一种是提交已经push到了远端,再接一条命令把远端也修正:
git push --force-with-lease origin main--force-with-lease比--force安全得多,它在强推前会检查远端是否还是你上次看到的状态。如果同事已经往这条分支上推了新提交,强推会被拒绝,防止你覆盖别人的工作。但即便如此,改动已经推送的公共分支前一定要和团队沟通,这不是命令层面的问题,而是协作礼仪问题。
5.3 amend、reset误操作后的恢复
Git并不是一个“删除了就完蛋”的工具,它设计了非常强大的安全网:reflog。reflog记录了你在本地仓库中每一次HEAD移动的历史,包括commit、reset、checkout、merge。即使你用git reset --hard丢弃了提交,reflog里依然能找到那个提交的哈希。
先看reflog:
git reflog输出大概是这样的形式:
abc1234 HEAD@{0}: commit: feat: 登录模块 def5678 HEAD@{1}: reset: moving to HEAD~1比如你不小心执行了git reset --hard HEAD~1,把最新提交丢掉了。reflog里能看到上一个commit的哈希,这时可以恢复:
git reset --hard abc1234或者用git cherry-pick把那个提交捡回来:
git cherry-pick abc1234reflog也有过期清理机制,一般默认保有90天左右的操作记录。所以遇到误删操作,第一反应不要是“完蛋了”,而是git reflog先把现场找回来。这个命令我救过自己很多次,有一次分支误删,就是靠reflog里的commit哈希重建的:
git branch recover-branch abc1234我强烈建议任何一个用Git的人,在理解“分支=指针”“提交不可变”的基础上,把reflog当成最后一层保险,它可以让你在Git里的大部分误操作都有后悔药吃。
5.4 添加了.gitignore但文件还在被跟踪
很多人往.gitignore里加了一堆路径,却发现文件仍然出现在git status里。原因是:.gitignore只对未被跟踪的文件生效,如果一个文件已经被git add过(被跟踪了),它就不会再被ignore规则排除。
解决办法是把文件从跟踪列表移除,但不删除物理文件:
git rm --cached target/config.yml然后提交这次移除。之后再把这个文件加进.gitignore,以后就不会再被跟踪了。这个场景在数据库配置、本地环境变量文件上最常见。比如项目有一个config/database.yml,里面存着本地数据库连接信息,团队不同成员的内容各不相同,就不应该提交到仓库,正确做法是提交一份config/database.example.yml作为模板,把实际的数据库配置文件ignore掉。
5.5 合并冲突处理的经验
合并冲突是Git用户绕不过去的坎,但处理冲突有一些经验能大幅降低痛苦。
第一条经验是合并越频繁,冲突越少。很多冲突都是因为分支活了太久、偏离主干太远才爆发的。一个feature分支每完成一个小功能就merge一次主干,或者用git merge main把主干的最新代码定期合回feature分支,让两边始终保持在较近的距离上。这样每次需要手动解决冲突的范围都很小,定位也容易。
第二条经验是处理冲突时先看清楚双方意图,再动手。冲突标记<<<<<<<和>>>>>>>之间分别来自两个分支,但文件里可能有好几处冲突,不要只改一处就提交。建议全局搜一遍冲突标记:
git grep -n "<<<<<<< HEAD"逐个处理完之后,用git diff --check确认没有残留冲突标记和空白错误,再git add和commit。
第三条经验是不要让Git自动合并二进制文件。图片、压缩包、二进制依赖这类文件一旦双方都改了,Git无法合并,只能以一方为准。如果你的项目里这类文件经常变动,建议考虑用Git LFS统一管理,避免反复出现无法合并的问题。我自己就遇到过设计稿图片冲突,最后只能找设计同事重新导出,白白花了半小时沟通。
第四条经验是用stash管理临时修改:
git stash push -m "暂存当前改动" git stash list git stash popstash会把当前工作区未提交的改动打包暂存,让你能在干净状态下切分支、合并,处理完再恢复。注意pop时如果和目标文件有冲突,同样要手动解决。多分支并行时stash是保命技能,比如紧急修复线上bug,又不想丢掉正在写的新功能,可以先stash、切分支、修完、切回来、pop。
5.6 Windows环境特有的坑
Windows上是Git问题最多的平台,主要围绕换行符和文件系统。
换行符问题前面提过,表现症状是:只是打开文件保存了一下,整个文件在diff里显示全变了。因为编辑器把LF换成了CRLF,而Git在autocrlf=false的配置下把CRLF当成差异。解决方法是统一提交规范:仓库里所有文本文件统一使用LF,通过.gitattributes显式声明:
* text=auto *.js text eol=lf *.json text eol=lf *.md text eol=lf这样不管哪个平台、什么编辑器,检出文件都用LF,提交不产生无意义的换行差异。
Windows文件系统的另一个特点是大小写不敏感。如果你在一个仓库里创建了README.md,过两天又改成readme.md,在Windows上Git可能不会识别为文件重命名,导致提交时出现两个文件或者改错对象。Git提供了一个配置项core.ignorecase,Windows上默认是true,如果你确实需要严格区分大小写,可以设成false,但更推荐的做法是保持团队统一,命名时一开始就不要用容易混淆的大小写组合。
5.7 回退与恢复:reset和revert怎么选
“代码改坏了要回退”是高频需求,但很多人分不清git reset和git revert。
git reset是移动分支指针,相当于“让历史从某个点重新开始”。它改写历史,适合还没推送的提交。git revert是创建一个新提交,这个新提交的内容是撤销指定提交的改动,但历史里保留原提交的记录。它不改写历史,适合已经推送到远端的提交——因为推送过的提交被改写会直接坑队友。
例子:你想撤销main分支上一次提交。
如果用revert:
git revert HEADGit会打开编辑器让你写提交信息,默认是Revert "feat: xxx"。这条命令生成的新提交会包含反向修改,main的历史上会看到两条记录:原来的提交和撤销它的提交。
如果提交还没推送,用reset更干净:
git reset --hard HEAD~1但--hard会把工作区文件也恢复成旧状态,如果之前有未提交改动会全部丢失。所以如果你只想撤销提交但保留改动,用--soft或--mixed。
我的习惯是:区分使用场景,绝不在推送过的公共分支上用reset,这是团队协作的底线。日常个人开发分支可以随意reset,一旦进了集体仓库的main分支,一律用revert或者先跟团队确认。
6. 写在最后的个人经验
最后分享一个不算技巧但非常重要的习惯:提交信息要写清楚。我见过太多update、fix、修改这样的提交信息,三个月后回看历史,完全不知道当时干了什么。我自己的提交信息风格是类型(范围): 简短描述,比如fix(auth): 修复token过期未跳转登录页、feat(api): 添加用户列表分页参数。这种习惯在协作时特别值钱——定位问题、做code review、回退版本,第一件事都是看提交信息。
还有一个习惯是“小步提交”。别攒了一堆改动才commit一次,改一个功能、修一个bug,就单独提交。每次提交只包含一个逻辑单元,将来出问题定位会非常快。攒几天再提交的代价是:一旦中间有一步写错了,要么回退一大片,要么在reflog里翻半天。
Git这门工具不难,难的是把“频繁提交、清晰记录、及时同步、谨慎改写”这些习惯真正落实到日常。踩过几次坑之后,你会发现它带来的安全感远超学习成本。如果这篇文章能帮你少踩几个坑,那很好。