Git的故事
Oh my Git! 如今,它已经成为我日常工作中不可或缺的工具。然而,当我最初接触Git时,学习过程充满挑战,甚至有些令人望而生畏。此前,我习惯于使用Perforce,并且——老实说,我一点也不喜欢它(这里有充分理由)。但时至今日,Git已无处不在,不仅开发人员和运维工程师,甚至设计师、内容创作者和艺术家都在广泛使用它。因此,深入理解这个工具,对你大有裨益。
在这篇文章中,我将分享自己学习Git的经历,这会与大多数常见的指南有所不同。我的目标不仅是帮助你掌握Git的使用方法,更重要的是,让你真正理解Git的底层原理和工作机制,从核心概念出发,构建系统性的知识体系。
我们将从Git的基本构件开始,探讨其数据类型和对象——它们的作用,以及Git如何存储数据。接着,我们会深入研究Git如何通过不可变的图结构来实现内容跟踪,以及这一机制如何确保其高效性和可靠性。随后,我们将了解Git的远程仓库,以及它如何支持去中心化的工作流,甚至允许同时管理多个远程仓库。在掌握这些基础知识后,我们将学习常用的Git命令及其背后的工作原理。最令人兴奋的部分来了!当你对Git的内部机制有了扎实的理解后,我们还会探索一个强大的高级工具——reflog,它能让你几乎撤销任何意外操作,无论是*误删分支*还是错误的rebase,都能找到解决方案。本系列文章的第二部分将深入探讨这些高级用法。
这篇文章内容丰富,让我们立即开始吧!
Git的诞生
Git项目最初由Linus Torvalds创建,他在开发Linux内核时,迫切需要一个快速、高效且适用于大规模分布式协作的源码管理系统。
2002年,Linux内核团队从传统的补丁邮件系统转向了专有的版本控制工具——BitKeeper SCM。然而,2005年4月,由于BitMover公司认为部分开发者违反了许可协议并试图对其进行逆向工程,他们决定不再向开源社区提供免费的BitKeeper版本。
Linus向来对现有的大多数版本控制系统持强烈的不满,因此,他做了一个果断的决定——自己开发一个新的系统。就这样,Git在2005年4月诞生了。几个月后,到了7月,Git的维护工作交由Junio Hamano接手,并由他持续维护至今。
最初,Git只是一个由shell和Perl脚本拼凑而成的工具集。随着时间推移,从1.0版本开始,越来越多的脚本被重写为C语言(称为“内建功能”),大大提升了Git的可移植性和运行速度。
虽然Git最初是为Linux内核开发量身定制的,但它很快传播开来,成为X.org、Mesa3D、Wine、Fedora等多个开源项目的主要版本控制工具。如今,我们都知道,几乎所有软件开发项目,尤其是开源社区,已经离不开Git了。
注意: 本文中的部分内容借鉴了开源社区的资源和灵感,我个人无法制作出如此精美的插图。所有的功劳归于原作者,我在文章末尾的参考部分中列出了相关信息。(所有引用内容均经过授权并符合使用条款。)
版本控制技术
那么,什么是版本控制?简单来说,它就是在创建和编辑代码或内容的过程中,记录所有变更,并支持回溯和协作。版本控制的核心目标,是让多个开发者或团队成员能够高效协作,而不会相互干扰。
目前,主流的版本控制方式主要分为两类:
- 集中式版本控制(Centralized Version Control,CVCS):所有操作都依赖于一个中央服务器,开发者必须与该服务器同步。
- 分布式版本控制(Distributed Version Control,DVCS):每个开发者的本地仓库都拥有完整的代码库历史记录,因此不依赖单一服务器,Git便属于这一类。除此之外,还有Mercurial等其他优秀的分布式版本控制工具。
集中式版本控制
在集中式系统中,代码仓库存储在一个中央服务器上,所有开发者都需要与它进行交互。这个工作流简单直观,但也有明显的局限性。
假设有两位开发者从服务器克隆了代码,并各自做出修改。第一个开发者可以顺利地将自己的更改推送回服务器。但当第二个开发者尝试推送时,如果他的更改与第一个开发者的改动存在冲突,他必须先拉取并合并第一个开发者的修改,然后才能继续提交,以避免覆盖他人的工作。
分布式版本控制
分布式版本控制(Version Control)采取了完全不同的方式。每个开发者的计算机上都存有完整的代码库副本,包括完整的历史记录。这带来了诸多优势:
- 本地操作更快:大多数操作(如提交、查看历史记录等)都可以在本地完成,无需依赖网络。
- 支持离线工作:即使无法访问服务器,也可以进行提交、创建分支等操作,待恢复联网后再同步至远程仓库。
- 避免单点故障:因为每个开发者的本地仓库都包含完整的历史记录,即使中央服务器宕机,仍然可以恢复数据。
- 更强的分支管理能力:Git的分支和合并操作远比集中式系统更高效,并且无需与服务器交互即可完成。
换句话说,分布式意味着没有单一的中央服务器作为数据的唯一来源。在Git的世界里,每个开发者的本地仓库都是完整的,每个人都拥有对历史记录的独立控制权。
“可能是过去十年中软件开发技术的最大进步。” — Joel Spolsky
Git 仓库、数据类型与对象数据库
谈及 Git,我们通常会将其与一个项目的根目录联系在一起。Git 负责跟踪该目录中的所有内容,而这个目录也被称为 Git 仓库(Repository)。在 Git 仓库内,有一个隐藏的 .git
目录,它保存了所有的 Git 对象 和 引用,是 Git 运行的核心数据存储。
在 Git 中,有 四种主要的对象类型,其中前三种是理解 Git 主要功能的关键。这些对象被存储在 Git 对象数据库 中,而该数据库正是 .git
目录的一部分。
值得注意的是,每个 Git 对象都经过 Zlib 压缩,并通过其内容及一个小型头部数据计算出的 SHA-1 哈希值 进行引用。这种设计确保了内容的唯一性——即便是最微小的变更,也会导致完全不同的 SHA-1 哈希值。这意味着 Git 具有极高的数据完整性与安全性,因为任何改动都会被精准地识别。
SHA-1(Secure Hash Algorithm 1)是一种安全哈希算法,它会对输入数据生成一个固定长度的哈希值,从而唯一标识该内容。更多信息可参考 维基百科。
Blob(数据块)
在 Git 中,文件的内容被存储为 Blob(二进制大对象)。一个需要特别注意的点是:Git 只存储和跟踪文件的内容,而不会记录文件名、文件权限或目录结构。换句话说,Git 追踪的是数据,而非文件本身。
一个有趣的事实是:在 Git 中,你无法直接跟踪一个文件,你只能跟踪其内容的快照。
那么,Blob 实际上是如何工作的呢?Git 读取文件内容,使用 Zlib::Deflate
进行压缩,然后计算其 SHA-1 哈希值。最后,Git 以该哈希值作为键,将 Blob 存储到 .git/objects/
目录中。
这种存储方式带来了一个极大的优势:如果两个不同的文件具有相同的内容,即使它们的文件名不同,Git 也只会存储一次该内容,并在多个地方重复使用该 Blob。Git 的低级命令 hash-object
正是用于此操作。
提示: 如果使用
-w
参数,Git 会将该哈希对象真正存储到 Git 仓库中。
提示: 你可以使用
git cat-file -p <SHA-1>
命令,从 Git 数据库中查看指定哈希对象的内容。例如,git cat-file -p 52c0e8
将会返回该对象所存储的文件内容。
当你在 Git 仓库中存放文件时,它们会被 Git 以 Blob 形式存储,如下所示:
术语说明:
工作区(Working Tree) 指的是与 Git 仓库相关联的文件系统目录,其中包含项目文件和.git
目录。
Tree(树对象)
在 Git 中,目录的概念对应于 Tree(树) 对象。
一个 Tree 对象实际上就是该目录下 所有 Blob 和子目录(子树)的列表,并且包含这些条目的 文件模式、类型、名称以及 SHA-1 哈希值。这类似于文件系统中的目录结构。
如果我们在 Git 仓库中构造一个简单的目录结构,可以用 Tree 对象来表示:
Tree 对象将不同的 Blob 组织起来,形成目录层级结构,为 Git 存储快照提供了基础。
Commit(提交对象)
Git 之所以被称为“版本控制系统”,是因为它不仅存储文件内容,还记录 文件的历史变更。这个历史信息由 Commit(提交对象) 负责管理。
每当你提交更改时,Git 都会创建一个新的 Commit,它记录了该时刻的 完整仓库快照,而不仅仅是单个文件的变更。
Commit 对象结构相对简单,它包含以下信息:
- 指向某个 Tree(表示该提交时的项目目录结构)
- 作者(Author)
- 提交者(Committer)
- 提交信息(Message)
- 父提交(Parent Commit),即该提交之前的状态
通常,每次提交都有一个 父提交,但如果发生 合并(Merge),则可能会有 多个父提交。而如果这是仓库的 第一个提交,则没有父提交。
Tag(标签对象)
Git 还提供了一种 标签(Tag) 机制,允许你为某个提交创建一个 固定的、易读的标识,类似于里程碑。
Tag 对象包含以下信息:
- 指向的对象(通常是一个提交)
- 对象类型(一般是 Commit)
- 标签名称
- 标签创建者
- 标签信息
Tag 主要用于 标记软件版本,并支持 GPG 签名,可用于验证版本的完整性。
提示: 你可以使用
git cat-file
命令查看一个标签对象的内容,例如:> git cat-file -p <tag-SHA1> > ```  ### Git 引用(Refs) 与 Git 对象不同,**Git 引用(Reference)是可变的**。引用是指向某个 **特定提交** 的简单指针,类似于 **标签(Tag)**,但它们可以随时更改。例如: - `HEAD` 指向当前分支的最新提交 - `refs/heads/main` 指向 `main` 分支的最新提交 在 Git 中,**对象不可变,而引用可以改变**。这种机制使 Git 能够高效管理分支、合并和版本演进。 ## Git 分支与远程仓库 在 Git 中,最重要的引用概念之一就是 **分支** 和 **远程**。本质上,Git 的分支只是 `.git/refs/heads/` 目录中的一个文件,内部存储着该分支最近提交的 SHA-1 哈希值。当创建新的分支时,Git 仅仅是在该目录中生成一个新文件,并指向相同的 SHA-1。随着提交的推进,其中一个分支会不断更新,指向最新的提交 SHA-1,而另一个分支则可能保持不变。 基于上述概念,我们可以把 Git 的基本数据模型简单概括如下:Git 由 **对象**(如提交、树、Blob)和 **引用**(如分支、远程)组成。  ### Git 仓库结构 在 Git 仓库中,**HEAD 是一个特殊的符号引用**,它始终指向当前检出的分支。通常,HEAD 应该指向某个分支,但如果直接检出某个提交(而非分支),**HEAD 就会进入“分离状态”**,直接指向该提交,而不关联任何分支。 一个 Git 仓库至少包含一个分支,该分支指向某个提交,而提交又指向一棵树,这棵树进一步引用多个子树和 Blob,形成类似 **有向无环图(DAG)** 的结构。  #### 记录更改 假设我们对某些文件进行了修改,并尝试提交更改。Git 会创建一个新的提交,并指向一个包含更新后内容的 **树(tree)**,而未修改的 Blob 仍然保持不变。此时,分支也会更新,指向最新的提交。  随着提交的积累,整个 Git 结构逐步演变,我们也可以在任何时间点 **为提交创建标签**。不同于分支,标签是永久性的,并不会随着新的提交而移动。  ### Git 的安全性 Git 通过 SHA-1 哈希值对所有对象进行引用,因此它的结构具有高度的安全性。如果某个文件被篡改,不仅该文件的 SHA-1 会改变,所有引用它的对象(包括提交、树等)也需要更新,最终影响整个 Git 历史。这种机制确保了 Git **防篡改**,也使其成为可靠的版本控制系统。 > **类似区块链**:如果你听说过区块链,它的工作原理与 Git 类似。区块链中的每个区块都包含前一个区块的哈希引用,因此一旦篡改数据,整个链条都会失效。 --- ### Git 如何检索对象 Git 通过 `.git/refs` 目录存储的 **分支、标签或远程引用** 来找到对应的 SHA-1 哈希值。然后,它会沿着这些引用 **遍历对象**,依次检索提交、树和 Blob,并最终恢复整个项目的文件内容。  这一机制不仅高效,还确保了完整性。值得注意的是,在 Git 中,**创建分支是一个极其廉价的操作**,因为它仅仅是创建了一个存储 SHA-1 哈希值的文件,而不是复制整个目录的内容。 > **Git vs 传统版本控制工具**: > 许多传统的版本控制系统在创建分支时,会复制整个项目文件,这可能需要几秒甚至几分钟;而 Git 仅仅是创建一个指向提交的引用,因此几乎是瞬间完成的。 --- ## Git 基础命令 Git 并不会直接从 **工作目录(Working Tree)** 提交更改到 **仓库(Repository)**。相反,它采用 **暂存区(Index / Staging Area)** 作为中间缓冲,确保开发者可以有选择地提交文件,而不是一股脑提交所有改动。 ### 1. `git add` – 添加到暂存区 该命令用于将文件的修改添加到暂存区,标记为**即将提交的更改**。 ```bash git add <file> ## 添加指定文件到暂存区 git add <directory> ## 添加整个目录到暂存区 git status ## 查看暂存区状态
在 Git 逻辑中,git add
只是将修改添加到暂存区,并不会真正提交,只有执行 git commit
之后,改动才会正式记录。
2. git commit
– 提交更改
一旦更改被 git add
添加到暂存区,就可以用 git commit
进行提交。
git commit -m "提交说明"
每次提交都会生成一个唯一的 SHA-1 哈希,并保留一个不可变的快照。Git 不会修改历史提交,除非开发者明确执行某些重写历史的操作(如 git rebase
)。
3. git log
– 查看提交历史
git log
此命令可以查看 Git 项目的完整历史记录,并支持各种筛选、格式化选项。
推荐工具:Tig 是一个更直观的 Git 日志浏览工具,可在终端中可视化查看提交历史。
4. git checkout
– 切换分支
git checkout <branchname> ## 切换到某个分支
git checkout -b <new_branch> ## 创建并切换到新分支
git checkout <commit_id> ## 切换到某个历史提交(进入分离状态)
git checkout
既可以用于切换分支,也可以用于检出历史提交。当切换到某个历史提交时,HEAD 会进入 “分离状态”,此时的修改不会被保留到某个分支中,除非创建新的分支。
5. git reset
– 撤销更改
git reset
主要用于撤销暂存或提交的更改。
git reset HEAD~3 ## 回退到前三个提交(保留文件修改)
git reset --hard HEAD~3 ## 彻底回退到前三个提交(丢弃文件修改)
git reset --soft HEAD~3 ## 仅回退提交,但保留暂存区的修改
6. git merge
vs git rebase
这两种方式都用于合并不同分支的更改,但方式有所不同:
git merge
直接创建一个合并提交,保持原始提交历史git rebase
通过重新应用提交,使分支历史保持线性
git checkout feature
git merge master ## 传统合并
git rebase master ## 变基
结论
本篇文章介绍了 Git 的核心概念,包括数据结构、提交管理和基础命令。在接下来的文章中,我们将深入探讨 Git 远程仓库、协作开发流程以及 GitHub 最佳实践,敬请期待!
下一部分即将发布!(译者注:作者没有发布第二部分) 🎉
参考资料:
以下是激发我撰写这篇文章的参考资料链接。我认为很多人做了大量工作,不仅使Git成为一个强大的工具链,还提供了非常全面的指南和教程,帮助我们有效地使用Git。
https://medium.com/swlh/git-from-the-bits-and-pieces-beyond-the-basics-part-1-aca2d02d360b
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。
文章由技术书栈整理,本文链接:https://study.disign.me/article/202510/5.git-from-beginner-to-master.md
发布时间: 2025-03-03