Git:掌握版本控制的指南

Git 是一个分布式版本控制系统。Linux 的创建者 Linus Torvalds 仅用 5 天就编写了自己的版本控制系统 Git。多年来,它迅速传播开来,成为全球开发人员的首选版本控制工具。

在 Git 出现之前,Linus 和许多其他开源开发人员都使用过专有版本控制系统 BitKeeper。然而,当 BitKeeper 与开源社区的关系恶化时,Linus 决定创建自己的解决方案。因此,Git 诞生了,其设计目标是快速、可靠和开源。

Git 允许您使用命令轻松地搜索、操作和恢复历史记录。

Git 的效率和灵活性彻底改变了开发人员在软件项目中协作、管理代码和维护历史记录的方式。

瓷器和水管命令

Git 使用两种类型的命令进行操作:**porcelain** 和 **plumbing**。

  • Porcelain 命令是高级且用户友好的命令,旨在直观地执行日常任务,例如提交更改、查看日志和管理分支。(您将在 999% 的时间里使用这些命令)
  • 管道命令是低级命令,通常由脚本和其他工具使用,提供对 Git 内部更精细的控制。
  • 瓷器命令示例:

  • git clone – 创建存储库的副本。
  • git commit – 记录对存储库的更改。
  • git push –将本地更改上传到远程存储库。
  • git status –检查工作目录中文件的状态。
  • 管道命令示例:

  • git cat-file – 显示 Git 内部数据库中的对象内容。
  • git ls-tree — 列出树中的对象(通常用于检查存储库结构)。
  • git update-index – 更新索引(暂存区),允许进行低级更改。
  • 虽然日常 Git 交互中会用到瓷器命令(它们占了你要使用的命令的 99%),但管道命令可以让开发人员和高级用户更好地控制系统的操作。我们将在这篇文章中讨论一些重要的命令。

    要安装 Git,请访问 Git --distributed-even-if-your-workflow-isnt。

    安装后,你可以使用以下命令检查 Git 的版本:

    git --version

    **RTFM**,即“阅读该死的/友好的手册”,是开发者社区中常见的一句话,鼓励您查阅文档以获取详细信息。在 Git 中,您可以通过运行以下命令来访问其手册:

    man git

    该手册提供了有关 Git 命令、选项和用法的全面信息,以帮助您更好地了解如何有效地使用 Git。

    Linus 创建 Git 是因为与 BitKeeper 的许可协议禁止对该软件进行逆向工程,因为它是专有的。然而,Linux 组织中的某个人违反了此协议,导致 BitKeeper 撤销了许可证。作为回应,Linus 创建了 Git 作为替代方案。

    配置 Git

    Git 配置可以在多个级别进行:全局(适用于所有存储库)或本地(特定于存储库)。配置文件可以位于用户的主目录(`~/.gitconfig`)中,也可以位于特定存储库的 `.git` 文件夹中。

    要设置和配置您的 Git 存储库,请按照以下步骤操作:

    **检查当前配置:**

  • 检查当前配置的用户名:git config --get user.name
  • 检查当前配置的电子邮件:git config --get user.email
  • **添加全局配置(设置用户名和电子邮件):**

  • 全局设置您的用户名(适用于所有存储库): git config --add --global user.name "github_username_here"
  • 全局设置您的电子邮件(适用于所有存储库):git config --add --global user.email "email@example.com"
  • **设置默认分支名称:**

  • 配置 Git 在初始化新存储库时使用 master 作为默认分支名称:git config --global init.defaultBranch master
  • **检查全局配置文件:**

  • 要查看全局 Git 配置,可以打开配置文件:cat ~/.gitconfig # Linux 上全局配置文件的位置
  • 这些命令设置您的 Git 配置,以确保正确跟踪提交、所有存储库中的一致用户身份以及新存储库的默认行为设置。

    Git 存储库

    **Git 存储库** 是存储所有项目文件和版本历史记录的地方。项目文件夹内的 `.git` 目录包含所有内部跟踪和版本信息,例如提交、分支和配置。

    git init

    要初始化新的 Git 存储库,请使用:

    git init

    此命令创建“ .git”目录,将您的项目文件夹转变为 Git 存储库,并允许您开始跟踪更改。

    Git 状态

    在 Git 中,文件的生命周期可以分为几个阶段:

  • 未跟踪:Git 尚未意识到该文件。
  • 已暂存:文件已准备好提交(添加到下一个快照)。
  • 已提交:文件的更改已保存在 Git 的历史记录中。
  • Working Directory
    +---------------------+
    |                     |
    |   Untracked Files   |  <-- Files not added to Git
    |                     |
    |   Staged Files      |  <-- Files added with `git add`
    |                     |
    +---------------------+
             |
             v
    +---------------------+
    |                     |
    |   Committed Files   |  <-- Files saved in Git history
    |                     |
    +---------------------+

    要查看存储库的当前状态,包括所有文件的状态,您可以使用:

    git status

    此命令显示哪些文件未跟踪、暂存或有需要提交的更改。这是跟踪工作进度的有用方法。

    分期

    在 Git 中,暂存是指在提交文件之前将文件添加到暂存区的过程。暂存区就像是下一次提交将包含的内容的预览。

    要暂存特定文件,您可以使用:

    git add i-use-arch.btw

    或者,如果您想要暂存所有修改过的或新的文件,您可以使用:

    git add .

    这会为下一次提交的更改做好准备,让您可以控制包含哪些更改。

    承诺

    在 Git 中,**提交** 是在特定时间点保存存储库快照的过程。每次提交都包含一条描述所做更改的提交消息。

    要创建提交,请使用:

    git commit -m "your message here"

    这会将暂存区的当前状态存储为带有所提供消息的提交。

    如果您想更改**最后一次提交**的消息,您可以使用:

    git commit --amend -m "new message"

    这使您可以更新最后的提交消息而无需创建新的提交。

    你已经学习了 Git 的一半(有点)

    到目前为止,您已经了解了管理本地存储库的核心命令。这些是独立开发的基本工具:

  • git status – 检查存储库的当前状态。
  • git add – 暂存要提交的更改。
  • git commit – 用消息保存你的工作快照。
  • 这些命令足以让您自己管理项目。但是,Git 还可以提供更多功能来增强协作和版本控制。

  • Git 的 40% 专注于与他人合作,包括处理远程以及向/从托管在 GitHub 或 GitLab 等平台上的存储库推送或拉取代码。
  • 最后 10% 涉及处理错误、回滚更改以及分支、合并和变基等高级主题。这些内容可帮助您处理更复杂的场景并优化您的工作流程。
  • 但是通过您目前所学的知识,您已经能够熟练使用 Git 进行单独开发了!

    Git 日志

    `git log` 命令对于查看存储库中的提交历史记录至关重要。它允许您查看每次提交的执行者、提交时间以及引入了哪些更改。以下是 `git log` 中使用的常用标志的概述:

  • git log 此命令显示完整的提交历史记录。默认情况下,它使用分页器显示日志,因此您可以一次滚动一屏查看历史记录。
  • git --no-pager log -n 10 不使用分页器显示最近 10 次提交,因此输出会直接显示在终端中而无需暂停。当您想要快速查看最近的提交时,这很有用。
  • git --no-pager log -n 10 --oneline --parents --graph -n 10:将输出限制为最后 10 次提交。--oneline:将输出压缩为每个提交一行,仅显示提交哈希和提交消息。--parents:显示父提交,显示合并之间的关系。--graph:绘制分支结构图以显示提交如何关联,直观地表示分支和合并。
  • git log --decorate=full/short/no --decorate=full:在提交旁边完整显示引用(如分支名称或标签)。--decorate=short:以缩短的格式显示引用。--decorate=no:完全隐藏引用名称,仅显示提交详细信息。
  • git log --oneline -p --oneline:在一行上显示每个提交。-p:显示每次提交中引入的更改,可以轻松查看每次提交的文件中修改的内容。
  • git log --oneline --graph --all --oneline:将每个提交压缩为一行。--graph:将分支结构以图形形式可视化。--all:在日志中包含所有分支(而不仅仅是当前分支),以便您可以查看整个存储库的完整历史记录。
  • git log --oneline --graph --decorate --parents 这结合了几个选项:--oneline:每个提交的一行摘要。--graph:提交历史的图形表示。--decorate:显示参考(如分支名称或标签)。--parents:显示父提交,特别适合于理解合并。
  • 每个标志都允许您自定义“git log”的输出以满足不同的需求,从而更容易查看提交历史记录并跟踪项目中的更改。

    提交哈希 (SHA-1)

    提交哈希是使用 SHA-1 哈希算法为 Git 中的每个提交生成的唯一标识符。它用于跟踪存储库中的提交及其更改。

    例如:`5ba78624h4i5hslv831c01e71444b9baa2228a4f`

    实际上,通常只需要提交哈希的前 7 个字符即可识别它。

    提交哈希是以下函数:

  • 提交消息:描述提交中所做更改的文本。
  • 作者的姓名和电子邮件:提交的人。
  • 日期和时间:提交的时间。
  • 父(前一个)提交哈希:当前提交之前的提交,将历史记录链接在一起。
  • 由于这些因素,哈希冲突(两个不同的提交具有相同的哈希值)的概率极低,从而确保每个提交都可以被唯一地标识。

    让我们看看管道

    在 Git 中,**管道** 指的是处理存储库数据存储和组织的内部机制。所有 Git 数据都存储在 `.git` 目录中,该目录隐藏在项目文件夹中。

  • .git/objects:这是 Git 存储其数据对象(包括提交、树和 blob)的地方。提交实际上是一种对象,所有版本信息都以对象的形式存储在此处。
  • Git 的文件存储方法有助于确保高效的访问和检索,同时避免系统限制(例如传统文件系统中发现的限制)。

    文件系统中的 Inode

  • Inode 是文件系统(例如 Linux)用来管理文件元数据(例如权限、时间戳等)的数据结构。当同一目录中有许多文件时,可能会导致 inode 破坏,文件系统难以在一个地方管理如此多的文件,从而导致性能问题。
  • 为了避免 inode 破坏,Git 采用了一种巧妙的方法:

  • Git 根据提交哈希的前两个字符来组织对象,并创建包含以哈希其余字符命名的文件的目录。这减少了每个目录中的文件数量,从而提高了性能。
  • 例子:

    如果你查看特定对象,例如:

    cat .git/objects/78/asfadfefj8e0r48...

    它将打印一堆压缩的原始字节数据。这是对象的内容,Git 以压缩形式存储它以节省空间并缩小 `.git` 目录。

    Git 使用这个对象存储系统来有效地管理和跟踪变化,同时保持高度优化的文件结构。

    内置管道命令:git cat-file

    Git 提供了内置的管道命令来访问存储库的原始内部数据。其中一个命令是 **git cat-file**,它允许您通过哈希值与 Git 对象进行交互。

    git cat 文件 -p

  • 此命令用于根据给定的哈希值漂亮地打印 Git 对象的内容。
  • 您可以使用哈希的前 4 个字符而不是完整哈希,以便更快地访问。
  • 例子:
  • git cat-file -p 

    这将以可读格式显示对象(提交、树或 blob)的内容。

    文件的十六进制转储

    要查看 Git 中文件对象的原始二进制内容,可以使用 `xxd` 将其转换为十六进制格式:

    xxd path/to/file > /tmp/commit_object_hex.txt

    这将生成指定文件的十六进制转储并将其保存到“/tmp/commit_object_hex.txt”文件中。

    Git 对象的类型

  • 提交 提交对象表示存储库在特定时间点的快照。提交包含指向 blob 对象(表示文件)的树对象(表示目录)。示例:提交可能如下所示:提交 → 树 → Blob → 内容(即文件的内容)
  • 树 树对象表示 Git 中的目录。它包含对 blob 对象(文件)或其他树(子目录)的引用。树类似于文件系统中的目录结构,但表示为 Git 对象。
  • Blob Blob 对象存储文件的内容。它包含文件的实际数据,不包含任何目录结构。
  • 提交、树、Blob 和内容之间的关系

    **提交** 包含:

  • 对树对象的引用,它是提交时的目录结构的表示。
  • 树对象指向 blob 对象,代表提交中的文件。
  • Blob 对象包含实际的文件内容。
  • 示例结构:

    Commit → Tree → Blob → Contents
    (A) → (tree) → (blob) → contents.md

    Git 日志中的父哈希

  • Git 日志中的父提交是前一个提交的哈希值。
  • 对于 Git 来说,跟踪父提交非常重要,这样它才能随着时间的推移维护存储库的历史记录和结构。
  • 管道与瓷器命令

  • git log 是一个 Porcelain 命令
  • git cat-file 是一个管道命令
  • 通过了解 **trees** 和 **blob** 的工作原理,您可以更深入地了解 Git 如何组织和存储数据。这种内部结构有助于 Git 高效地管理更改,同时保持存储库轻量级。

    在 Git 中存储数据

    Git 不仅仅存储更改;它还存储每次提交中所有文件的**完整快照**。每次提交都会捕获当时项目的完整状态,而不仅仅是差异。

    通过压缩提高效率

    Git 使用 **压缩** 来最小化 `.git` 目录的大小,这有助于减少存储需求。得益于 Git 的内部机制,即使是大文件也能高效存储。

    树对象和 Blob

    每次提交都会存储一个唯一的 **树对象**,它是目录的快照。虽然每次提交都会创建一个新树,但 Git 不会再次存储未更改的文件。相反,它会 **指向先前提交的现有 blob**(文件),从而减少重复并提高流程效率。

    删除后续提交的文件

    如果在后续提交中删除了文件,则这些文件的 **哈希值** 将指向 null。这会在历史记录中创建断开的链接。要清理这些引用,您可以 **修剪** 存储库以删除未使用的对象并减小大小。

    修剪和清理

    您可以使用以下命令修剪断开的链接并优化存储库:

    git gc --prune=now

    这有助于删除未引用的对象并保持存储库更小。

    再次配置 Git

    Git 提供多级配置,允许您全局、本地或针对特定工作树设置配置选项。以下概述了如何管理和与 Git 配置交互。

    Git 配置命令

  • 查看本地配置 要查看特定存储库的本地配置:
  • git config --list --local
       cat .git/config
  • 获取特定配置值 要检索特定配置值:
  • git config --get 
  • 取消设置配置键要从配置中删除特定键:
  • git config --unset 
  • 取消设置某个键的所有出现次数 要删除某个配置键的所有实例:
  • git config --unset-all example.key

    (注意:Git 只会应用配置文件中键的最后一次出现,因此如果有重复,则最后一个键将优先。)

  • 从配置中删除部分 要从配置中删除整个部分:
  • git config --remove-section section
  • 手动编辑您还可以直接编辑 .git/config 文件(或根据范围的其他配置文件),以获得更简单的方法。
  • 配置文件位置

    Git 配置文件可以存在于多个地方,每个地方都有不同的作用域:

  • 系统:/etc/gitconfig 此文件为系统上的所有用户配置 Git。
  • 全局:~/.gitconfig 此文件为用户的所有存储库配置 Git。
  • 本地:.git/config 此文件为特定存储库配置 Git。
  • 工作树:.git/config.worktree 此文件为项目的特定部分(工作树)配置 Git。
  • Git 分支

    Git 中的分支本质上是指向特定提交的 **命名指针**。创建分支时,实际上是在创建一个跟踪特定提交的新指针。分支指向的提交称为分支的 **尖端**。分支是轻量级的,因为它们只是指针,不需要复制整个项目,因此是一种资源节约型的创建方式。

    常用命令

  • 重命名分支您可以使用以下命令重命名分支:
  • git branch -m oldname newname
  • 创建新分支(不切换) 要创建新分支但不切换到该分支,请使用:
  • git branch my_new_feature
  • 创建并切换到新分支 要通过一个步骤创建并切换到新分支,请使用:
  • git switch -c my_new_feature
  • 切换到现有分支 要简单地切换到现有分支,请使用:
  • git switch my_existing_feature

    或者,按照老方法:

    git checkout my_existing_feature

    分支机构信息存储

    Git 将有关分支的所有信息存储在项目根目录 `.git` 子目录中的文件中。分支的“heads”(或“tip”)专门存储在 `.git/refs/heads/` 目录中。

    合并分支

    为了找到两个分支之间最佳的共同祖先提交(合并基),Git 将使用 **合并基** 来识别要合并的共同提交点。例如:

    git merge my_feature_branch

    修改提交信息

    如果你需要更改最后一次提交的消息,你可以使用 `--amend` 标志:

    git commit --amend -m "Updated commit message"

    快进合并

    在**快速合并**中,如果功能分支具有基础分支的所有提交,则 Git 将简单地将基础分支的指针移动到功能分支的尖端。例如:

    git merge my_feature_branch

    删除分支

    完成分支后,可以使用以下方法删除它:

    git branch -d my_feature_branch

    如果分支已合并,这将在本地删除该分支。如果分支尚未合并,请使用 `-D` 强制删除。

    团队开发的常见 Git 工作流程

  • 创建分支 首先为变更创建一个新的分支。
  • 进行变更 对变更进行工作,然后在准备就绪后提交。
  • 将分支合并到主分支 一旦更改完成,就将分支合并回主分支。
  • 删除分支 合并后,删除分支以保持存储库清洁。
  • 重复对每个新的变化或特征重复此过程。
  • Git 中的 Rebase

    变基是一种将一系列提交移动或合并到新的基准提交的方法。与合并相比,它有助于维护更清晰、更线性的历史记录。

    考虑以下场景:

    A - B - C    main
       \
        D - E    feature_branch

    您正在处理“feature_branch”,并且想要从“main”引入最新更改以避免使用过时的代码。您可以将“main”合并到“feature_branch”,但这会引入合并提交,从而可能使历史记录混乱。相反,**rebase** 会将“feature_branch”中的提交重新应用于“main”之上,从而创建线性历史记录。

    运行以下 rebase 命令后:

    git rebase main

    历史将会像这样:

    A - B - C         main
             \
              D' - E' feature_branch

    请注意,提交“D”和“E”已被重写为“D'”和“E'”,因为“feature_branch”的历史记录现在基于“main”中的提交“C”。这就是提交哈希在重新定基后发生变化的原因——这些提交的基础已经转移。

    为什么要使用 Rebase?

  • 更清晰的历史记录:Rebase 通过避免不必要的合并提交来创建线性历史记录。它使项目历史记录更易于阅读和理解,尤其是在审查更改时。
  • 避免合并提交:与可能导致许多合并提交的合并不同,rebase 可以使历史记录更清晰、更线性。
  • 但是,变基会重写提交历史,因此应谨慎使用。

    重要提示:

  • 在功能分支上进行变基:在您自己的功能分支上进行变基是安全的。您可以根据需要随时进行变基,以使您的分支与主分支的最新更改保持同步。
  • 切勿对公共分支进行变基:不要对其他开发人员正在处理的共享分支(如主分支)进行变基。更改公共分支的提交历史可能会给已经基于该历史开展工作的其他人带来冲突和困惑。
  • 使用合并保留历史记录:虽然 rebase 可使历史记录更清晰,但合并可保留项目的真实历史记录,准确显示分支合并的时间和地点。如果保留完整历史记录很重要,请使用合并。否则,请使用 rebase 以获得更直接的提交历史记录。
  • 切换分支的示例:

    如果您需要从历史记录中的特定提交分支,则可以使用以下命令:

    git switch -c new_feature 

    这将从由以下标识的提交中创建一个名为“new_feature”的新分支:`。

    在共享分支上使用 rebase 时要小心,因为它会改变提交历史。

    您可能想知道的一些术语

  • 索引:提交之前添加更改的暂存区。此区域中的文件已准备好提交,但尚未成为提交历史记录的一部分。
  • 工作树:项目的工作目录,所有文件都位于其中。这包括暂存和未暂存的更改,这些更改可能会或可能不会添加到提交历史记录中。
  • Git Reset:撤消更改

    `git reset` 命令用于撤消存储库中的更改。它将当前分支指针移动到不同的提交,并可能影响暂存区和工作目录。

    重置类型

  • git reset --soft 将分支重置为指定提交。将您的更改暂存以进行新的提交。
  • git reset --hard 重置分支并放弃所有更改(暂存和未暂存的更改)。警告:不可逆,所有未提交的更改都将丢失。
  • 例子:

  • 软重置(保留更改):
  • git reset --soft HEAD~2
  • 硬重置(放弃更改):
  • git reset --hard abc1234

    警告:

  • 硬重置不可逆。它将永久丢弃任何未提交的更改,因此请谨慎使用。在执行 --hard reset 之前,请务必确保您已保存或提交了重要工作。
  • .gitignore

    `.gitignore` 文件是一个**隐藏文件**(以点开头),用于告诉 Git 忽略项目中的哪些文件或目录。它用于防止在版本控制中跟踪不必要或敏感的文件(例如构建工件、日志或配置文件)。

    工作原理:

  • 全局 .gitignore:通常位于项目的根目录中,并适用于整个存储库。
  • 本地 .gitignore:您还可以在子目录中拥有 .gitignore 文件。这些文件仅适用于该特定子目录中的文件,从而实现更精细的控制。
  • 例子:

  • Root .gitignore(忽略所有地方的 *.log 文件):
  • *.log
  • 子目录 .gitignore(在 logs/ 目录中,忽略那里的 *.tmp 文件):
  • logs/*.tmp

    在这种情况下,存储库中所有位置的 `*.log` 文件将被忽略,但只有 `logs/` 目录中的 `*.tmp` 文件会被忽略。

    要点:

  • 您可以在多个目录中拥有 .gitignore 文件,每个文件都适用于该目录及其子目录中的文件。
  • .gitignore 基于相对路径工作。您可以指定要忽略的文件或文件夹,或使用通配符,如 *.log 或 **/temp/。
  • 示例结构:

    /project-root
      .gitignore       # ignores *.log globally
      /logs
        .gitignore     # ignores *.tmp in the logs directory
        error.log      # will be ignored because of the global .gitignore
        temp.tmp       # will be ignored because of the subdirectory .gitignore

    这种灵活性有助于管理哪些文件应该被跟踪以及哪些文件应该保留在开发环境中。

    在向有经验的人询问之前,请务必**RTFM**(阅读手册)。了解基础知识并参考官方文档不仅可以节省您的时间,还可以帮助您更彻底地掌握概念。

    每周复习一次 Git 以保持技能的敏锐,最好的方法是将 Git 集成到您正在从事的项目中。通过定期使用它,您将更加熟悉高级功能和工作流程,这将使版本控制成为您的第二天性。

    这是本系列的第 1 部分,第 2 部分即将推出!请继续关注有关掌握 Git 的更多见解。