什么是Git?

Git是一个分布式版本控制系统,由Linux内核创始人Linus Torvalds于2005年开发。相较于集中式版本控制系统,Git的分布式特性使得团队成员可以在本地独立地进行代码开发,不再依赖于中央服务器的稳定性和网络连接。这为团队协作带来了巨大的便利,同时也为个人开发者提供了强大的版本管理能力。

在本篇博客中,我们将深入探讨Git的使用方法,带你了解Git如何帮助你高效地管理代码版本。无论你是刚刚接触Git还是想深入了解其高级功能,这篇指南都将为你提供全面的帮助。

接下来,我们将一步步学习如何安装和配置Git,并介绍Git的基本概念,例如仓库、提交、分支等。随后,我们将探讨常用的Git工作流程,涵盖从创建新功能分支到合并代码的全过程。同时,我们还将了解如何处理代码冲突、回滚错误提交以及优化Git操作,使你在日常开发中更加游刃有余。

Git是一个功能强大而灵活的工具,但也可能会让初学者感到有些复杂。但不用担心,本篇博客将以简洁明了的语言,结合实例演示,带你逐步掌握Git的精髓。让我们一起开始,成为Git版本控制的高手吧!

基本流程:Git工作流

当学习Git时,建议按照以下顺序逐步掌握各个命令:

  1. git init:创建一个新的空白仓库或在现有目录中初始化一个Git仓库。

  2. git add:将文件添加到暂存区,准备进行提交。

  3. git commit:将暂存区中的文件提交到本地仓库,创建一个新的版本。

  4. git status:查看当前仓库状态,包括已修改和未跟踪的文件。

  5. git diff:查看未暂存文件与最后一次提交之间的差异。

  6. git log:查看提交历史,包括提交者、日期和提交信息。

  7. git checkout:切换分支或恢复文件,可以用于创建新分支。

  8. git branch:查看、创建和删除分支。

  9. git merge:将一个分支的更改合并到另一个分支。

  10. git remote:连接远程仓库,可以添加、重命名和删除远程仓库。

  11. git pull:从远程仓库拉取并合并更改到本地仓库。

  12. git push:将本地仓库的更改推送到远程仓库。

  13. git clone:克隆远程仓库到本地,用于初始化一个新的本地仓库。

  14. git fetch:从远程仓库拉取代码,但不合并到本地仓库。

  15. git reset:撤销提交,将HEAD指向之前的提交,并可选择保留或丢弃更改。

  16. git revert:撤销指定的提交,并创建一个新的提交来反转更改。

  17. git stash:将当前工作目录的更改保存在临时区域,以便稍后恢复。

  18. git tag:创建、列出和删除标签,用于标记重要的提交或版本。

基本用法:案例

步骤1:初始化仓库和第一个提交

首先,我们创建一个新的空目录,进入该目录,并初始化Git仓库。

1
2
3
mkdir my-web-app
cd my-web-app
git init

接下来,在该目录下创建一个简单的HTML文件。

1
2
3
4
5
6
7
8
9
10
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Web App</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

现在,将该文件添加到暂存区并提交第一个版本。

1
2
git add index.html
git commit -m "Initial commit: Created index.html"

步骤2:创建并切换分支

我们决定在项目中添加一个新的功能,为此,我们需要创建一个新的分支。

1
2
git branch feature/add-about-page
git checkout feature/add-about-page

或者可以使用一条命令来创建并切换分支:

1
git checkout -b feature/add-about-page

现在,我们在index.html文件中添加一个“关于”页面。

1
2
3
4
5
6
7
8
9
10
11
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Web App</title>
</head>
<body>
<h1>Hello, World!</h1>
<a href="about.html">About</a>
</body>
</html>

然后添加并提交这些更改。

1
2
git add index.html
git commit -m "Added About page link"

步骤3:合并分支

在添加了“关于”页面之后,我们决定将这个功能合并回主分支。

首先,切换回主分支。

1
git checkout main

然后将feature/add-about-page分支合并到main分支。

1
git merge feature/add-about-page

步骤4:部署项目

现在,我们已经完成了项目的开发并将功能合并回主分支。接下来,我们可以将项目部署到服务器或托管平台上。

这里假设我们将代码托管在GitHub上。首先,我们将远程仓库与本地仓库关联。

1
git remote add origin <GitHub仓库URL>

然后将代码推送到远程仓库。

1
git push -u origin main

至此,我们的简单Git案例就完成了,包括了从初始化仓库到创建和合并分支,并最后将代码推送到远程仓库的过程。

