git 学习大纲

架构及操作

我们来深入了解 Git 的架构和常用操作。

Git 是一个分布式版本控制系统 (Distributed Version Control System, DVCS)。这意味着每个开发者都拥有代码仓库的完整副本,而不仅仅是工作文件的副本。这与集中式版本控制系统(如 SVN、CVS)有本质区别。


一、Git 架构 (核心概念)

理解 Git 的架构,关键在于理解它的三个区域(Three States)数据存储方式

1. 三个区域 (Three States)

Git 将文件分为三个主要状态或区域,这是 Git 工作流的核心:

  • 工作目录 (Working Directory / Working Tree):

    • 你实际在电脑上看到和编辑的文件。

    • 这是你当前项目版本的实际文件副本。

    • 文件处于“未跟踪 (untracked)”、“已修改 (modified)”或“未修改 (unmodified)”状态。

  • 暂存区 (Staging Area / Index):

    • 一个轻量级的文件,通常位于 .git 目录中,它记录了你下次提交(commit)时将要包含的更改。

    • 你可以将工作目录中已修改的文件添加到暂存区,准备提交。

    • 它像一个“草稿箱”或“待提交列表”,允许你选择性地提交部分更改。

  • 本地仓库 (Local Repository / .git Directory):

    • 这是 Git 存储所有版本历史、提交对象、分支、标签等元数据的地方。

    • 当你执行 git commit 命令时,暂存区中的内容会被永久保存到本地仓库中,形成一个新的提交对象。

    • 这个目录是 Git 魔法发生的地方,包含了项目的所有历史信息。

工作流程简述:

  1. 工作目录中修改文件。

  2. 使用 git add 将修改后的文件从工作目录添加到暂存区

  3. 使用 git commit 将暂存区中的内容作为一个新的版本(提交)保存到本地仓库

2. 数据存储方式 (快照而非差异)

Git 的一个核心特点是它存储的是快照 (snapshots),而不是文件之间的差异 (diffs)。

  • 快照: 每当你提交时,Git 会对你项目中的所有文件(在暂存区中的状态)创建一个完整的快照,并存储一个指向这个快照的引用。如果文件没有改变,Git 不会重新存储它,而是存储一个指向之前已存储文件的链接。

  • 内容寻址 (Content-Addressable): Git 的所有数据都通过其内容的 SHA-1 哈希值来引用。这意味着任何内容的更改都会导致其哈希值改变,从而生成新的对象。这保证了数据的完整性和不可篡改性。

3. Git 对象 (Objects)

Git 仓库中的所有数据都以四种基本对象类型存储在 .git/objects 目录下:

  • Blob (二进制大对象): 存储文件的内容。每个文件版本对应一个 Blob 对象。

  • Tree (树对象): 存储目录结构和文件(Blob)的引用。一个 Tree 对象可以包含多个 Blob 和其他 Tree 对象。

  • Commit (提交对象): 存储一个特定时间点的项目快照。它包含:

    • 指向一个 Tree 对象的指针(代表项目根目录的快照)。

    • 作者信息、提交者信息。

    • 提交消息。

    • 一个或多个父提交的指针(通常是一个,合并提交会有多个)。

  • Tag (标签对象): 存储一个指向特定提交的引用,通常用于标记重要的版本(如发布版本)。

4. 引用 (References / Refs)

Git 使用引用来指向提交对象,方便人类记忆和操作:

  • 分支 (Branches): 只是一个指向某个提交的可变指针。当你提交时,当前分支的指针会自动向前移动。

  • 标签 (Tags): 类似于分支,但它们是不可变的指针,一旦创建就固定指向某个提交,通常用于标记发布版本。

  • HEAD: 一个特殊的指针,它指向你当前所在的分支(或直接指向某个提交,如果处于“分离头指针”状态)。它代表你当前工作目录所基于的提交。

5. 远程仓库 (Remote Repositories)

  • 远程仓库是托管在网络上的 Git 仓库(如 GitHub, GitLab, Bitbucket)。

  • 它们用于团队协作,允许开发者之间共享代码。

  • origin 是 Git 默认给克隆的远程仓库起的别名。


