{
  "title": "版本控制(Git)",
  "excerpt": "如何_正确地_使用版本控制,并利用它来拯救你于灾难之中、与他人协作,以及快速查找和隔离有问题的更改。不再需要 `rm -rf; git clone`。不再有合并冲突(至少会少很多)。不再有大段大段被注释掉的代码。不再为如何找到破坏代码的问题而烦恼。不再有\"哦不,我们删掉可用的代码了吗?!\"的恐慌。",
  "content_html": "<p>版本控制系统(VCS)是用于跟踪源代码(或其他文件和文件夹集合)更改的工具。顾名思义,这些工具帮助维护更改历史;此外,它们还促进协作。VCS 通过一系列快照来跟踪文件夹及其内容的更改,其中每个快照封装了顶级目录中文件/文件夹的整个状态。VCS 还维护元数据,如谁创建了每个快照、与每个快照相关的消息等等。</p>\n\n<p>为什么版本控制有用?即使你独自工作,它也可以让你查看项目的旧快照,保留某些更改的原因日志,在并行的开发分支上工作等等。与他人协作时,它是查看其他人更改内容以及解决并发开发中冲突的宝贵工具。</p>\n\n<p>现代 VCS 还可以让你轻松地(通常是自动地)回答以下问题:</p>\n\n<ul>\n<li>谁编写了这个模块?</li>\n<li>这个特定文件的这一特定行是何时编辑的?由谁编辑?为什么要编辑?</li>\n<li>在过去的 1000 次修订中,某个特定的单元测试是何时/为何停止工作的?</li>\n</ul>\n\n<p>虽然存在其他 VCS,但 <strong>Git</strong> 是版本控制的事实标准。这幅 <a href=\"https://xkcd.com/1597/\">XKCD 漫画</a>捕捉了 Git 的声誉:</p>\n\n<p>因为 Git 的接口是一个有漏洞的抽象,自顶向下学习 Git(从其接口/命令行界面开始)可能会导致很多困惑。可以记住一些命令并将它们视为魔法咒语,并在出现任何问题时遵循上面漫画中的方法。</p>\n\n<p>虽然 Git 的接口确实很丑陋,但其底层设计和思想是美丽的。虽然丑陋的接口必须被<em>记忆</em>,但美丽的设计可以被<em>理解</em>。因此,我们自底向上地解释 Git,从其数据模型开始,然后介绍命令行界面。一旦理解了数据模型,就可以更好地理解命令如何操作底层数据模型。</p>\n\n<h1>Git 的数据模型</h1>\n\n<p>你可以采用许多临时方法来进行版本控制。Git 有一个经过深思熟虑的模型,可以实现版本控制的所有优秀特性,如维护历史、支持分支和启用协作。</p>\n\n<h2>快照</h2>\n\n<p>Git 将某个顶级目录中文件和文件夹集合的历史建模为一系列快照。在 Git 术语中,文件被称为\"blob\",它只是一堆字节。目录被称为\"tree\",它将名称映射到 blob 或 tree(因此目录可以包含其他目录)。快照是正在跟踪的顶级树。</p>\n\n<h2>历史建模:关联快照</h2>\n\n<p>版本控制系统如何关联快照?一个简单的模型是线性历史。历史将是一个快照列表,按时间顺序排列。由于各种原因,Git 不使用简单的模型。</p>\n\n<p>在 Git 中,历史是一个由快照组成的有向无环图(DAG)。这听起来可能很花哨,但这只是意味着 Git 中的每个快照都引用一组\"父级\"——它之前的快照。请注意,这是一组父级而不是单个父级,因为快照可能来自多个父级,例如,由于合并两个并行开发分支。</p>\n\n<p>Git 将这些快照称为\"提交\"。可视化提交历史可能看起来像这样:</p>\n\n<pre><code>o &lt;-- o &lt;-- o &lt;-- o\n            ^\n             \\\n              --- o &lt;-- o\n</code></pre>\n\n<p>在上面的 ASCII 艺术中,<code>o</code> 对应单个提交(快照)。箭头指向每个提交的父级(这是一个\"先于\"关系,而不是\"之后\")。在第三次提交之后,历史分支成两条独立的分支。这可能对应于,例如,同时开发两个独立的特性。将来,这些分支可能会合并,以创建一个包含两个特性的新快照,生成一个具有多个父级的新提交。</p>\n\n<h2>数据模型,作为伪代码</h2>\n\n<p>用伪代码表示 Git 的数据模型可能很有用:</p>\n\n<pre><code>// 文件就是一堆字节\ntype blob = array&lt;byte&gt;\n\n// 目录包含命名文件和目录\ntype tree = map&lt;string, tree | blob&gt;\n\n// 提交有父级、元数据和顶级树\ntype commit = struct {\n    parents: array&lt;commit&gt;\n    author: string\n    message: string\n    snapshot: tree\n}\n</code></pre>\n\n<h2>对象和内容寻址</h2>\n\n<p>\"对象\"是 blob、tree 或 commit。在 Git 数据存储中,所有对象都通过其 SHA-1 哈希进行内容寻址。blob、tree 和 commit 统一这种方式:它们都是对象。当它们引用其他对象时,它们实际上并不包含它们的磁盘表示,而是通过它们的哈希引用它们。</p>\n\n<h2>引用</h2>\n\n<p>现在,所有快照都可以通过它们的 SHA-1 哈希来标识。这很不方便,因为人类不擅长记住 40 个十六进制字符的字符串。</p>\n\n<p>Git 对这个问题的解决方案是人类可读的名称,称为\"引用\"。引用是指向提交的指针。与对象不同,对象是不可变的,引用是可变的(可以更新以指向新的提交)。例如,<code>master</code> 引用通常指向主开发分支的最新提交。</p>\n\n<h2>仓库</h2>\n\n<p>最后,我们可以定义 Git <em>仓库</em>:它是数据 <code>objects</code> 和 <code>references</code>。在磁盘上,Git 只存储对象和引用:因为其数据模型就是这些。所有 <code>git</code> 命令都映射到对提交 DAG 的某种操作,通过添加对象和添加/更新引用。</p>\n\n<h1>暂存区</h1>\n\n<p>Git 还有一个概念叫做\"暂存区\"或\"索引\",这是你想要包含在下一个快照中的更改的一种机制。</p>\n\n<h1>Git 命令行界面</h1>\n\n<p>为了避免重复信息,我们不会详细解释以下命令。有关更多信息,请参阅 Pro Git 中文版或只需使用 <code>git command --help</code>。</p>\n\n<h2>基础</h2>\n\n<ul>\n<li><code>git help &lt;command&gt;</code>:获取 git 命令的帮助</li>\n<li><code>git init</code>:创建一个新的 git 仓库,其数据存储在 <code>.git</code> 目录中</li>\n<li><code>git status</code>:告诉你正在发生什么</li>\n<li><code>git add &lt;filename&gt;</code>:将文件添加到暂存区</li>\n<li><code>git commit</code>:创建一个新提交</li>\n<li><code>git log</code>:显示扁平化的日志历史</li>\n<li><code>git log --all --graph --decorate</code>:可视化历史为 DAG</li>\n<li><code>git diff &lt;filename&gt;</code>:显示自上次提交以来的更改</li>\n<li><code>git diff &lt;revision&gt; &lt;filename&gt;</code>:显示文件在快照之间的差异</li>\n<li><code>git checkout &lt;revision&gt;</code>:更新 HEAD 和当前分支</li>\n</ul>\n\n<h2>分支和合并</h2>\n\n<ul>\n<li><code>git branch</code>:显示分支</li>\n<li><code>git branch &lt;name&gt;</code>:创建分支</li>\n<li><code>git checkout -b &lt;name&gt;</code>:创建分支并切换到它,等同于 <code>git branch &lt;name&gt;; git checkout &lt;name&gt;</code></li>\n<li><code>git merge &lt;revision&gt;</code>:合并到当前分支</li>\n<li><code>git mergetool</code>:使用工具来处理合并冲突</li>\n<li><code>git rebase</code>:将一组补丁变基到新基础</li>\n</ul>\n\n<h2>远程</h2>\n\n<ul>\n<li><code>git remote</code>:列出远程</li>\n<li><code>git remote add &lt;name&gt; &lt;url&gt;</code>:添加远程</li>\n<li><code>git push &lt;remote&gt; &lt;local branch&gt;:&lt;remote branch&gt;</code>:将对象发送到远程,并更新远程引用</li>\n<li><code>git branch --set-upstream-to=&lt;remote&gt;/&lt;remote branch&gt;</code>:设置本地和远程分支之间的对应关系</li>\n<li><code>git fetch</code>:从远程检索对象/引用</li>\n<li><code>git pull</code>:等同于 <code>git fetch; git merge</code></li>\n<li><code>git clone</code>:从远程下载仓库</li>\n</ul>\n\n<h2>撤销</h2>\n\n<ul>\n<li><code>git commit --amend</code>:编辑提交的内容/消息</li>\n<li><code>git reset HEAD &lt;file&gt;</code>:取消暂存文件</li>\n<li><code>git checkout -- &lt;file&gt;</code>:丢弃更改</li>\n</ul>\n\n<h2>高级 Git</h2>\n\n<ul>\n<li><code>git config</code>:Git 高度可定制</li>\n<li><code>git clone --depth=1</code>:浅克隆,不含完整版本历史</li>\n<li><code>git add -p</code>:交互式暂存</li>\n<li><code>git rebase -i</code>:交互式变基</li>\n<li><code>git blame</code>:显示谁最后编辑了哪一行</li>\n<li><code>git stash</code>:暂时移除工作目录的修改</li>\n<li><code>git bisect</code>:通过二分搜索历史(例如用于回归)</li>\n<li><code>.gitignore</code>:指定有意的未跟踪文件以忽略</li>\n</ul>\n\n<h1>杂项</h1>\n\n<ul>\n<li><strong>GUI</strong>:有许多 Git 的 GUI 客户端。我们个人不使用它们,并使用命令行界面。</li>\n<li><strong>Shell 集成</strong>:在 shell 提示符中集成 Git 状态非常方便。</li>\n<li><strong>编辑器集成</strong>:与上面类似,集成编辑器的便利功能。</li>\n<li><strong>工作流程</strong>:我们已经教你 Git 的数据模型,以及一些基础命令;我们没有告诉你遵循什么实践来处理大型项目(例如,有许多关于 Git 工作流程的不同方法)。</li>\n<li><strong>GitHub</strong>:Git 不是 GitHub。GitHub 有一种特定的方式来为 Git 仓库做贡献,称为拉取请求。</li>\n<li><strong>其他 Git 提供商</strong>:GitHub 不是特别的:还有许多 Git 仓库托管,如 GitLab 和 BitBucket。</li>\n</ul>\n\n<h1>资源</h1>\n\n<ul>\n<li><a href=\"https://git-scm.com/book/zh/v2\">Pro Git 中文版</a> 是<strong>强烈推荐的阅读</strong>。前几章涵盖了本讲座涵盖的大部分内容,而后面的章节则有一些有趣的高级材料。</li>\n<li><a href=\"https://ohshitgit.com/\">Oh Shit, Git!?!</a> 是一个简短的指南,介绍如何从一些常见的 Git 错误中恢复。</li>\n<li><a href=\"https://eagain.net/articles/git-for-computer-scientists/\">Git for Computer Scientists</a> 是对 Git 数据模型的简短解释,侧重点与我们略有不同,但如果您对其他领域感兴趣,这可能很有用。</li>\n<li><a href=\"https://smusamashah.github.io/explain-git-in-simple-words\">Git from the Bottom Up</a> 是对 Git 实现细节的详细解释,超出了这篇文章的范围,但如果您感兴趣,可能会很有用。</li>\n<li><a href=\"https://learngitbranching.js.org/\">Learn Git Branching</a> 是一个基于浏览器的游戏,教你 Git。</li>\n</ul>\n\n<h1>练习</h1>\n\n<ol>\n<li>如果您以前没有使用过 Git,请尝试阅读 <a href=\"https://git-scm.com/book/zh/v2\">Pro Git</a> 的前几章或完成像 <a href=\"https://learngitbranching.js.org/\">Learn Git Branching</a> 这样的教程。当您学习 Git 时,请将 Git 命令与数据模型关联起来。</li>\n<li>克隆本课程网站的<a href=\"https://github.com/missing-semester/missing-semester\">仓库</a>。探索版本历史,通过可视化为图形。是谁最后修改了 <code>README.md</code>?最后对 <code>collections:</code> 行的提交消息是什么?</li>\n<li>修改 Git 配置的一个常见错误是提交具有敏感信息的大文件。尝试向仓库添加文件,进行一些提交,然后从历史中删除该文件(您可能想查看<a href=\"https://help.github.com/articles/removing-sensitive-data-from-a-repository/\">这个</a>)。</li>\n<li>从 GitHub 克隆某个仓库,并修改其中一个现有文件。当您使用 <code>git stash</code> 时会发生什么?当您运行 <code>git log --all --oneline</code> 时会发生什么?运行 <code>git stash pop</code> 以撤销您所做的操作。在什么场景下这可能有用?</li>\n<li>像许多命令行工具一样,Git 提供了一个名为 <code>~/.gitconfig</code> 的配置文件(或点文件)。在 <code>~/.gitconfig</code> 中创建一个别名,以便当您运行 <code>git graph</code> 时,您获得 <code>git log --all --graph --decorate --oneline</code> 的输出。</li>\n<li>您可以通过创建全局忽略模式,在 <code>~/.gitignore_global</code> 中定义应该在所有仓库中被忽略的模式。通过编写 <code>~/.gitignore_global</code> 来实现这一点,并使用 <code>git config --global core.excludesfile ~/.gitignore_global</code>。</li>\n<li>通过克隆一个仓库,例如本讲座网站的仓库,使用 <code>git log --all --graph --decorate</code> 来可视化提交图,并手动创建一个新提交,其父级与 HEAD 当前所在位置相同(在 Git 中重复历史是困难的,但如果您操纵 <code>.git</code> 目录中的内容,您会很方便地做到这一点)。</li>\n</ol>",
  "source_hash": "sha256:1882fed561269610d4ac35bc5a461efe73f8481070dd58a45db04a8899598a98",
  "model": "claude-sonnet-4-5-20250929",
  "generated_at": "2026-01-01T00:00:00.000000+00:00"
}