提交:commit 命名规范

遵循一致的命名规范可以使提交历史更加清晰、易于理解和管理。以下是一些建议的Git commit命名规范:

  1. 使用简洁明了的描述:每个commit的标题应该简洁明了地描述此次提交所做的更改。尽量避免冗长的标题。

  2. 使用现在时态:提交标题通常使用现在时来描述一个动作或更改,例如:”Add feature”, “Fix bug”等。

  3. 使用动词开头:在标题中使用动词开头,说明这次提交所做的主要操作。常见的动词有:”Add”(新增)、”Update”(更新)、”Fix”(修复)、”Remove”(删除)等。

  4. 分离主题和描述:提交标题与提交描述之间使用一个空行进行分隔,提交描述可以进一步补充说明本次提交的详细信息。

  5. 参考项目规范:如果你在一个团队或项目中工作,请遵循项目的提交规范。许多团队会有特定的提交规范,例如Angular的规范(Angular Commit Message Guidelines)。

  6. 使用标签:可以在标题中使用标签来更好地标识提交类型,如”[Feature]“, “[Bugfix]“, “[Docs]“, “[Refactor]“等。

可以参考以下案例:

  • 添加新功能:
    Add user authentication feature
  • 修复Bug:
    Fix login page alignment issue
  • 更新文档:
    Docs: Update README with installation instructions
  • 重构代码:
    Refactor: Improve performance of data processing

其中,commit的类型至关重要,能够将我们的提交很好地分门别类。

  1. feat(Feature):添加新功能或特性。
  2. fix(Bugfix):修复Bug或错误。
  3. docs(Documentation):更新文档,如README、注释等。
  4. style:对代码样式进行调整,如缩进、空格、格式等,不影响代码逻辑。
  5. refactor:重构代码,优化代码结构、提高性能,不是Bug修复,也不是添加新功能。
  6. test:添加或修改测试用例、测试代码。
  7. chore:对构建过程或辅助工具的修改。
  8. perf(Performance):改善代码性能的提交。
  9. build:修改项目构建相关的内容,如更新依赖、修改构建脚本等。
  10. revert:撤销之前的提交。
  11. merge:合并分支或解决合并冲突。
  12. init:初始化项目。
  13. dependencies:更新或修改项目依赖的外部库或工具。
  14. security:涉及安全性的提交,如修复安全漏洞。

然而,我们有时仍然会忘记,或者偷懒而没有遵循提交的规范,这会给后期查看代码带来麻烦。而Commitizen是一个用于规范化Git提交消息的工具,旨在帮助团队成员更好地遵循一致的提交规范。它提供了一个交互式的命令行界面,引导用户填写提交信息,并根据预定义的提交规范生成规范化的提交消息。使用方法如下:

  1. 安装:-g commitizen表示全局安装;cz-conventional-changelog为项目配置Commitizen适配器,最常用的适配器cz-conventional-changelog(约定式提交适配器):
    1
    npm install -g commitizen cz-conventional-changelog
  2. 在项目根目录下创建一个.czrc文件,指定使用的适配器:
    1
    2
    3
    {
    "path": "cz-conventional-changelog"
    }
  3. 使用git cz命令代替git commit来进行提交,然后按照交互式界面的提示填写提交信息。

命令详解:parameter-level explanation

GIT BRANCH: 分支操作

git branch 是用于管理Git分支的命令,用于查看、创建、删除、重命名和切换分支。以下是git branch命令的常用参数及其作用:

  1. 不带参数:

    1
    git branch

    作用:查看当前仓库中的所有分支,并在当前分支前面添加一个星号*标识当前所在的分支。

  2. 1
    git branch <branch_name>

    作用:创建一个新的分支,名称为<branch_name>

  3. -d / —delete

    1
    git branch -d <branch_name>

    作用:删除指定的分支,如果分支未合并到其他分支,则会警告;如果确定要删除,可以使用 -D 参数进行强制删除。

  4. -m / —move

    1
    git branch -m <old_branch_name> <new_branch_name>

    作用:重命名分支,将 <old_branch_name> 改为 <new_branch_name>

  5. -M:

    1
    git branch -M <name>

    作用:执行 git branch -M main 命令后,将会发生以下操作:你的默认分支将变成 main,并且原先的默认分支(如果存在)的更改将被保存在新的 main 分支上。

  6. 如果当前仓库的默认分支不是 main,Git 会将默认分支的名称更改为 main
  7. 如果 main 分支已经存在,则将当前所在的分支(当前分支)重命名为 main,并且该操作是强制性的(因此使用了 -M 参数)。

  8. -r / —remotes:

    1
    git branch -r

    作用:显示远程分支。

  9. -a / —all:

    1
    git branch -a

    作用:显示所有本地和远程分支。

  10. -v / —verbose:

    1
    git branch -v

    作用:显示每个分支的最后一次提交信息。

  11. -vv:

    1
    git branch -vv

    作用:显示每个分支的最后一次提交信息,并显示跟踪的远程分支。

  12. —no-color:

    1
    git branch --no-color

    作用:在输出中禁用颜色。