二、Git 常用操作 (命令)

以下是 Git 最常用的一些命令,涵盖了从初始化到协作的各个方面:

1. 初始化与克隆

  • git init:

    • 作用: 在当前目录创建一个新的 Git 仓库。它会生成一个 .git 子目录,用于存储所有版本控制信息。

    • 示例: git init

  • git clone <repository_url>:

    • 作用: 克隆一个远程仓库到本地。这会下载远程仓库的所有历史记录,并自动设置一个名为 origin 的远程别名。

    • 示例: git clone https://github.com/user/repo.git

2. 基本工作流程 (修改、暂存、提交)

  • git status:

    • 作用: 查看工作目录和暂存区的状态。它会告诉你哪些文件已修改、哪些已暂存、哪些是新文件未被跟踪等。

    • 示例: git status

  • git add <file_name> / git add .:

    • 作用: 将文件或目录的更改添加到暂存区。

      • git add <file_name>: 添加指定文件。

      • git add .: 添加当前目录下所有未跟踪和已修改的文件。

    • 示例: git add index.htmlgit add .

  • git commit -m "Commit message":

    • 作用: 将暂存区中的所有更改作为一个新的提交永久保存到本地仓库。必须提供一个有意义的提交消息。

    • 示例: git commit -m "feat: Add user login functionality"

  • git diff:

    • 作用: 查看文件修改的差异。

      • git diff: 查看工作目录中未暂存的更改。

      • git diff --stagedgit diff --cached: 查看暂存区中已暂存但未提交的更改。

    • 示例: git diff

3. 查看历史记录

  • git log:

    • 作用: 查看提交历史记录。默认按时间倒序显示。

    • 常用选项:

      • --oneline: 每条提交只显示一行简洁信息。

      • --graph: 以图形方式显示分支合并历史。

      • --all: 显示所有分支的提交历史。

      • --decorate: 显示分支和标签的名称。

    • 示例: git log --oneline --graph --all

4. 分支管理

  • git branch:

    • 作用:

      • git branch: 列出所有本地分支。

      • git branch <new_branch_name>: 创建一个新分支。

    • 示例: git branch feature/new-feature

  • git checkout <branch_name> / git switch <branch_name>:

    • 作用: 切换到指定分支。git switch 是 Git 2.23+ 引入的更清晰的命令,专门用于切换分支。

    • 示例: git checkout developgit switch develop

  • git checkout -b <new_branch_name> / git switch -c <new_branch_name>:

    • 作用: 创建并立即切换到新分支。

    • 示例: git checkout -b hotfix/bug-fix

  • git merge <branch_to_merge>:

    • 作用: 将指定分支的更改合并到当前分支。

    • 示例: git merge feature/new-feature (将 feature/new-feature 合并到当前分支)

  • git branch -d <branch_name>:

    • 作用: 删除一个已合并的本地分支。

    • 示例: git branch -d feature/new-feature

5. 远程协作

  • git remote -v:

    • 作用: 查看已配置的远程仓库。

    • 示例: git remote -v

  • git fetch <remote_name>:

    • 作用: 从远程仓库下载最新的提交和分支信息,但不合并到你的本地分支。这些信息会存储在 remote/<remote_name>/<branch_name> 这样的远程跟踪分支中。

    • 示例: git fetch origin

  • git pull <remote_name> <branch_name>:

    • 作用: 从远程仓库下载最新更改并自动合并到当前本地分支。相当于 git fetch 后再 git merge

    • 示例: git pull origin main

  • git push <remote_name> <branch_name>:

    • 作用: 将本地分支的提交推送到远程仓库。

    • 示例: git push origin main

  • git push -u <remote_name> <branch_name>:

    • 作用: 首次推送新分支时,设置上游(upstream)跟踪,这样以后只需 git push 即可。

    • 示例: git push -u origin feature/my-feature

