用了这么多年Git,我发现一个现象:办公室里干活最利索的同事,往往只用那二十来个命令就把需求安排得明明白白;而每次遇到合并冲突就抓瞎的人,手头却存着上百条冷门命令的速查表。Git常用命令这个事,从来不是比谁背得多,而是比谁把核心工作流跑得顺。
这篇文章我想把日常开发里真正高频使用的Git命令按真实场景串一遍——不是从一个命令跳到另一个命令的命令字典,而是沿着“初始化→提交→分支→远程→改写→救火”这条实际工作路径,讲讲每个命令出现的时机和背后的机制,顺手把我踩过的坑也一并交代了。适合刚接触Git没几天的新手,也适合长期用GUI工具、想转命令行的同学。读完你不需要记住所有命令,只需要理解这条链路,其他的随时查就行。
1. 为什么我还在用命令行使唤Git
有人可能会问:现在SourceTree、VS Code、IDEA自带的Git面板都做得挺好了,为什么还要折腾命令行?我的回答很直接:命令行是唯一能让你真正看懂Git在干什么的方式。
1.1 GUI工具说不清楚的那些事
GUI的问题不在于不好用,而在于它把每一步都封装成了按钮。点击“Commit”按钮,代码提交了;点击“Push”按钮,代码上去了。听起来没问题,可一旦出现冲突、提交错文件、想撤销某次操作,GUI只会弹出一个确认框,你根本不知道背后发生了什么,也不知道还有什么补救手段。
命令行则强迫你看清楚每一次操作的对象和位置。git status告诉你工作区什么状态,git log告诉你有几次提交,git diff告诉你改了什么。每敲一条命令,你都在和仓库的实际状态对话,而不是隔着GUI点来点去。我见过太多人用GUI推错分支、把代码commit到错误仓库,就是因为界面上的分支切换太隐蔽了。
1.2 命令行的真正优势:可复现、可脚本化
命令行第二个优势是能写成脚本。举个真实场景:我有个项目发版前要把版本号从1.2.3改成1.3.0,然后打tag、推远程、触发CI。这一串操作如果全用GUI点,每一步都可能手滑;如果用命令写成一个shell脚本,每次发版执行一次即可。
还有一个常被忽略的好处:命令行输出的信息非常透明。git push被拒绝时它会直接告诉你“non-fast-forward”,然后提示你可以用git pull先合并。这种明确的反馈在图形界面里经常被简化成一句“提交失败”,至于为什么失败、怎么解决,全要靠猜。
所以我的建议是:GUI可以用来查看历史、快速对比文件,但所有会改变仓库状态的操作,尽量养成用命令行的习惯。这也是为什么下面整篇文章都围绕命令展开,而不是某个IDE的操作演示。
2. 环境准备:从安装到第一次提交
工欲善其事,必先利其器。想跑通Git命令,先把环境和基础配置弄明白,这一步卡住的人其实不少。
2.1 不同系统的安装方式一览
| 操作系统 | 安装方式 | 备注 |
|---|---|---|
| Windows | 下载Git官方安装包,一路Next即可 | 安装时建议选择“Git from the command line”,这样Git bash和CMD都能直接调用git |
| macOS | brew install git | 直接用Homebrew装最方便 |
| Ubuntu/Debian | sudo apt install git | 优先用系统源的版本,稳定优先 |
| CentOS/RHEL | sudo yum install git | 老系统注意源里的版本可能偏旧 |
Windows上装Git时,我强烈建议顺带把Git Bash一起装上。它不只给一个Linux风格的终端,还自带ssh、vim等工具,省去很多额外配置。很多人第一次接触Linux常用命令就是靠Git Bash学起来的——ls、cd、mkdir、grep这些在Git Bash里都能直接跑,对Windows用户来说是一扇通向命令行世界的大门。
2.2 全局配置:git config那些必填项
装完Git第一件事不是clone仓库,而是配置身份信息。没有这一步,你连commit都提交不了:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"这里有个细节需要注意:邮箱最好和你常用的代码托管平台账号保持一致,否则提交记录里的头像和用户名串不起来。我见过有人公司邮箱和个人邮箱混着提交,到年底看贡献统计时数据看着很乱。
除了身份信息,还有两个配置我建议顺手加上:
git config --global core.autocrlf input git config --global core.quotepath falsecore.autocrlf解决的是Windows和Linux之间换行符差异问题。设成input的意思是:提交时把CRLF转成LF,拉取时不强制转换,这样能避免因为换行符导致整个文件显示被改动。core.quotepath false则是让中文文件名正常显示,而不是显示成\346\265\213\350\257\225那串转义数字,对国内开发者来说几乎是必选项。
2.3 第一次init与commit
进入一个新项目目录,敲下面三条命令完成初始化:
git init git add . git commit -m "Initial commit"git init会在当前目录里生成一个隐藏的.git文件夹,这个文件夹里装着的就是整个仓库的全部历史。你不小心删掉它,Git会瞬间失忆,所以千万别去动它。
如果你只是在已有仓库里干活,则不需要git init,而是用git clone把远程仓库拉下来。克隆和初始化的区别这里先提一嘴:前者是复制一个已经存在的仓库,后者是在空白目录里新建一个仓库。
2.4 我第一次用Git时的翻车现场
我头一回用Git是在Windows上,直接双击Git Bash,然后敲git commit,结果报了一串英文错误说我连姓名都没设置。当时完全不知道要配git config,跑去搜索引擎搜了半天才弄明白。后来带新人我就学乖了,先让他们把git config --list跑一遍,确认配置没问题再开始干活。
3. 日常工作流:status、add、commit、log、diff组合
这一套命令占据了日常Git操作至少八成,也是理解Git一切机制的地基。
3.1 status:每天开工的第一条命令
git status这条命令没有任何副作用,只是把当前仓库的状态列出来:哪些文件被修改了、哪些文件是新加的、当前在哪个分支上、和远程分支比是领先还是落后。我自己的习惯是每次准备提交前一定会跑一次git status,确认自己到底要提交哪些东西。
刚用Git的人最容易犯的错是git add .一把梭,把所有文件都塞进暂存区。等提交完才发现里面混进来一个.env环境变量文件,密钥已经推到仓库里了。后来我给自己定了个规矩:git add之前必须先看status,想清楚再动手。
3.2 add:理解暂存区这个概念
git add 文件名 # 添加指定文件 git add . # 添加当前目录所有改动 git add -u # 只添加已跟踪文件的修改和删除 git add -p # 交互式选择要暂存的代码块为什么要设计一个“暂存区”?因为Git想让你把提交拆成逻辑清晰的小块。你可以上午改了两个功能,下午改了一个Bug,通过add选择性地把Bug相关的文件先提交,再提交两个功能。这相当于让你在写提交信息之前,先整理一遍到底要记录哪些变更。
[ 工作区(working directory)\xrightarrow{git add}暂存区(staging area)\xrightarrow{git commit}本地仓库(local repository) ]
3.3 commit:写好message也是命令的一部分
git commit -m "feat: 添加用户注册功能"提交信息不是写给自己看的,是写给未来的自己和团队看的。三个月后再回来看这条提交,如果信息只写着“fix”,你根本不知道修了什么。我习惯用一套简单的约定:
feat:新功能fix:修Bugdocs:文档变更refactor:重构(不改变行为)test:测试相关
git commit --amend命令在这条分支上还有一个特殊用途:如果你刚才提交的message写错了、或者漏提交了一个文件,可以在下一次提交前“修补”这次提交:
git add 漏掉的文件 git commit --amend这句话的含义是:把当前暂存区的改动合并到上一次提交里,而不是生成一个新提交。执行后会打开编辑器让你修改message,也可以直接加参数:
git commit --amend -m "修正后的提交信息"需要注意,amend会改变提交的哈希值。如果这个提交已经推送到了远程、而且别人已经基于它做了新提交,那就不要amend了,否则会把团队历史搅成一锅粥。只有在提交还没推送、或者你确定只有自己在用这条分支时,amend才是安全的。
3.4 log和diff:复盘与审查
git log --oneline --graph --decorate -20这是我最常用的log命令:--oneline压缩成一行显示,--graph用字符画出分支图,--decorate标记出tag和分支名,-20只看最近20条提交。打开以后整个仓库的历史脉络一目了然。
git diff负责看具体改动内容:
git diff # 工作区对比暂存区 git diff --staged # 暂存区对比本地仓库(也就是即将提交的内容) git diff <commit1> <commit2> # 对比任意两次提交git diff --staged在提交前的审查价值极高。Commit前跑一条,你会看到Git到底准备记录哪些改动,能拦截掉八成“提交了不该提交文件”的事故。
4. 分支操作:创建、合并与冲突处理
分支是Git用来并行开发的核心能力,但很多人用着用着就撞锁了,主要原因是没有理解分支的本质。
4.1 分支的本质:一个会移动的指针
在Git里,分支并不是一个文件夹,而是一个指向某个提交的指针。你执行git branch dev,其实就是创建了一个叫dev的指针,指向当前HEAD所在的提交。
[ \text{分支指针} \rightarrow \text{commit对象} \rightarrow \text{父提交} \rightarrow \cdots \rightarrow \text{初始提交} ]
理解了指针模型,很多问题就迎刃而解:
- 新分支和原分支“共享历史”是很正常的事,因为指针本来就指向同一个提交。
- 切换分支只是改变HEAD指向的指针,工作区文件会跟着切换,所以Git才会要求你在切换前把工作区清理干净。
- 合并分支本质上是把某条分支上的提交“接”到另一条分支上。
4.2 高频分支命令速查
git branch # 查看本地分支 git branch -a # 查看所有分支,包括远程分支 git checkout -b feature/login # 创建并切换分支 git switch -c feature/login # 新版Git的推荐写法 git merge feature/login # 把feature/login合并到当前分支 git branch -d feature/login # 删除已合并的分支这里我要特别提一下git switch。Git 2.23之后官方把checkout拆成了switch和restore两个命令,switch专门负责分支切换,restore负责文件恢复。对新手来说,这个拆分能大大降低困惑度:想换分支就用switch,想还原文件就用restore,不用再纠结checkout那一个命令怎么变出多种行为。
4.3 合并冲突的完整排查链路
合并冲突是Git新手最害怕的事,但其实只要走一遍完整排查链路,解决起来非常机械。假设我在main分支要合并feature/login分支:
git checkout main git merge feature/login如果两边都改了同一个文件的同一行,Git会输出CONFLICT并告诉你哪个文件冲突。这时候很多人第一反应是慌,其实只需要三步:
第一步,查看冲突文件和冲突区域:
git status冲突文件会被标记为both modified。打开这个文件,你会看到这样的标记:
<<<<<<< HEAD 你当前分支的代码 ======= 正在合并进来的分支的代码 >>>>>>> feature/login**第二步,手动解决每一处冲突。**保留你想要的内容,把<<<<<<<、=======、>>>>>>>这些标记行删掉。如果两边都想要,就把两者拼起来;如果要结合,就得自己写逻辑。这一步没有自动化手段,必须人来判断。
第三步,标记为已解决并提交:
git add 冲突文件 git commit -m "merge: 合并feature/login"有些人解决完冲突后只add就以为完事了,结果分支一直停在MERGING状态,下次操作出现各种莫名其妙的现象。记住,合并冲突解决完必须有一次commit,这次commit就是“合并提交”,它把两条分支的历史正式缝合在一起。
4.4 一个小技巧:合并前先看分叉情况
执行merge之前,我先跑一条:
git log --oneline --graph --left-right --cherry-pick HEAD...feature/login这能直接看出两个分支之间有多少提交、哪些已经在对方分支上存在。如果两边其实没什么分叉,Git走的是「快进合并」(fast-forward),连冲突都不会有。先看清楚再合并,比闷头git merge然后被冲突淹没舒服得多。
5. 远程协作:clone、push、pull与SSH配置
5.1 remote概念与clone
git clone git@gitee.com:你的用户名/项目名.gitgit clone做了三件事:把远程仓库完整下载到本地、把远程地址记成origin、自动建一个本地分支跟踪远程分支。执行完之后你什么都不用配置就能直接开发。
如果你的项目在Gitee上,我建议优先用SSH而不是HTTPS。HTTPS每次push都要输账号密码,SSH配置一次就能一直免密。配置SSH密钥的流程如下:
ssh-keygen -t ed25519 -C "你的邮箱"一路回车会在~/.ssh/下生成id_ed25519(私钥)和id_ed25519.pub(公钥)。公钥可以随便给别人看,私钥绝对不要泄露。然后执行:
cat ~/.ssh/id_ed25519.pub把输出的公钥内容复制到Gitee个人设置里的“SSH公钥”页面保存。之后测试连通性:
ssh -T git@gitee.com如果看到欢迎信息就说明连上了。这里有个易踩的坑:Windows用户用Git Bash执行ssh-keygen时可能遇到权限问题,检查一下~/.ssh目录的所有者和权限,私钥文件权限如果是-rw-r--r--,SSH会认为不安全并拒绝使用。
5.2 push与pull:同步的本质
git push origin main git pull origin mainpush是把本地提交推送到远程,pull是把远程提交拉下来并合并。日常开发里标准姿势是:开发前先git pull同步最新代码,开发完git commit,再git push。
很多人会遇到一个令人费解的报错:
git push被拒绝:! [rejected] 因为远程有本地没有的提交这不是什么大问题,本质是远程分支比本地先进,Git不允许你直接覆盖。解决方式也简单:
git pull origin main # 解决可能的冲突,或者让Git自动完成合并 git push origin main另一种思路是git pull --rebase,它会把你本地的提交先“摘下来”,拉到远程最新提交之后再把你自己的提交“放”到最上面,形成一条线性历史。关于rebase和merge的争论很多,我的建议是:如果你能理解rebase是“重放提交”,那就用;如果还不太理解,用普通的pull更稳妥。等对Git有感觉了再切换也不迟。
5.3 fetch与pull的区别
git fetch originfetch和pull最大的区别是:fetch只是把远程的提交下载到本地,但不合并到当前分支;pull则是fetch加merge两步一起做。
有时候我不想立刻合并,只想看看远程改了什么,就先fetch,再用git log HEAD..origin/main对比差别。确认没有可疑提交之后才merge。对于多人协作的大型仓库,这个习惯能避免很多莫名冲突。
5.4 远程分支的追踪关系
git clone之后,本地main会自动跟踪origin/main,所以直接git pull/git push即可。但是你自己创建的新分支,第一次push时要用:
git push -u origin feature/login-u的作用是建立本地分支和远程分支的追踪关系。之后在这个分支上再push/pull就不用带远端参数了。如果漏了-u,每次都要写全git push origin feature/login,也不是不行,就是很烦。
6. 历史改写:amend、reset、rebase与reflog
Git最强大的地方是历史可以修改,最危险的地方也是历史可以修改。这一节讲讲常用的“后悔药”和它们的适用范围。
6.1 reset:回退提交的三种模式
git reset --soft HEAD~1 git reset --mixed HEAD~1 git reset --hard HEAD~1三种模式的差异用一张表说清楚:
| 模式 | HEAD指向 | 暂存区 | 工作区 | 适用场景 |
|---|---|---|---|---|
--soft | 回退 | 保留 | 保留 | 想重新整理提交信息 |
--mixed | 回退 | 清空 | 保留 | 想重新add后分多次提交 |
--hard | 回退 | 清空 | 清空 | 想彻底丢弃改动 |
git reset --hard会直接删除工作区所有未提交的改动,而且很难找回。我在带新人时反复强调:使用--hard之前必须先确认工作区的改动是否已经备份或推送到远程,否则一把下去,一上午白干。
6.2 reflog:Git的“后悔药”保险柜
git reset --hard之后你以为改动没了,其实Git还有一个隐藏机制叫reflog,它会记录所有分支指针的移动历史,包括被reset掉的提交:
git reflog输出里能看到每一行:HEAD@{0}: reset: moving to HEAD~1,后面跟着一堆哈希值。假如你不小心reset --hard删了三次提交,只要在上面的记录里找到那个“删之前”的哈希,然后:
git reset --hard 恢复目标哈希就能把分支指针拨回误删之前的状态。reflog只在本地有效,而且有90天的过期时间,但它是本地恢复操作里最可靠的一道保险。
6.3 rebase:整理一串提交
git rebase -i HEAD~3这条命令会打开一个交互式编辑界面,让你对最近3次提交做处理。常见操作有:pick保留、squash把多个提交合并成一个、reword修改提交信息、drop删除提交。
我建议一个功能一个提交,不要在代码里出现“fix2”“fix3”“fix4”这种噪音提交。开发周期结束前用git rebase -i把这几条合并成一条干净的提交,配合上一条补丁,历史看起来会很舒服。
6.4 绝对不要改写的场景
所有修改历史提交哈希的命令(amend、rebase、reset --hard之后的push --force),都不要对已经推送到远程、并且别人可能已经拉取过的分支使用。改写共享历史等于告诉别人“你拉到的那个提交根本不存在”,会让整个团队进入混乱状态。
如果确实需要修正已推送的提交,一种相对安全的方式是提交一个“反向提交”,把改动回滚掉,然后正常push,而不是强制改写历史。
7. 高频报错排查:从fatal到一切正常
命令行用多了,总会遇到报错。这一节是我遇到过、也带新人遇到次数最多的几个问题,完整走一遍排查链路。
7.1 fatal: not a git repository (or any of the parent directories): .git
这个报错典型症状是:在某个目录下敲git log、git status,结果提示这根本不是Git仓库。
根因分析:当前目录不是仓库,或者当前目录不在任何仓库的子目录里。Git找仓库的方式是逐级向上找.git文件夹,一直找到文件系统根目录都找不到就报这个错。
排查链路:
- 执行
pwd确认当前路径在哪。 - 执行
ls -a看当前目录有没有.git文件夹。 - 如果不在仓库内,切回仓库目录:
cd /path/to/repo。 - 如果不知道仓库在哪,用
find / -name ".git" -type d 2>/dev/null全盘搜一下(Linux/macOS/Git Bash下可用)。
有一次我帮同事看问题,他坚称自己就在仓库里,结果一查,原来他在仓库的上一层目录建了个子文件夹,把代码复制进去开发了。git只认.git的位置,不认文件夹名字,复制到新目录后原来那个.git并不会跟着过来。
7.2 其他高频报错速查表
| 报错信息 | 大概率原因 | 解决办法 |
|---|---|---|
Please tell me who you are | 未配置user.name/user.email | 执行git config --global user.name和user.email |
src refspec main does not match any | 本地还没有任何提交就push | 先git commit再push |
failed to push some refs | 远程有本地没有的提交 | git pull后再push |
Your branch is ahead of 'origin/main' by 1 commit | 本地领先远程 | git push |
fatal: refusing to merge unrelated histories | 两个仓库没有共同历史就合并 | 要合并两个独立仓库时加--allow-unrelated-histories;如果是误操作,检查自己是不是clone错了仓库 |
Permission denied (publickey) | SSH密钥没配好 | 检查公钥是否添加到托管平台,私钥是否存在且权限正确 |
The file will have its original line endings | 换行符转换提示 | 配置core.autocrlf,一般不影响功能 |
这里面refusing to merge unrelated histories值得一提。有次我初始化本地仓库后,又执行git remote add origin想关联远程,然后git pull直接报这个错。原因是我本地的初始提交和远程仓库完全没有共同祖先。解决办法很简单:要么直接git clone而不是先init再remote,要么确实需要合并两个独立项目时用--allow-unrelated-histories。搞清楚是哪种情况,比硬加参数重要。
7.3 一条被我盯了很久的冷门命令
热搜词里有一条看起来很奇怪的命令:
git -c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks这其实不是一条“常用命令”,而是某些IDE或作工具在调用Git时自动加上的配置参数。-c表示临时设置某个配置项而不写入全局配置,--no-optional-locks是告诉Git不要在某些纯读操作里创建不必要的锁文件。如果你在进程列表或日志里看到类似的长串命令,不用慌,这是工具在用Git,不代表你敲错了命令。
写在最后:我的一点个人体会
Git命令行这套东西,上手时觉得别扭,用久了反而离不开。现在我一般进一个项目目录,先git status看状态,再git log --oneline -10看最近的提交,心里对整个项目的进展脉络就很清晰了。这种“掌握感”是GUI给不了的。
再分享一个小技巧:如果你总记不住命令,别硬背,在~/.bashrc或~/.bash_profile里设置一些短别名。我自己常配的是:
alias gs='git status' alias ga='git add' alias gc='git commit -m' alias gl='git log --oneline --graph --decorate -20' alias gp='git push'现在开发新项目,只需要ga、gc、gp三个别名就能完成八成日常操作。剩下的命令遇到再查,Git的提示本身已经很友好了,报错了就按它说的改,改几次自然就记住了。