GIT ADD: 加入暂存

git add 是用于将工作目录中的文件更改添加到暂存区(Staging Area),准备提交到版本库。git add 命令有多个参数可以控制添加文件的方式。以下是常用的 git add 参数及其作用:

  1. 文件名:

    1
    git add <file>

    作用:将指定的文件 <file> 添加到暂存区。

  2. 目录名:

    1
    git add <directory>

    作用:将指定目录下的所有文件(包括子目录)添加到暂存区。

  3. -A / —all:

    1
    git add -A

    作用:将所有更改(包括新文件、修改和删除)添加到暂存区。相当于 git add .git add -u 的合并效果。

  4. -u / —update:

    1
    git add -u

    作用:将所有修改和新文件(不包括删除)添加到暂存区。不会添加新的未追踪文件。

  5. -p / —patch:

    1
    git add -p

    作用:交互式地选择要添加到暂存区的文件内容,可以选择部分修改并排除其他部分。

    1. y:表示将当前修改块添加到暂存区。
    2. n:表示不将当前修改块添加到暂存区。
    3. s:表示将当前修改块分割成更小的块。譬如一个文件中,修改了多次,可以将每一个不同的更改分开来选择添加或修改
    4. e:表示手动编辑当前修改块。
    5. q:表示退出 git add -p 命令。
    6. a:表示将当前的显示的修改添加到暂存区。由于多个文件中的修改是逐个显示的,可以理解为show next one;
  6. -i / —interactive:

    1
    git add -i

    作用:以交互式的方式添加文件,可以选择添加、忽略或取消添加。

    其中, 第一个表格展示了修改的状态, Commands展示了可输入的命令,可以通过输入序号首字母来调用该命令。

    我们修改了test2,随后unstaged栏有更新, 此时我们输入命令2u,命令前缀符变为Update,即命令名称时,会出现选项选择要操作的文件对象,输入1选中一个,随后回车选中这一个文件,文件行前出现星号*。但此时并不会执行,如果继续输入序号,仍然会继续选中。直到我们直接输入回车。

  7. —intent-to-add:

    1
    git add --intent-to-add

    作用:将新创建但尚未添加的文件添加到暂存区。这对于标记即将添加的文件非常有用。

  8. —renormalize:

    1
    git add --renormalize

    作用:将已经跟踪的文件重新标记为未标准化状态,并添加到暂存区。文件在 Git 中被保存为对象,并在存储库中进行压缩,其中某些文件在保存时可能会被转换为特定的格式(例如换行符或空格的处理)。在一些情况下,可能需要更改这些转换规则,以便 Git 在存储库中正确地保存文件。

GIT COMMIT: 提交至本地版本库

git commit 命令用于将暂存区中的文件更改提交到本地版本库,并生成一个新的提交(commit)。

  1. -m \ / —message=\
    作用:用于指定提交的消息(commit message),即提交的描述信息。消息应该简洁明了,描述本次提交的内容。

    1
    git commit -m "Add new feature"
  2. -a / —all:
    作用:自动将所有已跟踪文件的更改提交到本地版本库,==跳过 git add 步骤==。未跟踪的文件不会被提交。

    1
    git commit -a -m "Fix bugs"
  3. -v / —verbose:
    作用:在提交消息中显示 diff 信息,即显示每个更改的具体内容。

    1
    git commit -v -m "Update documentation"
  4. —amend:
    作用:修改最近一次提交的内容,将新的更改合并到该提交中。这个参数可以在忘记提交某些更改或修改了错误的提交消息时使用。

    1
    git commit --amend -m "Fix typo"

    需要注意的是,如果你已经推送(push)了上一次的提交到远程仓库,修改提交消息后,最好不要再次推送,除非你确定这个操作不会影响其他开发者的工作。因为 git commit --amend 实际上是对历史提交进行修改,会改变提交的 SHA-1 值,如果将修改后的提交推送到远程仓库,可能会导致其他开发者的工作出现问题。

GIT STATUS: 查看当前track状态