6. 撤销与修改

  • git reset <commit_hash>:

    • 作用: 移动当前分支的 HEAD 指针。

      • --soft: 移动 HEAD,保留工作目录和暂存区不变。

      • --mixed (默认): 移动 HEAD,清空暂存区,保留工作目录。

      • --hard: 移动 HEAD,清空暂存区,并重置工作目录到指定提交的状态(慎用,会丢失未提交的更改)。

    • 示例: git reset --hard HEAD~1 (回退一个提交)

  • git revert <commit_hash>:

    • 作用: 创建一个新的提交,用于撤销指定提交所引入的更改。它不会修改历史,而是添加一个新的“反向”提交。

    • 示例: git revert 06a15cd

  • git restore <file_name>:

    • 作用: 恢复文件。

      • git restore <file_name>: 撤销工作目录中对文件的修改(从暂存区或最新提交恢复)。

      • git restore --staged <file_name>: 将文件从暂存区移除,但保留工作目录中的修改。

    • 示例: git restore index.html

  • git commit --amend:

    • 作用: 修改上一次提交。可以修改提交消息,或添加/删除文件到上一次提交。

    • 示例: git commit --amend -m "Fix: Corrected typo in feature description"

  • git cherry-pick <commit_hash>:

    • 作用: 将指定提交的更改应用到当前分支,并创建一个新的提交。

    • 示例: git cherry-pick 06a15cd

7. 临时保存更改

  • git stash:

    • 作用: 临时保存工作目录和暂存区中未提交的更改,使工作目录回到干净状态。

    • 示例: git stash save "Work in progress"

  • git stash pop:

    • 作用: 恢复最近一次保存的暂存,并从暂存列表中删除。

    • 示例: git stash pop

  • git stash list:

    • 作用: 列出所有暂存的更改。

    • 示例: git stash list

8. 标签 (Tags)

  • git tag <tag_name>:

    • 作用: 为当前提交打一个轻量级标签。

    • 示例: git tag v1.0

  • git tag -a <tag_name> -m "Message":

    • 作用: 为当前提交打一个附注标签(推荐),包含标签信息和签名。

    • 示例: git tag -a v1.0 -m "Release version 1.0"

  • git push <remote_name> --tags:

    • 作用: 将所有本地标签推送到远程仓库。

    • 示例: git push origin --tags


Git 的强大之处在于其灵活性和分布式特性,但这也意味着它有更多的概念和命令需要学习。熟练掌握这些核心概念和常用操作,将大大提高你的开发效率和团队协作能力。

三个区域

好的,我们来详细展开分析 Git 的三个区域(Three States),这是理解 Git 工作原理和工作流的基石。

Git 将你的项目文件分为三个逻辑区域,它们共同构成了 Git 的核心工作循环:

  1. 工作目录 (Working Directory / Working Tree)

  2. 暂存区 (Staging Area / Index)

  3. 本地仓库 (Local Repository / .git Directory)


一、工作目录 (Working Directory / Working Tree)

  • 是什么?

    • 这是你电脑上实际可见、可编辑的项目文件和文件夹。当你 git clone 一个仓库或 git checkout 一个分支时,Git 会将对应版本的项目文件解压到这个目录。

    • 它是你进行日常开发、编写代码、修改文件的地方。

  • 在哪里?

    • 就是你项目所在的文件夹。例如,如果你克隆了一个名为 my-project 的仓库,那么 my-project 文件夹及其所有内容就是你的工作目录。
  • 作用/目的:

    • 实际操作区: 开发者在这里直接编辑、创建、删除文件。

    • 当前版本视图: 它反映了你当前正在处理的 Git 版本的实际文件状态。

  • 文件状态:

    • 未修改 (Unmodified): 文件内容与本地仓库中最新提交的版本完全一致。

    • 已修改 (Modified): 文件内容与本地仓库中最新提交的版本不同,你已经对其进行了更改。

    • 未跟踪 (Untracked): Git 还没有开始跟踪这些文件。它们是新创建的文件,或者你明确告诉 Git 忽略的文件(通过 .gitignore)。

  • 主要交互命令:

    • git status: 查看工作目录中文件的状态(已修改、未跟踪等)。

    • git add <file>: 将工作目录中已修改或未跟踪的文件添加到暂存区。

    • git restore <file>: 撤销工作目录中对文件的修改,使其回到暂存区或最新提交的状态。

  • 比喻: 想象你的办公桌。你正在桌上直接修改文件、写代码。这些文件是活的,可以随意更改,但它们还没有被正式记录下来。


