Git 核心原理与基础使用
在软件开发的全流程中,版本控制是保障协作效率、规避风险的核心基石。Git 作为目前最流行的分布式版本控制系统,已渗透到从个人开发到企业级项目的每一个环节。无论是多人协作时的代码冲突解决、开发过程中的版本回溯,还是跨环境的代码同步、分支管理,Git 都以其高效、安全、灵活的特性,成为开发者必备的核心工具。
然而,多数开发者对 Git 的使用仍停留在'会用基础命令'的层面——知道用 git add 提交暂存、git commit 提交本地、git push 推送远程,却未必理解这些命令背后的底层逻辑:暂存区(Stage)、本地仓库(Local Repository)、远程仓库(Remote Repository)之间的数据流是怎样的?Git 如何高效追踪文件的每一次变更?分布式架构与 SVN 等集中式版本控制系统相比,核心优势到底体现在哪里?
本文将分为上下两篇,全面深入地剖析 Git 的原理与使用。其中上篇将重点聚焦 Git 的核心原理——从版本控制的本质出发,拆解 Git 的核心架构、关键对象(Blob、Tree、Commit)、三大区域的工作机制,以及 Git 追踪文件变更的底层逻辑,帮你建立对 Git 的系统性认知。下篇则会聚焦实际应用,结合高频场景讲解 Git 的核心命令、分支管理策略、冲突解决方法等。无论你是刚接触 Git 的新手,还是有一定使用经验、希望突破瓶颈的开发者,相信通过本文的剖析,都能对 Git 形成更深刻的理解。
1. 初识 Git
你是否遇到过这样的情况:编写文档或代码时,为了防止丢失或修改失误,不得不复制出多个副本,比如'报告-v1'、'报告-v2'、'报告-最终版'……随着版本数量增多,你很难记得每个版本具体修改了什么。
文档如此,项目代码更是存在同样的问题。如何解决?引入版本控制器。它记录工程的每一次改动和版本迭代,方便多人协同作业。目前最主流的版本控制器就是 Git。
注意事项: 所有的版本控制系统(Git 也不例外),其实只能跟踪文本文件的改动,比如 TXT、网页、程序代码等。它可以告诉你每次的改动细节(如第 5 行加了什么)。而图片、视频等二进制文件,虽然也能由版本控制系统管理,但无法跟踪文件内部的变化,只能记录文件大小或哈希值的改变。
2. Git 安装
Git 最早是在 Linux 下开发的,现在可以在 Linux、Unix、Mac 和 Windows 这几大平台上正常运行。
2.1 Linux — CentOS 系统安装 Git
以 CentOS 7.6 为例,首先尝试输入 git 看看系统是否已安装:
$ git
-bash: git: command not found
如果提示未找到命令,可以使用以下命令安装并查看版本:
sudo yum -y install git
git --version
2.2 Linux — Ubuntu 系统安装 Git
以 Ubuntu 22.04 为例,初次安装时通常会提示如何安装:
$ git
Command 'git' not found, but can be installed with: sudo apt install git
执行安装命令:
sudo apt-get install git -y
git --version
2.3 Windows 系统安装 Git
Windows 用户可前往官网下载安装包,按照向导完成安装即可。
3. Git 基本操作
3.1 创建 Git 本地仓库
仓库是进行版本控制的一个文件目录。要对文件进行版本控制,必须先创建一个仓库。对应的命令为 git init,注意要在目标目录下执行。
执行后,当前目录下会多出一个 .git 隐藏目录。这是 Git 用来跟踪管理仓库的地方,包含诸多细节,不要手动修改其中的文件,否则可能破坏仓库结构。
3.2 配置 Git 本地仓库
安装 Git 后,首先要设置用户名和邮箱地址,这对提交记录至关重要。
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
--global 是可选项。使用该选项表示这台机器上所有的 Git 仓库都会使用这个配置。如果希望在不同仓库中使用不同的 name 或 email,可以不加该选项,但执行时必须处于仓库目录内。
查看配置:
git config -l
删除配置:
git config --global --unset user.name
git config --global --unset user.email
3.3 认识工作区、暂存区、版本库
Git 的工作流程涉及三个主要区域:
- 工作区(Working Directory):你在电脑上要写代码或文件的目录。
- 暂存区(Stage/Index):一般存放在
.git目录下的index文件中。 - 版本库(Repository):即
.git目录本身。它不算工作区,而是 Git 的版本库。里面的所有文件都可以被 Git 管理起来,每个文件的修改、删除,Git 都能跟踪。
工作机制:
- 创建 Git 版本库时,Git 会自动创建一个唯一的
master分支,以及指向master的一个指针叫HEAD。 - 对工作区修改(或新增)的文件执行
git add命令时,暂存区目录树的文件索引会被更新。 - 执行提交操作
git commit时,master分支会做相应的更新,暂存区的目录树才会被真正写到版本库中。
因此,仅仅新建或粘贴文件到目录,并不能称之为向仓库中新增文件,只是在工作区新增了文件。必须通过使用 git add 和 git commit 命令才能将文件添加到仓库中进行管理。
3.4 添加文件
在包含 .git 的目录下新建一个 ReadMe 文件,我们可以使用 git add 命令将文件添加到暂存区:
- 添加一个或多个文件到暂存区:
git add [file1] [file2] ... - 添加指定目录到暂存区(包括子目录):
git add [dir] - 添加当前目录下的所有文件改动到暂存区:
git add .
再使用 git commit 命令将暂存区内容添加到本地仓库中:
- 提交暂存区全部内容到本地仓库中:
git commit -m "message" - 提交暂存区的指定文件到仓库区:
git commit [file1] [file2] ... -m "message"
注意: git commit 后面的 -m 选项,要跟上描述本次提交的 message,这部分内容绝对不能省略,要好好描述,是用来记录你的提交细节的。
执行成功后,Git 会告诉我们文件变动情况。我们还可以多次 add 不同的文件,而只 commit 一次,因为需要提交的文件都是先被 add 到暂存区中,然后一次性 commit。
截至目前,我们已经能够将代码直接提交至本地仓库。可以使用 git log 命令来查看历史提交记录:
git log --pretty=oneline
该命令显示从最近到最远的提交日志。如果你看到的是一长串类似 11ae5079a637... 的字符,那是每次提交的 commit id(版本号)。Git 的 commit id 不是递增的数字,而是一个 SHA1 计算出来的十六进制数字。
3.5 查看 .git 文件
让我们看看 .git 目录的结构:
- index:就是我们的暂存区,
add后的内容都是添加到这里的。 - HEAD:默认指向
master分支的指针。 - refs/heads/master:文件里保存当前
master分支的最新commit id。 - objects:包含了创建的各种版本库对象及内容,简单理解为放了 Git 维护的所有修改。
当执行 git add 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新对象中,位于 .git/objects 目录下。查找 object 时要将 commit id 分成两部分,其前 2 位是文件夹名称,后 38 位是文件名称。该类文件是经过 SHA 加密过的,可以使用 git cat-file 命令来查看版本库对象的内容。
3.6 添加文件 — 场景二
学习到这里,我们已经清楚了如何向仓库中添加文件。再展示一种添加文件的场景,能加深对三区域的理解。
假设提交了两个文件,但发现打印了 1 file changed,而不是 2 个。这是因为 git add 是将文件添加到暂存区,git commit 是将暂存区的内容添加到本地仓库中。如果某个文件没有使用 git add,它就不在暂存区中维护,所以 commit 的时候只是把已经在暂存区的文件提交了,遗漏了工作区的文件。解决方法很简单,再次 add,commit 即可。
3.7 修改文件
Git 跟踪并管理的是修改,而非文件。比如新增了一行、删除了一行、更改了某些字符,甚至创建一个新文件,都算是一个修改。
此时,仓库中的文件和我们的工作区的文件是不同的。如何查看当前仓库的状态?git status 命令用于查看在你上次提交之后是否有对文件进行再次修改。
如果想看具体哪些地方被修改了,可以使用 git diff [file] 命令来显示暂存区和工作区文件的差异。也可以使用 git diff HEAD -- [file] 命令来查看版本库和工作区文件的区别。知道了对文件做了什么修改后,再把它提交到本地仓库就放心多了。
3.8 版本回退
Git 能够管理文件的历史版本。如果发现之前做的工出现很大问题,需要在某个特定的历史版本重新开始,就需要版本回退的功能。
执行 git reset 命令用于回退版本,可以指定退回某一次提交的版本。要解释一下'回退'本质是:要将版本库中的内容进行回退,工作区或暂存区是否回退由命令参数决定:
git reset [--soft | --mixed | --hard] [HEAD]
- --mixed:默认选项。将暂存区的内容退回为指定提交版本内容,工作区文件保持不变。
- --soft:对于工作区和暂存区的内容都不变,只是将版本库回退到某个指定版本。
- --hard:将暂存区与工作区都退回到指定版本。切记工作区有未提交的代码时不要用这个命令,因为工作区会回滚,你没有提交的代码就再也找不回了。
HEAD 说明:
- 可直接写成
commit id,表示指定退回的版本。 HEAD表示当前版本。HEAD^上一个版本。HEAD^^上上一个版本。- 或者使用
~数字表示:HEAD~1上一个版本,HEAD~2上上一个版本。
如果清屏操作导致 commit 信息消失,还可以使用 git reflog 命令补救,该命令用来记录本地的每一次命令,方便找回操作记录。
3.9 撤销修改 — 情况一
如果在工作区写了很长时间代码,觉得写得不好,想恢复到上一个版本。
情况一:对于工作区的代码,还没有 add。
我们可以使用 git checkout -- [file] 命令让工作区的文件回到最近一次 add 或 commit 时的状态。要注意命令中的 -- 很重要,切勿省略,一旦省略,该命令就变为其他意思了。
3.10 撤销修改 — 情况二
情况二:已经 add,但没有 commit。
add 后文件保存到了暂存区。怎么撤销呢?回忆一下 git reset 回退命令,如果使用 --mixed 参数,可以将暂存区的内容退回为指定的版本内容,但工作区文件保持不变。这样就能回退暂存区的内容了。
3.11 撤销修改 — 情况三
情况三:已经 add,并且也 commit 了。
可以使用 git reset --hard HEAD^ 回退到上一个版本!不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。一旦你推送到远程版本库,你就真的麻烦了,因为远程已经有了历史记录。
3.12 删除文件
在 Git 中,删除也是一个修改操作。如果要删除文件,直接删除是没有用的,反而徒增烦恼。git status 命令会立刻告诉你哪些文件被删除了。此时,工作区和版本库就不一致了。
一般走到这里,有两种可能:
- 确实要从版本库中删除该文件。
- 不小心删错了。
对于第二种情况,误删了,需要使用 Git 来进行恢复。对于第一种情况,显然没有删完,我们只删除了工作区的文件。这时就需要使用 git rm 将文件从暂存区和工作区中删除,并且 commit。
4. 分支管理
4.1 理解分支
Git 的杀手级功能之一:分支。分支就是科幻电影里的平行宇宙。当你正在电脑前努力学习 C++ 的时候,另一个你正在另一个平行宇宙里努力学习 JAVA。如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了 C++ 又学会了 JAVA!
在版本回退里,你已经知道,每次提交,Git 都把它们串成一条时间线,这条时间线就可以理解为一个分支。截止到目前,只有一条时间线,在 Git 里,这个分支叫主分支,即 master 分支。再来理解一下 HEAD,HEAD 严格来说不是指向提交,而是指向 master,master 才是指向提交的,所以,HEAD 指向的就是当前分支。
4.2 创建、切换、合并分支
-
创建分支 Git 支持我们查看或创建其他分支。在这里我们来创建第一个自己的分支
dev,对应的命令为:git branch dev当我们创建新的分支后,Git 新建了一个指针叫
dev,*表示当前HEAD指向的分支是master分支。 -
切换分支 那如何切换到
dev分支下进行开发呢?使用git checkout命令即可完成切换。git checkout dev我们发现
HEAD已经指向了dev,就表示我们已经成功的切换到了dev上!接下来,在dev分支下修改文件,新增一行内容,并进行一次提交操作。现在,
dev分支的工作完成,我们就可以切换回master分支。切换回master分支后,发现新增的内容不见了。这是因为我们在dev分支上提交的,而master分支此刻的提交点并没有变。 -
合并分支 为了在
master主分支上看到新的提交,就需要将dev分支合并到master分支。git merge devgit merge命令用于合并指定分支到当前分支。合并后,master就能看到dev分支提交的内容了。此时的状态如图如下所示:Fast-forward代表'快进模式',也就是直接把master指向dev的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward。
4.3 删除分支
合并完成后,dev 分支对于我们来说就没用了,那么 dev 分支就可以被删除掉。注意如果当前正处于某分支下,就不能删除当前分支。但是可以在其他分支下删除当前分支。
因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。
4.4 合并冲突
可是,在实际分支合并的时候,并不是想合并就能合并成功的,有时候可能会遇到代码冲突的问题。为了演示这问题,创建一个新的分支 dev1,并切换至目标分支,我们可以使用 git checkout -b dev1 一步完成创建并切换的动作。
在 dev1 分支下修改文件,更改文件内容如下,并进行一次提交。切换至 master 分支,观察文件内容。此时在 master 分支上,我们对文件再进行一次修改,并进行提交。
现在,master 分支和 dev1 分支各自都分别有新的提交。这种情况下,Git 只能试图把各自的修改合并起来,但这种合并就可能会有冲突。
发现文件有冲突后,可以直接查看文件内容。Git 会用 <<<<<<<, =======, >>>>>>> 来标记出不同分支的冲突内容。此时我们必须要手动调整冲突代码,并需要再次提交修正后的结果!!(再次提交很重要,切勿忘记)。
到这冲突就解决完成。最后,不要忘记 dev1 分支使用完毕后就可以删除了。
4.5 分支模式
通常合并分支时,如果可能,Git 会采用 Fast forward 模式。在这种模式下,删除分支后,查看分支历史时,会丢掉分支信息,看不出来最新提交到底是 merge 进来的还是正常提交的。
Git 支持我们强制禁用 Fast forward 模式,那么就会在 merge 时生成一个新的 commit,这样,从分支历史上就可以看出分支信息。下面实战一下 --no-ff 方式的 git merge。
请注意 --no-ff 参数,表示禁用 Fast forward 模式。禁用 Fast forward 模式后合并会创建一个新的 commit。所以加上 -m 参数,把描述写进去。合并后,查看分支历史。可以看到,不使用 Fast forward 模式,merge 后就像这样。所以在合并分支时,加上 --no-ff 参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并。
4.6 分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master 分支应该是比较稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在 dev 分支上,也就是说,dev 分支是不稳定的,到某个时候,比如 1.0 版本发布时,再把 dev 分支合并到 master 上,在 master 分支发布 1.0 版本。你和你的小伙伴们每个人都在 dev 分支上干活,每个人都有自己分支,时不时地往 dev 分支上合并就可以了。
4.7 bug 分支
假如我们现在正在 dev2 分支上进行开发,开发到一半,突然发现 master 分支上面有 bug,需要解决。在 Git 中,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
可现在 dev2 的代码在工作区中开发了一半,还无法提交,怎么办?例如:Git 提供了 git stash 命令,可以将当前工作区信息进行储藏,被储藏的内容可以在将来某个时间恢复出来。
用 git status 查看工作区,就是干净的(除非有没有被 Git 管理的文件),因此可以放心地创建分支来修复 bug。储藏 dev2 工作区之后,由于我们要基于 master 分支修复 bug,所以需要切回 master 分支,再新建临时分支来修复 bug。
修复完成后,切换到 master 分支,并完成合并。至此,bug 的修复工作已经做完了,我们还要继续回到 dev2 分支进行开发。切换回 dev2 分支。工作区是干净的,刚才的工作现场存到哪去了?用 git stash list 命令看看。
工作现场还在,Git 把 stash 内容存在某个地方了,但是需要恢复一下,如何恢复现场呢?我们可以使用 git stash pop 命令,恢复的同时会把 stash 也删了。
恢复完代码之后我们便可以继续完成开发,开发完成后便可以进行提交。但我们注意到了,修复 bug 的内容,并没有在 dev2 上显示。此时的状态图为:master 分支目前最新的提交,是要领先于新建 dev2 时基于的 master 分支的提交的,所以我们在 dev2 中当然看不见修复 bug 的相关代码。
我们的最终目的是要让 master 合并 dev2 分支的,那么正常情况下我们切回 master 分支直接合并即可,但这样其实是有一定风险的。是因为在合并分支时可能会有冲突,而代码冲突需要我们手动解决(在 master 上解决)。我们无法保证对于冲突问题可以正确地一次性解决掉。
解决这个问题的一个好的建议就是:最好在自己的分支上合并下 master,再让 master 去合并 dev,这样做的目的是有冲突可以在本地分支解决并进行测试,而不影响 master。
4.8 删除临时分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个分支,我们可以将其称之为 feature 分支,在上面开发,完成后,合并,最后,删除该 feature 分支。
可是,如果我们今天正在某个 feature 分支上开发了一半,被产品经理突然叫停,说是要停止新功能的开发。虽然白干了,但是这个 feature 分支还是必须就地销毁,留着无用了。这时使用传统的 git branch -d 命令删除分支的方法是不行的。演示如下:
小结:分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了 50% 的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。现在有了分支,就不用怕了。你创建了一个属于自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。并且 Git 无论创建、切换和删除分支,Git 在 1 秒钟之内就能完成!无论你的版本库是 1 个文件还是 1 万个文件。