git status 是用于查看当前工作目录的状态和变更的命令。它会显示当前分支的状态,包括已修改、已暂存、未跟踪等文件的信息。以下是 git status 命令的详细用法和每个参数的介绍:

1
git status [<options>] [--] [<pathspec>...]
  • 无参数:

    1
    git status

    作用:显示当前分支的状态,包括已修改、已暂存、未跟踪的文件信息。

  • -s / —short:

    1
    git status -s

    作用:以更简洁的格式显示状态信息。使用这个参数,状态的输出将被缩短并显示为两列。

  • -b / —branch:

    1
    git status -b

    作用:显示分支的状态,包括本地分支和与之关联的远程跟踪分支。

  • -v / —verbose:

    1
    git status -v

    作用:显示详细的状态信息,包括修改的文件内容和行数的变化。

  • -u[\] / —untracked-files[\=\]:

    1
    git status -u

    作用:显示未跟踪文件的状态。<mode> 参数可以是 nonormalall,分别表示不显示、显示普通未跟踪文件和显示所有未跟踪文件。

  • —ignored:

    1
    git status --ignored

    作用:显示被忽略的文件的状态。

  • —no-renames:

    1
    git status --no-renames

    作用:在状态中不显示重命名/复制的文件。

  • —column[\=\]:

    1
    git status --column

    作用:以栏格式显示状态,以更好地对齐文件名和状态。

  • —ahead-behind:

    1
    git status --ahead-behind

    作用:在与远程分支比较时,显示本地分支的提交落后或领先的提交数。

GIT DIFF:看看改了什么

  1. git diff 作用:比较工作目录中尚未暂存的更改与最后一次提交之间的差异。
  2. git diff <commit> 作用:比较工作目录中尚未暂存的更改与指定提交 <commit> 之间的差异。可以使用提交的 SHA-1 或分支名等来指定 <commit>
  3. git diff <commit> <commit> 作用:比较两个提交 <commit> 之间的差异。可以使用提交的 SHA-1 或分支名等来指定 <commit>
  4. git diff --cachedgit diff --staged 作用:比较==暂存区(Staging Area)==中尚未提交的更改与最后一次提交(或是指定commit进行比较)之间的差异。即比较已暂存的更改与最后一次提交之间的差异。

如何分析一个git diff的结果:

  • a/file.txtb/file.txt:文件在变化之前和之后的路径。
  • <旧提交哈希><新提交哈希>:文件的上一个版本和当前版本的提交哈希。
  • <模式>:文件的模式信息(例如,100644 表示普通文件)。
  • @@ -5,7 +5,7 @@:该行指示了文件变化的上下文。
  • -This line was removed:这是一个被删除的行。
  • +This line was added:这是一个被添加的行。

GIT LOG:TIMELINE