二、暂存区 (Staging Area / Index)

  • 是什么?

    • 暂存区是一个轻量级的文件,通常被称为 "Index""Cache",它位于 .git 目录中(具体是 .git/index 文件)。

    • 它不是文件的完整副本,而是一个清单,记录了你下次提交时将要包含的文件的快照。它存储的是文件内容的 SHA-1 哈希值,以及文件路径、权限等元数据。

  • 在哪里?

    • 逻辑上位于工作目录和本地仓库之间。物理上是 .git/index 文件。
  • 作用/目的:

    • 精细化控制提交: 这是 Git 最独特和强大的功能之一。它允许你选择性地将工作目录中的部分更改添加到暂存区,而不是一次性提交所有更改。例如,你可以将一个文件中关于 Bug 修复的更改暂存,而将另一个文件中关于新功能的更改保留在工作目录中,以便分两次提交。

    • 提交前的预览区: 在提交之前,你可以在暂存区中“组装”你的提交内容,确保只包含你想要提交的更改。

    • 准备区: 它是工作目录到本地仓库的“中转站”。只有暂存区中的内容才能被提交到本地仓库。

  • 文件状态:

    • 已暂存 (Staged): 文件已添加到暂存区,准备好被提交。
  • 主要交互命令:

    • git add <file>: 将工作目录中的更改添加到暂存区。

    • git status: 查看哪些文件已暂存。

    • git diff --stagedgit diff --cached: 查看暂存区中与最新提交之间的差异。

    • git restore --staged <file>: 将文件从暂存区移除,但保留工作目录中的修改。

    • git commit: 将暂存区中的内容作为一个新的提交保存到本地仓库。

  • 比喻: 想象你的草稿箱待提交清单。你从办公桌上挑选出你认为已经完成、可以作为下一个正式版本一部分的修改,把它们放进这个草稿箱。你可以反复调整草稿箱里的内容,直到满意为止。


三、本地仓库 (Local Repository / .git Directory)

  • 是什么?

    • 这是 Git 存储所有版本历史、提交对象、分支、标签、配置等元数据的地方。它是 Git 真正的“数据库”。

    • 当你执行 git commit 命令时,暂存区中的内容会被永久保存到本地仓库中,形成一个新的提交对象。

  • 在哪里?

    • 在你的项目根目录下有一个隐藏的 .git 文件夹。这个文件夹就是你的本地仓库。
  • 作用/目的:

    • 版本历史存储: 存储了项目从开始到现在的每一个提交(版本)的完整历史记录。

    • 数据完整性: Git 使用 SHA-1 哈希值来标识和校验所有对象,确保历史记录的不可篡改性。

    • 离线操作: 由于每个本地仓库都包含完整的历史,你可以在没有网络连接的情况下进行提交、分支、合并等操作。

    • 协作基础: 它是你与远程仓库(如 GitHub)进行 pushpull 操作的基础。

  • 文件状态:

    • 已提交 (Committed): 文件内容已作为历史版本的一部分被永久保存。这些版本是不可变的快照。
  • 主要交互命令:

    • git commit: 将暂存区的内容保存为新的提交。

    • git log: 查看提交历史。

    • git checkout <commit_hash/branch_name>: 切换到历史版本或分支。

    • git reset: 回溯历史,修改 HEAD 指针。

    • git branch: 管理分支。

    • git merge: 合并分支。

    • git push: 将本地提交推送到远程仓库。

    • git pull: 从远程仓库拉取并合并最新提交。

  • 比喻: 想象你的档案室图书馆。一旦你把草稿箱里的内容整理好,并决定正式发布,它就会被永久地归档到这个档案室里,成为一个有编号、有记录的正式版本。你可以随时查阅任何一个历史版本。


