Git 从入门到精通

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了。

注意: 本文中的部分内容借鉴了开源社区的资源和灵感,我个人无法制作出如此精美的插图。所有的功劳归于原作者,我在文章末尾的参考部分中列出了相关信息。(所有引用内容均经过授权并符合使用条款。)

版本控制技术

那么,什么是版本控制?简单来说,它就是在创建和编辑代码或内容的过程中,记录所有变更,并支持回溯和协作。版本控制的核心目标,是让多个开发者或团队成员能够高效协作,而不会相互干扰。

目前,主流的版本控制方式主要分为两类:

  1. 集中式版本控制(Centralized Version Control,CVCS):所有操作都依赖于一个中央服务器,开发者必须与该服务器同步。
  2. 分布式版本控制(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>
> ```

![](https://study-cdn.disign.me/images/202510/8c73b5cee440cf9e6263003abcf74b09.webp)

### 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)和 **引用**(如分支、远程)组成。

![](https://study-cdn.disign.me/images/202510/f54165c69bd9eebfa03cd51972cfed2c.webp)

### Git 仓库结构

在 Git 仓库中,**HEAD 是一个特殊的符号引用**,它始终指向当前检出的分支。通常,HEAD 应该指向某个分支,但如果直接检出某个提交(而非分支),**HEAD 就会进入“分离状态”**,直接指向该提交,而不关联任何分支。

一个 Git 仓库至少包含一个分支,该分支指向某个提交,而提交又指向一棵树,这棵树进一步引用多个子树和 Blob,形成类似 **有向无环图(DAG)** 的结构。

![](https://study-cdn.disign.me/images/202510/169496ec6b4e5e96e07d4dae9edc81c4.webp)

#### 记录更改

假设我们对某些文件进行了修改,并尝试提交更改。Git 会创建一个新的提交,并指向一个包含更新后内容的 **树(tree)**,而未修改的 Blob 仍然保持不变。此时,分支也会更新,指向最新的提交。

![](https://study-cdn.disign.me/images/202510/dc34c9ef8e2ed786aaebf208eab44c2d.webp)

随着提交的积累,整个 Git 结构逐步演变,我们也可以在任何时间点 **为提交创建标签**。不同于分支,标签是永久性的,并不会随着新的提交而移动。

![](https://study-cdn.disign.me/images/202510/89f1d92ab69856d8d7a288c5c859f50b.webp)

### Git 的安全性

Git 通过 SHA-1 哈希值对所有对象进行引用,因此它的结构具有高度的安全性。如果某个文件被篡改,不仅该文件的 SHA-1 会改变,所有引用它的对象(包括提交、树等)也需要更新,最终影响整个 Git 历史。这种机制确保了 Git **防篡改**,也使其成为可靠的版本控制系统。

> **类似区块链**:如果你听说过区块链,它的工作原理与 Git 类似。区块链中的每个区块都包含前一个区块的哈希引用,因此一旦篡改数据,整个链条都会失效。

---

### Git 如何检索对象

Git 通过 `.git/refs` 目录存储的 **分支、标签或远程引用** 来找到对应的 SHA-1 哈希值。然后,它会沿着这些引用 **遍历对象**,依次检索提交、树和 Blob,并最终恢复整个项目的文件内容。

![](https://study-cdn.disign.me/images/202510/238bc3fdc59b72b438e8bde23bdf960b.webp)

这一机制不仅高效,还确保了完整性。值得注意的是,在 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