git log 是 Git 中一个用于查看提交历史的强大命令。它用于显示项目中的提交记录,按照提交时间的先后顺序排列。

  1. 基本用法:

    1
    git log

    这会显示从最新到最旧的提交记录列表。默认情况下,会显示每个提交的哈希值(commit hash)、作者(author)、日期、和提交信息(commit message)。

  2. 显示提交内容变更:

    1
    git log -p

    使用 -p--patch 参数,可以显示每个提交的详细内容变更(diff),包括哪些文件被修改以及具体的修改内容。

  3. 单行显示提交记录:

    1
    git log --oneline

    使用 --oneline 参数,可以以单行的格式显示每个提交记录,只显示提交哈希值和提交信息,更加简洁。

  4. 显示作者统计:

    1
    git log --author=<author_name>

    使用 --author=<author_name> 参数,可以过滤并只显示指定作者的提交记录。

  5. 按照提交时间排序:

    1
    git log --date=short

    使用 --date=short 参数,可以以简洁日期格式显示提交记录,并按照提交时间排序。

  6. 显示分支合并情况:

    1
    git log --graph

    使用 --graph 参数,可以显示提交历史的分支合并情况图。
    可以和--pretty=format联用:

    1
    git log --pretty=format:"%h %s" --graph
  7. 显示指定文件的提交历史:

    1
    git log <file_path>

    git log 命令后面加上指定文件的路径,可以仅显示该文件的提交历史

  8. 显示指定范围的提交历史:

    1
    git log <commit_range>

    git log 命令后面加上两个提交之间的范围,可以显示在这个范围内的提交历史。范围的格式可以是单个提交哈希、分支名,或者是提交范围(例如:master..develop)。
    在Git中,git log命令用于查看版本历史和提交日志。通过提供一个提交范围(commit_range),您可以仅查看该范围内的提交记录。提交范围可以指定一个或多个提交,以便从其中筛选出相应的提交历史。

    1. 单个提交(Single Commit):您可以指定一个具体的提交哈希(commit hash)或提交引用(例如分支名、标签名)来查看该提交的日志。
      1
      2
      3
      git log <commit_hash>
      git log <branch_name>
      git log <tag_name>
    2. 范围提交(Commit Range):您可以通过使用两个提交引用,创建一个范围,来查看这两个提交之间的所有提交。
      1
      git log <commit_ref1>..<commit_ref2>
      在上述命令中,<commit_ref1>是范围的起始提交引用,<commit_ref2>是范围的结束提交引用。范围是包含 <commit_ref1> 提交但不包含 <commit_ref2> 提交的所有提交。
    3. 引用与父提交(Reference with Parent Commit):您可以使用波浪符(~)后跟一个数字,来查看某个提交的父提交。例如,HEAD~2表示HEAD提交的第二个父提交。
      1
      git log <commit_ref>~<number>
    4. 时间范围: 使用 --since 和/或 --until 参数来指定时间范围内的提交。
      1
      git log --since=<date> --until=<date>
      其中<date>是日期时间的格式,例如:"2023-01-01"
    5. 作者过滤: 使用 --author 参数按作者筛选提交。
      1
      git log --author=<author_name>
  9. 限制显示的提交数量:
    使用 -n <num> 参数,可以限制只显示最近的 <num> 条提交记录。

  10. 显示某个提交之前的历史记录:

    1
    git log <commit_hash>~

    使用 <commit_hash>~ 表示显示某个提交之前的历史记录,其中 ~ 表示父提交。

  11. 显示某个提交之后的历史记录:

    1
    git log <commit_hash>..

    使用 <commit_hash>.. 表示显示某个提交之后的历史记录。

  12. 自定义输出格式:

    1
    git log --pretty=<format>

    使用 --pretty=<format> 参数,可以自定义输出的格式。例如,--pretty=format:"%h - %an, %ar : %s" 将以特定格式显示提交的哈希(%h)、作者名(%an)、提交日期(%ad)和提交信息(%s)。也可使用--pretty=oneline,等价于--oneline

    也可以使用其他选项:

  13. 显示变更的简要统计:

    1
    git log --stat

    使用 --stat 参数,可以显示每个提交的简要统计信息,包括文件改动数量和行数的增减。==这一命令加上 —date=short 参数效果更佳。==

  14. 汇总多个开发者提交
    git shortlog是Git的一个命令,用于生成简洁的提交统计报告,以作者为单位汇总提交信息。它可以快速显示每个作者所提交的提交数目,以及他们的提交信息摘要。

GIT CHECKOUT: 在提交节点间跳跃

git checkout 命令在 Git 中是一个非常常用且功能丰富的命令。它主要用于切换分支,恢复文件或撤销更改。

  1. 切换分支:

    1
    git checkout <branch_name>

    这是 git checkout 最常用的用法之一。它允许你切换到指定的分支,并将工作目录和索引设置为该分支的最新状态。

  2. 创建新分支并切换:

    1
    git checkout -b <new_branch_name> [<starting_point>]

    这个用法将创建一个新的分支,并将 HEAD 指向该分支。如果提供了 <starting_point> 参数,新分支将从指定的提交、分支或标签开始。

    -B参数,是-b的强制模式。当要创建的分支已然存在时,会出现fatal: A branch named 'feature/add-about-page' already exists.错误。而-B命令则会强制创建该分支,用新创建的分支覆盖原分支。

  3. 切换到先前的分支:

    1
    git checkout -

    使用单独的短横线 - 作为参数,将你切换回之前所在的分支。

  4. 切换到指定的提交:

    1
    git checkout <commit_hash>

    通过提供提交哈希值,可以将工作目录设置为该提交的状态,进入分离头指针状态(detached HEAD)。

  5. 丢弃工作目录中的更改:

    1
    git checkout -- <file_path>

    可以使用 -- 和文件路径来撤销对工作目录中某个文件所做的更改,将其恢复到最近一次提交的状态。

    如果文件已经被添加到暂存区(Index),但你在工作目录中对其进行了修改,可以使用相同的命令来撤销暂存区中的更改,将其恢复到最近一次提交的状态。

  6. 使用远程分支创建本地分支:

    1
    git checkout -b <local_branch_name> <remote_name>/<remote_branch_name>

    这个用法允许你使用远程分支作为起点创建一个本地分支。

  7. 切换到标签(Tag):

    1
    git checkout <tag_name>

    通过提供标签名称,可以将工作目录设置为标签所指向的提交状态,进入分离头指针状态(detached HEAD)。

  8. 强制切换:

    1
    git checkout -f <branch_name>git 

    这个命令将强制切换分支,即使有未保存的更改。慎用,因为未保存的更改将会丢失。

  9. 检出子模块的提交:

    1
    git checkout <submodule_path>

    在包含子模块的仓库中,这个命令可以用来检出子模块的提交。

  10. 查看分支和提交信息:

    1
    git checkout

    不带参数的 git checkout 会显示当前所在的分支或提交信息。