三个区域的交互流程总结:

  1. 修改 (Modify): 你在工作目录中对文件进行修改。此时文件处于“已修改”状态。

  2. 暂存 (Stage): 你使用 git add 命令将工作目录中你希望包含在下一次提交中的更改,从工作目录移动到暂存区。此时文件处于“已暂存”状态。

  3. 提交 (Commit): 你使用 git commit 命令将暂存区中的所有内容作为一个新的快照,永久地保存到本地仓库中。此时文件处于“已提交”状态,并且工作目录中的对应文件也回到了“未修改”状态(因为它们现在与最新提交的内容一致)。

这个“修改 -> 暂存 -> 提交”的循环是 Git 最核心的工作流。理解这三个区域及其相互作用,是掌握 Git 的关键。它赋予了开发者极大的灵活性和控制力,能够精确地管理每次提交的内容。

保存文件的核心逻辑

Git 保存文件的核心逻辑是其最精妙和独特之处,它与传统的版本控制系统(如 SVN)有着根本性的不同。其核心可以概括为:快照而非差异、内容寻址、以及基于对象的存储模型。


Git 保存文件的核心逻辑:快照、内容寻址与对象模型

1. 核心原则:快照而非差异 (Snapshots, Not Diffs)

  • 传统 VCS (如 SVN) 的方式: 大多数传统版本控制系统存储的是文件之间的差异 (deltas)。它们会存储文件的初始版本,然后记录每次修改相对于前一个版本的变化。

  • Git 的方式: Git 不存储差异,而是存储快照 (snapshots)。每当你执行 git commit 命令时,Git 会对你项目中的所有文件(在暂存区中的状态)创建一个完整的快照,并存储一个指向这个快照的引用。

    • 效率问题? 你可能会想,如果每次都存储所有文件的完整快照,那岂不是很占用空间?Git 通过智能的去重机制解决了这个问题。如果一个文件在两次提交之间没有发生变化,Git 不会重新存储它,而是存储一个指向之前已存储文件的链接。只有发生变化的文件才会被存储新的快照。

2. 内容寻址 (Content-Addressable Storage)

  • 核心思想: Git 的所有数据都通过其内容的 SHA-1 哈希值来引用。这意味着任何内容的更改都会导致其哈希值改变,从而生成新的对象。

  • 如何实现: 当你向 Git 仓库添加任何数据(文件内容、目录结构、提交信息等)时,Git 会计算该内容的 SHA-1 哈希值。这个哈希值就是该数据在 Git 内部的唯一标识符。

  • 优点:

    • 数据完整性: 任何对历史数据的篡改都会导致哈希值不匹配,从而立即被发现。

    • 不可变性: 一旦数据被存储,它的哈希值就确定了,内容也就不可更改。

    • 去重: 如果两个文件内容完全相同,即使它们在不同的路径或不同的提交中,它们也会共享同一个 Blob 对象,从而节省存储空间。

3. 基于对象的存储模型 (Object Model)

Git 仓库(.git 目录)的内部是一个简单的键值对数据库,其中键是 SHA-1 哈希值,值是四种基本类型的 Git 对象:

a. Blob (二进制大对象)
  • 存储内容: 存储文件的实际内容。它只关心文件的数据,不包含文件名、路径或权限信息。

  • 特点: 每个 Blob 对象对应一个文件的一个版本。如果文件内容相同,即使文件名不同,它们也会指向同一个 Blob 对象。

  • 比喻: 就像图书馆里的一本书的内容,不关心书名或放在哪个书架上。

b. Tree (树对象)
  • 存储内容: 存储目录结构和文件(Blob)的引用。一个 Tree 对象可以包含:

    • 指向 Blob 对象的指针(代表文件),并记录文件名和文件权限。

    • 指向其他 Tree 对象的指针(代表子目录),并记录子目录名。

  • 特点: 它将文件名、目录结构和文件内容(通过 Blob 引用)关联起来。每个 Tree 对象代表一个目录在某个时间点的快照。

  • 比喻: 就像图书馆的目录卡片,记录了书名、作者、以及这本书在哪个书架(子目录)上,或者直接指向一本书的内容(Blob)。

