文章目录
Git中的四个区域

git checkout是老命令,功能很多,既能切分支,也能恢复文件,语义容易混淆;git switch是后来拆分出来的新命令,专门用于切换分支
Workspace:工作区
定义:工作区是当前项目目录中真实存在于磁盘上的文件,也就是开发者正在编辑、创建、删除或重命名文件的地方
特点
- 工作区中的文件是真实存在于磁盘上的文件
- 保存的是当前可见的工作状态
当前仓库最近一次提交中有一个文件app.py,内容是print("Hello"),然后在编辑器中将它改成print("Hello Git"),此时print("Hello Git")就是工作区中的当前可见状态
可以用这个过程理解:Repository 中的版本:app.py -> print("Hello"),修改文件后Workspace 中的状态:app.py -> print("Hello Git"),此时运行git status,Git会发现modified: app.py,即工作区中的文件内容已经和当前HEAD指向的提交版本不同了 - 工作区中的状态可能包括新建文件、修改文件、删除文件、重命名文件,即对文件的增删改查
常用命令
git status:用于查看工作区和暂存区相对于当前HEAD的状态git diff:用于比较工作区与暂存区之间的差异
Index / Stage:暂存区
定义:暂存区保存的是下一次commit将要提交的内容清单
特点
- 暂存区不是整个工作区的自动快照,而是通过
git add主动加入的内容 - 可以被理解为“下一次提交的预备区域”
- Git允许开发者只暂存部分修改,从而把多个修改拆分为多个不同的
commit
常见命令
git add <file>:将工作区中的修改加入暂存区git diff --cached或git diff --staged:用于比较暂存区与当前HEAD之间的差异,也就是查看下一次commit将提交哪些内容
Repository:仓库区 / 本地仓库
定义:本地仓库保存的是项目的提交历史,也就是一系列commit对象
特点
- 每一次
commit都会生成一个不可变的历史版本 - 本地仓库位于项目目录中的
.git文件夹内 HEAD通常会指向当前所在的分支,而当前分支再指向最新的commit
例如,在main分支上git switch main,此时Git的指向关系是HEAD→ \to →main→ \to → 最新commit,如果继续commit,main分支会自动向前移动- 在detached HEAD状态下,
HEAD会直接指向某个具体的commit
常用命令
git commit:将暂存区中的内容提交到本地仓库,形成一个新的历史版本
Remote:远程仓库
定义:远程仓库是保存在远程服务器上的Git Repository,例如Github、Gitee上的仓库
作用:团队协作、云端备份、多设备同步、发布和托管代码
常用命令
git push:将本地仓库中的提交推送到远程仓库git fetch:从远程仓库中获取最新提交,但不会自动合并到当前分支git pull:从远程仓库获取最新提交,并尝试合并到当前分支
Git分支
Git分支的作用:用于让不同任务彼此独立,例如功能开发、Bug 修复、实验性代码等。通过分支,可以避免不同修改混在一起,从而降低代码管理和版本发布的复杂度
在Git仓库中,每一次commit都会形成一个历史节点
例如:A —— B —— C,默认情况下,Git 仓库通常会有一个主分支,例如main
A —— B —— C(main)
▲
│
HEAD
这里的main指向 Commit C。需要注意的是,main并不是“永远指向整个仓库最新的Commit”,而是指向main这条分支上的最新Commit
在Git中,分支本质上是一个可以移动的指针。它指向某个具体的commit,并且会随着新的提交不断向前移动
为什么需要分支?例如你正在开发一个在线商城,目前已经完成并上线了版本A —— B —— C(main)。此时,老板说需要增加“支付功能”,同时测试人员又发现“登录页面有 Bug”。如果没有分支,你只能直接在当前代码上修改
A —— B —— C(main)
│
▼
修改支付
但是,支付功能可能还没有写完,Bug 修复也可能只完成了一半。此时如果客户要求“先紧急修复登录 Bug 并上线”,就会很麻烦,因为支付功能的半成品代码已经混入当前代码中,难以单独发布
使用分支后,可以把不同任务隔离开来,即支付功能开发 → feature-payment、登录 Bug 修复 → fix-login,这样,功能开发和 Bug 修复可以互不影响
在分支上继续开发
一个软件开发案例:假设公司正在开发“学生管理系统”,目前已经完成了A 初始化项目 —— B 登录功能 —— C 学生成绩查询,当前历史A —— B —— C(main),现在老板要求增加“导出 Excel”的功能
可以先创建一个新的分支git branch export-excel,此时main和export-excel都指向Commit C:
A —— B —— C(main)
│
└── export-excel
不过,git branch export-excel只是创建分支,并不会自动切换到该分支。如果要切换到新分支,需要执行git switch export-excel,也可以直接创建并切换到新分支git switch -c export-excel,切换之后,可以理解为
A —— B —— C(main)
│
export-excel
│
HEAD
也就是说,当前位于export-excel分支,以后在这个分支上产生的新提交,都会让export-excel分支向前移动,而不会影响main分支
在export-excel分支上开发完成后,可以执行
git add .
git commit -m "新增 Excel 导出功能"
此时会产生新的 Commit D
A —— B —— C(main)
\
D(export-excel)
如果继续提交,又会产生 Commit E
A —— B —— C(main)
\
D —— E(export-excel)
因此,分支可以理解为:从某一个 Commit 开始,继续向前发展的一条新的时间线
在 export-excel 分支开发的同时,main 分支也可以继续进行其他修改。例如,main分支上产生了 Commit F
D —— E(export-excel)
/
A —— B —— C
\
F(main)
此时,main和export-excel已经产生了不同的提交历史
合并分支
当export-excel功能开发完成后,如果希望把它合并回main,可以先切换回main
git switch main
然后执行合并
git merge export-excel
合并之后,可能会生成一个新的 Merge Commit,例如 G
D —— E(export-excel)
/ \
A —— B —— C —— F —— G(main)
此时,main分支就包含了export-excel分支中的修改
为什么Git合并不会复制整个项目?因为 Git 采用的是Commit + 指针的机制
⇒
\Rightarrow
⇒Git 管理的核心不是“复制两个完整文件夹”,而是管理一系列 Commit 以及指向这些 Commit 的分支指针
A —— B —— C main
\
D —— E feature
其中,A、B、C、D、E 都是 Commit。每个 Commit 都记录了当前项目的文件快照,同时也记录了自己的父 Commit。Git 在内部会对对象进行压缩和复用,因此并不是每次都简单复制整个项目文件夹
Git 合并的不是两个文件夹,而是两条 Commit 历史
假设有如下历史
A —— B —— C main
\
D —— E feature
这里,main和feature的共同祖先是 Commit B。Git 在合并时会先找到共同祖先,也叫 Merge Base,即Merge Base:B,然后 Git 会比较B 到 main 发生了什么修改和B 到 feature 发生了什么修改,最后,Git 会尝试把两边的修改合并起来
Git Merge 的几种常见情况
- Fast-forward:快进合并
特点:只移动分支指针,不产生新的 Merge Commit
如果main分支在创建feature之后没有新的提交,那么合并feature时,Git 不需要创建新的 Merge Commit,只需要把main指针向前移动到 E 即可
当前历史:
A —— B —— C main
\
D —— E feature
执行:
git switch main
git merge feature
合并后:
A —— B —— C —— D —— E
│
main
│
feature
- Three-way Merge:三方合并
如果两个分支都各自向前发展了,就不能简单快进合并
D —— E feature
/
A —— B
\
C —— F main
Git 需要比较三方内容:共同祖先 B;main 分支的修改;feature 分支的修改
然后生成一个新的 Merge Commit:
D —— E
/ \
A —— B M
\ /
C —— F
这里的 M 就是 Merge Commit
Merge Commit 有两个父节点,可以同时保留两条分支的开发历史
- Already up to date
如果当前分支已经包含了要合并分支的全部内容,再执行合并命令时,Git 会提示Already up to date.
A —— B —— C —— D(main)
\
D(feature)
或者更一般地说,只要 feature 的提交已经被 main 包含,那么在 main 上执行git merge feature,Git 就不会产生新的 Commit,因为没有新的内容需要合并
- 自动合并成功
如果两个分支修改的是不同文件,或者修改的是同一个文件中的不同位置,Git 通常可以自动合并成功
main 修改了 login.py
feature 修改了 export.py
这种情况下,Git 一般可以直接完成合并
main 修改了 app.py 的第 10 行
feature 修改了 app.py 的第 100 行
如果两处修改没有冲突,Git 也通常可以自动合并
- 合并冲突
如果两个分支修改了同一个文件中的同一位置,Git 可能无法判断应该保留哪一方的内容,于是产生冲突
例如,两个分支的共同祖先中有a=10,main分支修改为a=20,feature分支修改为a=30,此时 Git 不知道应该保留 20 还是 30,于是会产生冲突标记
<<<<<<< HEAD
a = 20
=======
a = 30
>>>>>>> feature
其中,<<<<<<< HEAD表示当前分支的内容,=======表示分隔线,>>>>>>> feature表示另一个分支的内容
如果最终希望改成a=25可以手动编辑文件,删除冲突标记,并保留最终内容a=25,然后执行git add <file>和git commit就可以完成合并。如果不想继续这次合并,可以执行git merge --abort,该命令会尽量恢复到合并之前的状态
Git Merge的常用选项
- –no-ff:默认情况下,如果可以进行 Fast-forward 合并,Git 会直接移动分支指针,不产生新的 Merge Commit。如果希望即使可以 Fast-forward,也强制创建 Merge Commit,可以使用
git merge --no-ff feature,这种方式可以保留“曾经开过一个feature分支”的历史痕迹 - –ff-only:如果只允许快进合并,可以使用
git merge --ff-only feature,只有在能够快进合并的情况下才允许合并。如果当前情况不能 Fast-forward,Git 会直接失败,不会生成 Merge Commit。这通常用于希望保持线性提交历史的场景
bash命令
- 查看所有分支
git branch:查看本地分支
git branch -r:查看远程跟踪分支 → \to →git branch -r看到的是本地保存的远程跟踪分支,例如origin/main,并不是实时访问远程服务器后的结果。如果想要更新远程跟踪分支信息,可以先执行git fetch
git branch -a:查看本地分支和远程跟踪分支
git branch
* main
develop
feature-login
其中,* 表示当前所在的分支
git branch -r
origin/main
origin/dev
git branch -a
main
dev
remotes/origin/main
remotes/origin/dev
git branch feature-payment:创建分支,该命令会在当前HEAD所在的Commit上创建一个新分支feature-payment
例如,当前位于 main 分支,并且 main 指向 Commit C
A —— B —— C(main)
│
HEAD
执行git branch feature-payment
之后,main 和 feature-payment 都指向 Commit C,但当前仍然停留在 main 分支
A —— B —— C(main)
│
feature-payment
HEAD → main
需要注意:git branch feature-payment只创建分支,不会自动切换分支
git switch feature-payment:切换分支,该命令会将当前工作分支切换到feature-payment
切换之后,可以理解为
A —— B —— C(main)
│
feature-payment
│
HEAD
更准确地说:HEAD → feature-payment → C
git switch -c feature-payment:创建并切换分支,该命令等价于git branch feature-payment和git switch feature-payment,也就是说,它会先创建feature-payment分支,然后立即切换到该分支git branch -d feature-payment:该命令用于删除已经合并过的本地分支,如果分支还没有被合并,Git 通常会拒绝删除,以避免丢失提交
如果确认要强制删除,可以使用git branch -D feature-payment,其中-d表示安全删除、-D表示强制删除git branch -m old-name new-name:重命名分支,该命令会将本地分支old-name重命名为new-name
如果要重命名当前所在分支,也可以写成git branch -m new-namegit merge:合并分支
git merge feature-payment:将feature-payment分支合并到当前所在分支
git switch main
git merge feature-payment
把 feature-payment 中的修改合并回 main
如果希望远程仓库中的main分支也包含本地合并结果,需要执行git push origin main,完整的流程通常是git switch main、git merge feature和git push origin main
git init:初始化当前目录为 Git 仓库
它不会创建新的项目目录,而是在当前目录下创建一个.git目录
.git目录中保存了 Git 的所有版本历史、分支、配置和对象信息,是整个本地仓库的核心git init project-name:初始化一个新的项目目录,创建一个名为project-name的目录,并在该目录中初始化Git仓库,等价于mkdir project-name→ \to →cd project-name→ \to →git initgit clone <url>:通过远程仓库地址url克隆一个已有仓库到本地
git clone https://github.com/user/project.git,克隆后,本地会自动拥有工作区、.git本地仓库、远程仓库别名origin、远程跟踪分支origin/maingit remote add origin <url>:关联远程仓库,如果本地仓库是通过git init创建的,可以通过git remote add origin <url>关联远程仓库,其中origin是远程仓库的默认别名、url是远程仓库的真实地址,例如git remote add origin https://github.com/user/project.gitgit config --list:查看所有配置
user.name=Tom
user.email=tom@gmail.com
git config --global --edit:打开并编辑全局配置文件
在 macOS 和 Linux 中,全局配置文件通常是~/.gitconfig
[user]
name = Tom
email = tom@gmail.com
git config --global user.name "Tom":设置用户名,设置后以后提交时的默认会显示Author: Tomgit config --global user.email "tom@gmail.com":设置邮箱,设置后以后提交时默认会使用该邮箱作为提交者邮箱
需要注意,user.name和user.email只是 Git 提交记录中的作者信息,不等同于GitHub、或Gitee的登录账号git add <file>:添加文件到暂存区git mv <old> <new>:重命名文件,并将重命名操作记录到暂存区中git commit -m "message":将暂存区中的内容提交到本地仓库,形成一个新的 Commit
需要注意,git commit只会提交已经通过git add加入暂存区的内容。没有加入暂存区的修改不会被提交git push:推送本地分支到远程仓库
git push <远程仓库名> <本地分支名>:将本地分支推送到指定远程仓库,例如本地分支为feature-login、远程仓库名为origin,则git push origin feature-login。如果远程仓库中还没有feature-login分支,Git 会自动在远程仓库中创建一个同名分支,并把本地提交推送过去git push -u <远程仓库名> <本地分支名>:推送并建立追踪关系
git push -u origin feature-login:将本地feature-login分支推送到远程origin仓库,同时建立本地feature-login与远程origin/feature-login的追踪关系,建立追踪关系之后,在feature-login分支上通常可以直接执行git push或git pull,而不需要每次都写完整的远程仓库名和分支名git push <远程仓库名> <本地分支名>:<远程分支名>:将本地分支推送到一个不同名的远程分支
git push origin hotfix:hotfix-v2:将本地的hotfix分支推送到远程的hotfix-v2分支
git clone、git fetch、git pull、git restore对比
| bash 命令 | 是否访问远程仓库 | 是否修改本地仓库 | 是否修改暂存区 | 是否修改工作区 | 是否自动合并 |
|---|---|---|---|---|---|
| git clone | 是 | 是,创建新的本地仓库 | 是,初始化暂存区 | 是,检出默认分支 | 不涉及 |
| git fetch | 是 | 是, 更新远程跟踪分支 | 否 | 否 | 否 |
| git pull | 是 | 是,下载并合并或变基 | 通常会 | 通常会 | 是 |
| git restore | 否 | 否,不创建 Commit | 可能会 | 可能会 | 否 |
检出默认分支:“检出”可以被理解为将某个分支或某个默认的 Commit 对应的文件内容放到本地的工作区中,“默认分支”则是远程仓库指定的主要分支
git clone <url>:从远程服务器url复制一个 Git 仓库到本地
执行git clone后,Git 通常会完成以下工作:从远程仓库下载提交历史 → \to →下载分支和标签信息 → \to →创建本地项目目录 → \to →创建.git本地仓库 → \to →建立远程仓库别名origin→ \to →建立远程跟踪分支,例如origin/main→ \to →创建并检出默认本地分支,例如main→ \to →将默认分支内容写入工作区 → \to →最终得到project/、.git/、src/、README.md
需要注意,git clone默认通常只创建一个本地分支,也就是远程仓库的默认分支,例如main
其他远程分支通常不会立即变成本地分支,而是以远程跟踪分支的形式存在,例如origin/dev、origin/feature-login
如果要基于远程分支创建本地分支,可以执行git switch dev,如果本地还没有dev分支,而远程存在origin/dev,Git 通常会自动创建本地dev分支,并与origin/dev建立追踪关系git fetch:从远程仓库中下载最新提交、分支和标签信息,但不修改当前工作区,也不自动合并 ⇒ \Rightarrow ⇒git fetch只更新本地保存的远程跟踪分支
可以通过git diff main origin/main查看本地main分支和远程跟踪分支origin/main之间的文件差异
远程仓库
A —— B —— C —— D(origin/main)
本地当前分支
A —— B —— C(main)
在本地执行 git fetch 之后,本地的远程跟踪分支 origin/main 会前进到 D
A —— B —— C —— D(origin/main)
A —— B —— C(main)
此时 origin/main 前进了;main 没有动;工作区也没有动
git pull:拉取远程仓库的提交,并将其合并到当前所在的本地分支
默认情况下:git pull = git fetch + git merge,git fetch负责下载远程更新,即远程仓库 → 本地仓库中的远程跟踪分支;而git merge负责把远程跟踪分支合并到当前本地分支。因此,执行git pull origin main可以理解为git fetch origin main→ \to →git merge origin/main
如果启用了 rebase 模式:git pull = git fetch + git rebase
Merge:保留分叉历史和合并轨迹
假设当前历史如下:
A —— B —— C main
\
D —— E feature
在 main 分支执行:git merge feature
合并后:
A —— B —— C —— M main
\ /
D —— E feature
其中,M 是新的 Merge Commit。
Rebase:整理成线性历史 ⇒ \Rightarrow ⇒Rebase 的本质不是简单移动原来的提交,而是基于原来的修改内容创建新的提交
假设当前历史如下:
A —— B —— C main
\
D —— E feature
在 feature 分支执行:git rebase main
变基后:
A —— B —— C main
\
D' —— E' feature
需要注意,D' 和 E' 是新的提交
它们和原来的 D、E 修改内容可能相同,但提交哈希值不同
在 Git 中,Commit Hash 是提交的唯一标识,可以理解为提交的“身份证”。在 Rebase 中,D 和 D’ 的修改内容可能一样,但由于父提交变了,所以哈希值也会变
git restore:恢复文件或撤销文件修改
git restore是 Git 后来引入的命令,主要是为了拆分旧命令git checkout的复杂功能
丢弃工作区的未暂存修改:如果修改了文件,但还没有执行git add,想放弃这些修改,让文件恢复到最近一次提交的状态,可以执行git restore <file>。例如,丢弃对 index.html 的修改git restore index.html;如果想恢复当前目录下所有被修改的文件,可以执行git restore
取消暂存:如果已经使用git add将文件加入暂存区,但想把它撤回到“已修改但未暂存”的状态,可以执行git restore --staged <file>。例如,git restore --staged index.html⇒ \Rightarrow ⇒作用是把文件从暂存区撤回,工作区中的实际修改仍然保留,即git restore --staged不会删除对文件的修改,只是取消暂存
同时恢复暂存区和工作区:如果想让文件彻底回到HEAD状态,可以执行git restore --staged <file>和git restore <file>,第一步取消暂存,第二步丢弃工作区修改
参考文献
- 常用 Git 命令清单
- ChatGPT Plus
2万+

被折叠的 条评论
为什么被折叠?