GIT MERGE: 合并到当前分支

原理:dive into details

.git folders: 了解git原理的关键

对象数据库(Object Database)

在.git文件夹中,有一个名为”objects”的目录,它是Git的对象数据库。Git以对象的形式存储文件和目录的快照、提交信息等。这些对象是Git中的基本数据单元。Git使用SHA-1哈希算法对这些对象进行唯一标识,并根据内容计算哈希值。

Blob(文件快照): Blob是Git中表示文件内容的数据对象。它仅仅存储文件内容本身,不包含文件名或路径等信息。

1. Blob使用SHA-1哈希算法计算唯一标识,由Javascript逆向那一节我们知道,SHA-1生成一个160位的哈希值,转成16进制则为40个字符。为了管理方便,在文件系统中前两位作为`.git/objects/` 子目录的名字,后38为作为文件名字。`git cat-file -p <hash value>`能够将Blob文件逆向输出为文本文件。
2. 每当文件内容发生变化时,Git都会创建一个新的Blob对象,而非存储更改,保留文件历史的完整性。 Git通过SHA-1哈希值来唯一标识每个文件,如果文件内容未发生变化,Git会复用之前的数据块,从而节省存储空间。

Tree(目录结构): Tree是Git中表示目录结构的数据对象。

1. Blob对象中只存储了文件内容,并不含该对象的文件名等信息。故GIT用树结构来解决文件名保存的问题。具体而言,类似文件树,一个树对象包含一个文件模式(在新增文件时出现的 create mode xxxx),对象的类型(是Blob对象还是子树对象,可以用`git cat-file -t <key>`来查看类型),对象的SHA-1指针,最后是文件名。
2. ![](https://gcore.jsdelivr.net/gh/Molaison/IMages/20230722004544.png)![](https://gcore.jsdelivr.net/gh/Molaison/IMages/20230722004731.png)

Blob对象对应的是文件一次次不同的版本(位于不同的blob文件中);而Tree对象对应的是一次次版本,当版本更新时,新建一个Tree并将现有Tree作为其子节点,同为子节点的还有修改后的文件的Blob对象条目。

Commit(提交信息): Commit是Git中表示项目版本的数据对象。它包含了提交作者、提交时间、提交信息和指向对应文件树(Tree)的哈希指针等信息。每次执行”git commit”命令时,都会生成一个新的Commit对象,记录了当前项目状态的快照,可以看做是Tree对象的一个封装。

而对于多个提交,其格式如下:当文件内容未被修改时,复用之前的数据块。

Tag(标签): Tag是Git中表示版本标签的数据对象,Tag对象存储在Git的对象数据库中,和其他对象(如提交对象、树对象和Blob对象)一样,以压缩形式存储在.git/objects目录下。它是对某个Commit的引用,用于标记特定的版本,如软件发布的稳定版本或里程碑。类似Commit,Tag对象包含了标签名、标签作者、标签信息和指向对应Commit的哈希指针。

默认情况下,git push命令并不会传送标签到远程仓库服务器上。在创建完标签后,你必须显式地(手动)推送标签到远程服务器上。使用git push origin <tagname>git push origin --tags来完成。

分支与引用(Branches and References)

在.git文件夹中,有一个名为”refs”的目录,其中包含分支和引用。分支是指向提交对象(commits)的指针,而引用是指向分支或标签(tags)的指针。这些指针的存在使得Git能够跟踪项目的不同开发分支,并快速访问最新的提交。
存储于.git/refs/tags目录中的是以标签名作为文件名而以校验值作为内容的文件。

Tag对象的文件结构如下:

1
2
3
4
object <commit对象的SHA-1>
type commit
tag <标签名>
tagger <标签作者> <时间戳> <标签信息>

  • <commit对象的SHA-1>:这个字段存储了标签对应的提交的SHA-1哈希值,即标签所指向的提交。
  • type commit:表示这个Tag对象是一个指向提交的标签。
  • <标签名>:这个字段存储了标签的名称。
  • tagger <标签作者> <时间戳> <标签信息>:这个字段包含了标签的作者信息、创建时间和标签信息。通常情况下,Git会自动记录创建标签的人和时间,除非在创建Tag时指定了自定义信息。

而对于分支对象的引用, 它存储在refs/heads/目录下,当使用feature/add-about-page分支名时,会使用feature作为子目录,add-about-page作为子文件。该文件同样存储的是blob文件的校验码。
远程分支也采用类似操作,储存于refs/remotes目录下。每个远程仓库对应一个文件或目录。
这个目录位于.git目录下,是Git存储分支引用(即分支指针)的地方。每当你创建一个新的分支或切换到一个分支,Git会在refs/heads/目录下创建或更新相应的分支引用。

另一种特别的引用是stash,其储存于refs/stash中。实际上,.git/refs/stash文件只存储了最近一个stash的哈希值。这是因为在Git中,stash通常被存储为一个栈(stack)结构,最新的stash会覆盖之前的stash,弹出一个stash时使用上一个stash。其他stash的哈希值实际上存储在一个名为.git/logs/refs/stash的文件中。.git/logs/refs/stash文件记录了stash的历史,包含了过去每次stash的提交信息,每条记录包含了一个哈希值和相关的提交信息。这个文件允许你查看stash的历史,以及在需要时恢复特定的stash。

索引(Index)

Git使用一个名为”index”的文件来暂存即将提交的更改。在工作目录中修改文件后,将这些更改暂存到索引中,然后通过”git commit”命令将索引中的更改提交到版本历史中。

  • 暂存区:.git/index文件是Git的暂存区,用于存储即将提交的文件修改信息。在进行提交前,你可以使用git add命令将工作目录中的文件修改添加到暂存区,.git/index就会记录这些文件的状态和变化。
  • 跟踪差异:.git/index文件还用于跟踪当前工作目录和版本库之间的差异。它记录了哪些文件被修改,哪些文件被添加或删除,以便在提交时可以准确地记录这些变化。

主要存储的内容有:

  • 文件信息:.git/index中存储了当前暂存区的文件状态,包括文件路径、文件权限、对象(blob)的SHA-1哈希值和文件大小等。
  • 目录树信息:Git使用目录树来表示文件和目录的结构。.git/index中包含了当前版本库中的目录树信息,以便跟踪工作目录的变化。
  • 其他元数据:.git/index文件还可能包含一些其他元数据,例如文件版本、时间戳和标记信息等。
配置(Configuration)

Git的配置文件存储在.git/config中,也可以在用户主目录下的.gitconfig中设置。配置文件包含有关Git仓库和全局设置的信息,例如用户名、邮箱、默认分支等。

日志(Logs)

在.git文件夹中的”logs”目录下存储着对仓库进行操作的日志信息,如分支的提交历史和引用的更新记录。
.git/logs/HEAD.git/logs/HEAD文件用于记录HEAD引用的历史变更。这包括HEAD引用在不同分支或提交之间切换的历史记录,以及直接指向特定提交的情况。
内容通常包括一系列记录,每条记录对应一个HEAD引用的历史修改。每条记录可能包含以下信息:

  • HEAD的旧值和新值:记录HEAD引用在修改前后的状态,即旧的HEAD引用值和新的HEAD引用值(通常是提交的SHA-1哈希值)。
  • 提交信息:通常包括引用变更的提交信息、作者和提交时间等。
  • 操作者信息:记录进行HEAD引用修改的人员信息。
钩子(Hooks)

Git允许用户在特定的操作点上运行自定义脚本,这些脚本称为钩子。.git/hooks目录存储了这些钩子的样本文件,用户可以根据需要复制并重命名这些样本文件,以创建自己的钩子来触发特定的操作。

配置文件(config和ignore)

.git/config和.gitignore文件分别用于存储项目特定的仓库配置和要忽略的文件列表。.gitignore文件中列出的文件和目录将不会被Git跟踪和纳入版本控制。

.git/config: .git/config文件是每个Git仓库的仓库级别配置文件。它用于存储与该仓库相关的配置信息,这些配置信息包括用户信息、远程仓库的URL、分支设置、别名、提交模板等。这些配置项仅适用于当前的Git仓库,不会影响其他仓库或全局设置。

.gitignore: .gitignore文件中的规则用于指定要忽略的文件和目录,以防止它们被Git纳入版本控制。以下是一些常见的.gitignore规则案例:

  1. 忽略特定文件:
    1
    2
    3
    myfile.txt          # 忽略名为"myfile.txt"的文件
    *.log # 忽略所有以".log"为后缀的文件
    secrets.txt # 忽略名为"secrets.txt"的文件
  2. 忽略特定目录:
    1
    2
    build/              # 忽略名为"build"的目录及其所有内容
    output/ # 忽略名为"output"的目录及其所有内容
  3. 忽略指定后缀的文件:
    1
    2
    *.exe               # 忽略所有以".exe"为后缀的文件
    *.tmp # 忽略所有以".tmp"为后缀的文件
  4. 忽略特定文件或目录的所有内容:
    1
    2
    logs/*              # 忽略名为"logs"目录中的所有内容
    secret_folder/ # 忽略名为"secret_folder"的目录及其所有内容
  5. 使用通配符和正则表达式:
    1
    2
    3
    *.log               # 忽略所有以".log"为后缀的文件
    /build/ # 忽略位于仓库根目录下的名为"build"的目录
    /docs/*.pdf # 忽略位于仓库根目录下的名为"docs"目录中的所有以".pdf"为后缀的文件
  6. 忽略特定路径模式:
    1
    2
    **/temp.txt         # 忽略任意深度下的名为"temp.txt"的文件
    !/src/temp.txt # 除了"src"目录下的"temp.txt"文件之外,忽略其他位置的同名文件
其他
  1. COMMIT_EDITMSGCOMMIT_EDITMSG文件是在进行提交(commit)操作时使用的文件。当你执行git commit命令提交代码时,Git会打开一个文本编辑器,并让你输入提交消息(commit message)。这个编辑器打开的文件就是COMMIT_EDITMSG。你需要在其中输入一条简洁明了的提交消息,描述你所做的更改。

  2. HEADHEAD是一个特殊的引用(reference)指针,它始终指向当前所在分支的最新提交。在Git中,HEAD表示当前工作目录所在的位置。当你检出(checkout)不同的分支或提交时,HEAD引用会更新指向相应的分支或提交,从而指示当前工作目录所在的位置。在分支操作中,HEAD是非常关键的。

  3. ORIG_HEADORIG_HEAD是一个自动设置的引用,它用于保存分支操作前的HEAD引用的值。当你执行某些具有潜在风险的分支操作,例如合并(merge)、重置(reset)、回滚(revert)等时,Git会自动设置ORIG_HEAD引用,以保存你执行操作前HEAD引用所指向的位置。这样,你可以在需要时回到操作前的状态。

Merge的底层逻辑: file level

  1. 查找共同祖先: 在进行合并之前,Git首先查找两个分支的共同祖先(合并基础),这个共同祖先是它们最近的共同提交。通过找到共同祖先,Git可以确定要合并的范围,并知道各个分支的修改是从哪个点开始的。
  2. 比较修改: Git比较共同祖先和要合并的两个分支之间的差异,确定哪些内容在每个分支上进行了修改。对于BLOB对象,Git会比较文件内容,找出每个分支上的修改。
  3. 合并修改: 对于BLOB对象,合并修改意味着将两个分支对同一文件所做的修改合并到一起。如果两个分支对文件的修改没有冲突,Git会自动合并这些修改,生成一个新的文件内容,作为合并结果。
  4. 解决冲突: 如果两个分支对同一文件的修改发生冲突,即它们修改了相同的内容,并且没有共同祖先的提交来指导合并,那么就会发生合并冲突。对于BLOB对象的合并冲突,通常是Git无法自动解决的,需要开发者手动解决冲突。
  5. 生成新的BLOB对象: 在合并成功或者手动解决冲突后,Git会生成一个新的BLOB对象,其中包含了合并后的文件内容。这个新的BLOB对象将作为合并结果,用于创建一个新的合并提交。

总结一下:git使用的是三路合并(Three-way merge)的方法。我们首先要找到共同祖先,即修改前的版本作为基准。相对于基准,分别逐行进行比对,只要不是同时对一行文本进行修改,git会自动接受修改。

多个公共祖先?

merge X'' Y'X' Y''的时候发现有两个节点都符合最近的公共祖先,即:

  • X' Y
  • X Y'
    我们称这种情况为:Criss-cross-merge,这时就需要用到Recursive three-way merge算法,具体步骤如下:
  1. 先把候选的两个最近的公共祖先递归调用merge,生成成一个虚拟的节点。
  2. 然后让这个虚拟节点作为Base进行合并。