c. Commit (提交对象)
  • 存储内容: 存储一个特定时间点的项目完整快照。它是 Git 历史记录的基本单元。

  • 包含信息:

    • 根 Tree 对象的指针: 指向一个 Tree 对象,这个 Tree 对象代表了整个项目在提交时的根目录快照。

    • 父提交的指针: 通常指向一个父提交(普通提交),或者多个父提交(合并提交)。这构成了 Git 的提交历史链。

    • 作者信息: 提交者的姓名和邮箱。

    • 提交者信息: 实际执行提交操作的人的姓名和邮箱(可能与作者不同,例如在 Cherry-pick 时)。

    • 提交消息: 描述本次提交目的的文本。

  • 特点: 每次 git commit 都会创建一个新的 Commit 对象。通过 Commit 对象,Git 可以追溯到项目在任何一个提交时的完整状态。

  • 比喻: 就像图书馆的借阅记录,记录了你在某个时间点借走了哪些书(通过 Tree 对象指向所有文件快照),谁借的,什么时候借的,以及上次是谁借的(父提交)。

d. Tag (标签对象)
  • 存储内容: 存储一个指向特定提交的引用,通常用于标记重要的版本(如发布版本)。

  • 特点: 标签是不可变的,一旦创建就固定指向某个提交。

  • 比喻: 就像图书馆里给某些特别重要的书贴上的永久性标签,方便快速找到。

4. 引用 (References / Refs)

  • 是什么: Git 使用引用(如 HEAD、分支名、标签名)来指向 Commit 对象。这些引用是人类可读的名称,方便我们操作。

  • HEAD 一个特殊的引用,它指向你当前所在的分支(或直接指向某个提交,如果处于“分离头指针”状态)。它代表你当前工作目录所基于的提交。

  • 分支 (Branches): 只是一个指向某个 Commit 的可变指针。当你提交时,当前分支的指针会自动向前移动到新的 Commit。

  • 标签 (Tags): 类似于分支,但它们是不可变指针,一旦创建就固定指向某个 Commit。


提交 (Commit) 过程的内部逻辑串联

当你执行 git commit 时,Git 内部会发生以下核心步骤:

  1. 创建 Blob 对象: 对于暂存区中所有已修改或新增的文件,Git 会计算它们内容的 SHA-1 哈希值,并将文件内容作为 Blob 对象存储到 .git/objects 目录中。如果文件内容没有变化,则不会创建新的 Blob,而是重用已有的 Blob。

  2. 创建 Tree 对象: Git 会根据暂存区中文件的当前状态和目录结构,递归地创建 Tree 对象。

    • 每个子目录会对应一个 Tree 对象。

    • 根目录也会对应一个最终的 Tree 对象,它包含了所有文件和子目录的引用(通过 Blob 和其他 Tree 对象的哈希值)。

  3. 创建 Commit 对象: Git 会创建一个新的 Commit 对象,其中包含:

    • 指向刚刚创建的根 Tree 对象的指针(代表了整个项目的快照)。

    • 指向当前分支上一个 Commit 对象的指针(作为父提交)。

    • 作者、提交者信息和提交消息。

  4. 更新引用: 最后,Git 会将当前分支的引用(例如 maindevelop)从旧的 Commit 对象移动到这个新创建的 Commit 对象。同时,HEAD 指针(如果它指向当前分支)也会随之更新。

总结来说,Git 保存文件的核心逻辑是:

  • 不存储文件本身,而是存储文件的内容(Blob)。

  • 不存储目录本身,而是存储目录的结构(Tree),通过 Tree 将文件名、权限与 Blob 关联起来。

  • 不存储每次修改的差异,而是存储每次提交时整个项目的完整快照(通过 Commit 对象指向根 Tree)。

  • 所有数据都通过其内容的 SHA-1 哈希值进行唯一标识和寻址,确保数据完整性和去重。

  • 通过分支和标签等引用,方便地指向和管理这些提交快照。

这种设计使得 Git 极其高效、健壮,并且非常适合分布式协作。