<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; Commit</title>
	<atom:link href="http://tonybai.com/tag/commit/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Mon, 08 Jun 2026 23:32:23 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>拯救你的Commit Log：Conventional Commits实践指南</title>
		<link>https://tonybai.com/2025/04/24/conventional-commits-guide/</link>
		<comments>https://tonybai.com/2025/04/24/conventional-commits-guide/#comments</comments>
		<pubDate>Thu, 24 Apr 2025 14:09:01 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[Changelog]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[conventional-commits]]></category>
		<category><![CDATA[Cursor]]></category>
		<category><![CDATA[docs]]></category>
		<category><![CDATA[feat]]></category>
		<category><![CDATA[fix]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[major]]></category>
		<category><![CDATA[minor]]></category>
		<category><![CDATA[patch]]></category>
		<category><![CDATA[perf]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[refactor]]></category>
		<category><![CDATA[release]]></category>
		<category><![CDATA[semver]]></category>
		<category><![CDATA[style]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[type]]></category>
		<category><![CDATA[插件]]></category>
		<category><![CDATA[语义化版本]]></category>
		<category><![CDATA[软件工程]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4613</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/04/24/conventional-commits-guide 告别混乱Commit Log！用规范指引你写出有意义的提交！ 大家好，我是Tony Bai。 Git的Commit Log (提交日志) 是项目演进的脉络，也是开发者之间沟通变更、追溯历史、理解代码演变的关键载体。然而，在实际开发中，我们常常面对杂乱无章、意义不明的提交信息——”fix bug”、”update code”、”wip” 等屡见不鲜。这些模糊的记录不仅让代码审查、问题排查和版本追溯变得异常困难，也阻碍了自动化流程的实施。Conventional Commits (约定式提交) 规范提供了一套清晰、简洁的指引，旨在将每一次提交都转化为有意义、结构化的信息单元，从而显著提升 Commit Log 的价值和可利用性。 在这篇文章中，我们将探讨Conventional Commits如何作为一项关键指引，帮助开发者和团队构建更清晰、更一致、更具信息量的提交历史。 1. Commit Log的困境：为何需要指引？ 缺乏明确指引的Commit Log往往会陷入以下困境： 信息熵高，有效信息少: 大量模糊、随意的提交信息混杂在一起，难以快速定位关键变更或理解特定提交的目的。 沟通效率低下: 团队成员需要花费额外时间去解读他人的提交意图，代码审查效率降低。 历史追溯困难: 当需要回溯某个功能或 Bug 的引入/修复历史时，无结构的日志如同大海捞针。 自动化阻碍: 不一致、不可预测的提交信息使得自动化生成 Changelog、语义化版本控制（SemVer）等流程难以实现。 面对这些普遍存在的困境，业界亟需一套行之有效的规范来引导开发者记录更有价值的提交信息。这正是 Conventional Commits 规范所要解决的核心问题，它通过引入一套简洁而强大的结构化指引来实现这一目标。Conventional Commits并非强制性的铁律，而是一套强大的指引 (Guidance)，它通过引入轻量级的结构化约定，引导开发者在提交时思考并明确表达变更的性质、范围和影响。 2. Conventional Commits 核心指引：结构化的力量 该规范的核心指引体现在其简洁的提交信息结构上(如下所示)： &#60;type&#62;[optional scope]: &#60;description&#62; [optional body] [optional [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/conventional-commits-guide-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2025/04/24/conventional-commits-guide">本文永久链接</a> &#8211; https://tonybai.com/2025/04/24/conventional-commits-guide</p>
<blockquote>
<p>告别混乱Commit Log！用规范指引你写出有意义的提交！</p>
</blockquote>
<p>大家好，我是Tony Bai。</p>
<p>Git的Commit Log (提交日志) 是项目演进的脉络，也是开发者之间沟通变更、追溯历史、理解代码演变的关键载体。然而，在实际开发中，我们常常面对杂乱无章、意义不明的提交信息——”fix bug”、”update code”、”wip” 等屡见不鲜。这些模糊的记录不仅让代码审查、问题排查和版本追溯变得异常困难，也阻碍了自动化流程的实施。<a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits (约定式提交) 规范</a>提供了一套清晰、简洁的指引，旨在将每一次提交都转化为有意义、结构化的信息单元，从而显著提升 Commit Log 的价值和可利用性。</p>
<p>在这篇文章中，我们将探讨Conventional Commits如何作为一项关键指引，帮助开发者和团队构建更清晰、更一致、更具信息量的提交历史。</p>
<h2>1. Commit Log的困境：为何需要指引？</h2>
<p>缺乏明确指引的Commit Log往往会陷入以下困境：</p>
<ul>
<li><strong>信息熵高，有效信息少:</strong> 大量模糊、随意的提交信息混杂在一起，难以快速定位关键变更或理解特定提交的目的。</li>
<li><strong>沟通效率低下:</strong> 团队成员需要花费额外时间去解读他人的提交意图，代码审查效率降低。</li>
<li><strong>历史追溯困难:</strong> 当需要回溯某个功能或 Bug 的引入/修复历史时，无结构的日志如同大海捞针。</li>
<li><strong>自动化阻碍:</strong> 不一致、不可预测的提交信息使得自动化生成 Changelog、语义化版本控制（SemVer）等流程难以实现。</li>
</ul>
<p>面对这些普遍存在的困境，业界亟需一套行之有效的规范来引导开发者记录更有价值的提交信息。这正是 Conventional Commits 规范所要解决的核心问题，它通过引入一套简洁而强大的结构化指引来实现这一目标。Conventional Commits并非强制性的铁律，而是一套<strong>强大的指引 (Guidance)</strong>，它通过引入轻量级的结构化约定，引导开发者在提交时思考并明确表达变更的<strong>性质、范围和影响</strong>。</p>
<h2>2. Conventional Commits 核心指引：结构化的力量</h2>
<p>该规范的核心指引体现在其简洁的提交信息结构上(如下所示)：</p>
<pre><code>&lt;type&gt;[optional scope]: &lt;description&gt;

[optional body]

[optional footer(s)]
</code></pre>
<p>遵循这项指引，每次提交都应包含以下关键要素：</p>
<ul>
<li>
<p><strong>Type (类型):</strong> <strong>[必须遵循的指引]</strong> 表明提交的性质。规范定义了基础类型：</p>
<ul>
<li>fix:：修复 Bug (对应 SemVer PATCH)。</li>
<li>feat:：引入新功能 (对应 SemVer MINOR)。</li>
<li><strong>鼓励扩展:</strong> 团队可以根据需要定义其他类型，如 build, chore(用于标记那些不涉及新特性或修复的常规维护工作，比如更新依赖项等), ci, docs, style, refactor, perf, test等，以适应具体工作流。这些扩展类型本身通常不直接影响版本号（除非包含破坏性变更）。</li>
</ul>
</li>
<li>
<p><strong>Scope (范围):</strong> <strong>[可选但推荐的指引]</strong> 明确提交影响的代码库区域或模块，用括号包裹，如 feat(api): 或 fix(parser):。这极大地增强了信息的可定位性。</p>
</li>
<li>
<p><strong>Description (描述):</strong> <strong>[必须遵循的指引]</strong> 紧跟冒号和空格，用简洁的语言（推荐使用祈使句现在时）概括本次提交的核心变更内容。这是提交信息的“标题”。</p>
</li>
<li>
<p><strong>Body (正文):</strong> <strong>[可选指引]</strong> 当简短描述不足以说明时，提供更详细的上下文、动机和实现细节。与 Description 之间需空一行。</p>
</li>
<li>
<p><strong>Footer(s) (脚注):</strong> <strong>[可选指引]</strong> 提供元数据，如关联 Issue (Refs: #123)。特别重要的两个脚注指引：</p>
<ul>
<li>BREAKING CHANGE: <description>：明确标示不兼容的 API 变更 (对应 SemVer MAJOR)。</li>
<li>INITIAL STABLE RELEASE: <description>：标记项目从 0.y.z 进入 1.0.0。</li>
</ul>
</li>
</ul>
<p><strong>强调重要变更的简化指引：</strong> 规范还提供了 ! (紧跟 type 或 scope 之后) 和 !! 作为标记 BREAKING CHANGE 和 INITIAL STABLE RELEASE 的快捷方式，进一步简化遵循指引的实践。</p>
<p>为了更直观地理解这个结构，以下是一些典型的Conventional Commits示例：</p>
<ul>
<li><strong>简单的 Bug 修复:</strong></li>
</ul>
<pre><code>fix: correct minor typos in documentation
</code></pre>
<ul>
<li><strong>带范围的新功能:</strong></li>
</ul>
<pre><code>feat(lang): add Polish language support
</code></pre>
<ul>
<li><strong>使用 ! 标记破坏性变更:</strong></li>
</ul>
<pre><code>refactor!(auth): remove deprecated JWT authentication method
</code></pre>
<p>注意：这里的 ! 表明这是一个破坏性变更，即使type是refactor。</p>
<ul>
<li><strong>包含详细正文和脚注的提交:</strong></li>
</ul>
<pre><code>perf(api): improve user query performance significantly

Implemented a new indexing strategy for the users table and optimized
the SQL query execution plan. Initial tests show a 50% reduction
in average query latency under heavy load.

Reviewed-by: Alice &lt;alice@example.com&gt;
Refs: #456, #478
</code></pre>
<ul>
<li><strong>使用 !! 标记首次稳定版发布:</strong></li>
</ul>
<pre><code>chore(release)!!: prepare for 1.0.0 stable release

Finalized documentation, updated dependencies, and ran comprehensive
end-to-end tests to ensure stability for the first major release.

INITIAL STABLE RELEASE: The project is now considered stable for production use.
</code></pre>
<p>通过遵循这些简单的指引，原本混乱的Commit Log就被转化为结构清晰、信息丰富的记录。</p>
<p>理解了 Conventional Commits 的核心结构和要素后，我们自然会问：遵循这项指引究竟能为开发者和团队带来哪些实实在在的好处？答案是多方面的，它能让原本静态、难以利用的 Commit Log “活”起来，释放出巨大的潜在价值。</p>
<p>首先，结构化的 type 和 scope 提升了可读性与可理解性，使团队能够快速筛选和定位信息，清晰的 description 和 body 阐述了变更的“什么”和“为什么”。</p>
<p>其次，一致的格式增强了团队沟通与协作，减少了误解，提高了代码审查和协作效率，使每一次提交都成为清晰的沟通。</p>
<p>此外，结构化的日志简化了历史追溯与问题排查，便于查找特定功能引入、Bug 修复或破坏性变更的源头。</p>
<p>最后，一个充满有意义提交的日志自然而然地成为自动化工具的理想输入，能够驱动自动化生成 CHANGELOG、自动化 SemVer 版本判断，以及基于提交类型触发不同的 CI/CD 流程。</p>
<p>认识到 Conventional Commits 带来的显著价值后，如何在日常开发中有效地遵循并最大化其效益，就成了一个关键问题。仅仅了解规范的语法是不够的，掌握一些最佳实践和深入的洞察，能帮助我们更好地将这项指引融入工作流。</p>
<h2>3. 遵循指引的最佳实践与洞察</h2>
<p>为了更好地应用Conventional Commits指引，以下几点值得关注：</p>
<ul>
<li>
<p><strong>原子化提交:</strong> 我们鼓励将复杂的变更分解为多个逻辑上独立的、遵循单一type的提交。这本身就是一种良好的 Git 实践，很多大厂的git commit规范以及代码review规范也是这么要求的。Conventional Commits 进一步强化了这一点。</p>
</li>
<li>
<p><strong>选择最合适的Type:</strong> 当一次提交包含多种类型的变更时（虽然应尽量避免），选择最能代表其核心意图的 type，并在 Body 中详述其他变更。</p>
</li>
<li>
<p><strong>祈使句现在时:</strong> 推荐使用如 “Add feature”、”Fix bug” 的风格撰写 Description，简洁、直接，如同给代码库下达指令。</p>
</li>
<li>
<p><strong>利用工具辅助:</strong> 社区提供了丰富的工具（如<a href="https://commitizen-tools.github.io/commitizen/">Commitizen</a>, <a href="https://commitlint.js.org/">commitlint</a>等）来帮助开发者遵循规范格式，并在提交前进行校验，降低遵循指引的负担。</p>
</li>
<li>
<p><strong>团队共识与逐步采纳:</strong> 引入规范需要团队达成共识。可以通过分享、讨论和使用工具逐步推广。</p>
</li>
</ul>
<p>当然，良好实践的推广离不开工具的支持。幸运的是，围绕 Conventional Commits 已经形成了一个活跃的社区和丰富的工具生态系统，它们极大地降低了开发者遵循规范的门槛，让指引更容易落地。</p>
<h2>4. 社区生态：工具让指引落地</h2>
<p>Conventional Commits 的流行离不开活跃的社区和丰富的工具支持，它们帮助开发者轻松地将这项指引融入日常工作流：</p>
<ul>
<li><strong>Commitizen:</strong> 交互式命令行工具，引导用户创建符合规范的提交信息。</li>
<li><strong>Commitlint:</strong> 用于校验提交信息是否符合规范，常与 Git Hooks (如 husky) 集成。</li>
<li><strong>IDE 插件:</strong> 主流 IDE (VS Code, JetBrains IDEs 等) 均有插件提供模板、补全和校验支持。</li>
<li><strong>自动化版本与 Changelog 工具:</strong> 如 <a href="https://github.com/semantic-release/semantic-release">semantic-release</a>, <a href="https://github.com/goreleaser/chglog">goreleaser/chglog</a>等，它们消费符合规范的提交历史。</li>
</ul>
<p>这两年基于大模型的辅助生成commit log的工具以及一些代码智能体应用(如Cursor等）也在规范git commit log方面起到了非常积极的作用，对于像我这样英语非母语但又喜欢以英文log提交的选手来说，这些工具大幅降低了我在纠结如何写commit log时的心智负担，给予了我很大的帮助。</p>
<h2>5. 小结</h2>
<p>总而言之，Conventional Commits 远不止一套冷冰冰的格式规则，它更像是一位<strong>贴心的向导</strong>，一项旨在<strong>将每一次提交都转化为宝贵信息资产</strong>的核心指引。它赋予我们结构化的力量，能够将困扰许多团队的<strong>混乱、低效的Commit Log</strong>，转变为<strong>清晰、一致且富有洞察力</strong>的项目演进历史——这对于提升代码可维护性、团队协作效率乃至自动化流程都至关重要。</p>
<p>现在，<strong>就将这项指引融入你的日常开发吧！</strong> 让每一次git commit不再是随意的记录，而是对项目演进负责任的、有意义的贡献。</p>
<p><strong>那么，你的团队是如何采纳和实践提交规范的？你在使用Conventional Commits或其他规范时，有什么独到的心得或踩过的“坑”吗？</strong></p>
<p><strong>非常期待在评论区看到你的分享与交流！</strong></p>
<p>如果这篇文章让你觉得“提交信息确实应该更有意义”，请分享给你的同事或团队，一起提升代码库的 Commit Log 质量吧!</p>
<p>别忘了<strong>关注我</strong>，持续获取更多提升研发效能的实用技巧与深度解析。</p>
<h2>6. 参考资料</h2>
<ul>
<li><a href="https://www.conventionalcommits.org/en/v1.0.0/#specification">Conventional Commits v1.0.0</a> &#8211; https://www.conventionalcommits.org/en/v1.0.0/#specification</li>
</ul>
<hr />
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/04/24/conventional-commits-guide/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go模块发布流程再加固：go mod verify -tag提案详解</title>
		<link>https://tonybai.com/2025/03/28/go-mod-verify-tag/</link>
		<comments>https://tonybai.com/2025/03/28/go-mod-verify-tag/#comments</comments>
		<pubDate>Fri, 28 Mar 2025 00:16:36 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[git-tag]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-mod-verify]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[sumdb]]></category>
		<category><![CDATA[模块]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4528</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/03/28/go-mod-verify-tag Go模块(module)在Go 1.11版本中引入，显著简化了依赖管理，使开发者能够通过go.mod文件明确声明和管理库依赖，支持语义版本控制，并提高了构建速度和可移植性。使得Go语言的依赖管理更加现代化和高效，提升了开发者的体验。 同时引入的校验和数据库 (sumdb) 也极大地增强了Go生态的依赖管理的确定性和安全性。然而，在模块作者发布新版本时，从本地代码库打上标签推送到代码托管平台，再到被Go Proxy和sumdb收录，这个过程中仍然存在一个微妙但关键的信任验证环节缺失。近期，Go团队接受了一项备受关注的提案(Issue #68669，旨在通过扩展go mod verify命令来弥补这一空白，为模块作者提供一种官方途径来验证他们本地的代码和标签确实与Go生态系统将收录的版本一致。在这一篇文章中，我就根据issue中的内容，来简单介绍一下这一新增安全机制的背景和运作原理。 注：该机制的提案刚刚被Accept，尚未确定在哪个版本落地，不过大概率是在Go 1.25版本中。 1. 问题背景：发布过程中的信任鸿沟 当前，Go开发者在发布一个新的模块版本时，通常的流程是： 在本地代码库完成开发和测试。 使用git tag (例如git tag v1.2.3) 创建版本标签。 使用git push &#8211;tags 将代码和标签推送到代码托管平台 (如 GitHub)。 等待Go Proxy (如proxy.golang.org) 拉取新版本，并将其信息提交给官方sumdb。 虽然sumdb保证了下游用户下载的模块代码未被篡改 (相对于sumdb中的记录)，但它无法保证sumdb中记录的版本就精确地是模块作者在本地打标签时所期望的版本。潜在的风险点包括： 代码托管平台被篡改: 拥有强制推送权限的攻击者可能在标签推送后修改了标签指向的提交。 代码托管平台自身问题: 平台自身可能存在Bug或被攻击，导致返回给Go Proxy的代码与原始标签不符。 Go Proxy或sumdb问题: 尽管概率较低，但中间环节也可能存在问题。 正如提案贡献者和Go核心团队成员在讨论中指出的，目前缺少一个简单直接的方式让模块作者确认：“我本地标记为v1.2.3的代码，是否就是全世界通过Go工具链获取到的那个v1.2.3？”。 2. 提案核心：go mod verify -tag 为了解决这个问题，提案#68669建议为现有的go mod verify命令增加一个新的-tag标志。go mod verify命令目前用于检查本地缓存的依赖项是否被修改，而新的-tag标志则将关注点转向了当前模块本身。 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-mod-verify-tag-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/03/28/go-mod-verify-tag">本文永久链接</a> &#8211; https://tonybai.com/2025/03/28/go-mod-verify-tag</p>
<p><a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go模块(module)在Go 1.11版本中引入</a>，显著简化了依赖管理，使开发者能够通过go.mod文件明确声明和管理库依赖，支持语义版本控制，并提高了构建速度和可移植性。使得Go语言的依赖管理更加现代化和高效，提升了开发者的体验。</p>
<p>同时引入的校验和数据库 (sumdb) 也极大地增强了Go生态的依赖管理的确定性和安全性。然而，在模块作者发布新版本时，从本地代码库打上标签推送到代码托管平台，再到被Go Proxy和sumdb收录，这个过程中仍然存在一个微妙但关键的信任验证环节缺失。近期，Go团队接受了一项备受关注的提案(<a href="https://github.com/golang/go/issues/68669">Issue #68669</a>，旨在通过扩展go mod verify命令来弥补这一空白，为模块作者提供一种官方途径来验证他们本地的代码和标签确实与Go生态系统将收录的版本一致。在这一篇文章中，我就根据issue中的内容，来简单介绍一下这一新增安全机制的背景和运作原理。</p>
<p><img src="https://tonybai.com/wp-content/uploads/go-mod-verify-tag-2.png" alt="" /></p>
<blockquote>
<p>注：该机制的提案刚刚被Accept，尚未确定在哪个版本落地，不过大概率是在<a href="https://github.com/golang/go/milestone/364">Go 1.25版本</a>中。</p>
</blockquote>
<h2>1. 问题背景：发布过程中的信任鸿沟</h2>
<p>当前，Go开发者在发布一个新的模块版本时，通常的流程是：</p>
<ul>
<li>在本地代码库完成开发和测试。</li>
<li>使用git tag <version> (例如git tag v1.2.3) 创建版本标签。</li>
<li>使用git push &#8211;tags 将代码和标签推送到代码托管平台 (如 GitHub)。</li>
<li>等待Go Proxy (如proxy.golang.org) 拉取新版本，并将其信息提交给官方sumdb。</li>
</ul>
<p>虽然sumdb保证了下游用户下载的模块代码未被篡改 (相对于sumdb中的记录)，但它无法保证sumdb中记录的版本就<strong>精确地</strong>是模块作者在本地打标签时所期望的版本。潜在的风险点包括：</p>
<ul>
<li><strong>代码托管平台被篡改</strong>: 拥有强制推送权限的攻击者可能在标签推送后修改了标签指向的提交。</li>
<li><strong>代码托管平台自身问题</strong>: 平台自身可能存在Bug或被攻击，导致返回给Go Proxy的代码与原始标签不符。</li>
<li><strong>Go Proxy或sumdb问题</strong>: 尽管概率较低，但中间环节也可能存在问题。</li>
</ul>
<p>正如提案贡献者和Go核心团队成员在讨论中指出的，目前缺少一个简单直接的方式让模块作者确认：“我本地标记为v1.2.3的代码，是否就是全世界通过Go工具链获取到的那个v1.2.3？”。</p>
<h2>2. 提案核心：go mod verify -tag</h2>
<p>为了解决这个问题，提案#68669建议为现有的go mod verify命令增加一个新的-tag标志。go mod verify命令目前用于检查本地缓存的依赖项是否被修改，而新的-tag标志则将关注点转向了<strong>当前模块本身</strong>。</p>
<h3>2.1 拟议的功能</h3>
<pre><code>$go mod verify -tag=&lt;value&gt;
</code></pre>
<p>其中 <value> 可以是：</p>
<ul>
<li><strong><version></strong>: 一个具体的 Git 标签，例如v1.2.3。命令将检查本地仓库中该标签对应的代码树，计算其哈希，并与sumdb中记录的该版本的哈希进行比对。</li>
<li><strong>latest</strong>: 检查本地仓库中最新的Git标签。</li>
<li><strong>all</strong>: 检查本地仓库中所有的Git标签。</li>
</ul>
<h3>2.2 核心价值与使用场景</h3>
<ol>
<li><strong>发布后验证 (主要场景)</strong>：这是该提案最核心的预期用途。模块作者在推送标签后，可以立即运行此命令来确认他们的代码已经“安全”地进入了Go的模块分发体系，且内容无误。</li>
</ol>
<pre><code># 假设已完成开发
$git tag v1.2.3
$git push origin v1.2.3 # 或 git push --tags

# 关键一步：验证刚推送的标签
$go mod verify -tag=v1.2.3
</code></pre>
<p>这个操作还有一个重要的<strong>副作用</strong>：如果v1.2.3 尚未被Go Proxy和sumdb收录，运行go mod verify -tag=v1.2.3 会<strong>触发Go工具链去查询这个版本，从而加速它被Go生态系统发现和记录的过程，同时完成验证</strong>。</p>
<ol>
<li><strong>安全审计与代码审查</strong>: 当需要对某个模块的特定版本进行安全审计或深入的代码审查时，可以使用此命令验证本地检出的代码副本确实是sumdb中记录的那个“官方”版本，而不是可能已被篡改的某个代码托管平台上的版本。</li>
</ol>
<h2>3 社区讨论与设计考量</h2>
<p>在提案的讨论过程中，社区也探讨了该功能是否应该放在go mod verify命令下，因为它与验证依赖项的现有功能有所不同。一些替代方案被提出，例如创建一个新的子命令go mod verify-tags或go mod proxy -check=TAG等。</p>
<p>最终，提案审查小组倾向于并接受了将此功能作为go mod verify的扩展，主要是考虑到：</p>
<ul>
<li><strong>概念一致性</strong>: 虽然对象不同（当前模块 vs 依赖项），但核心都是进行某种形式的“验证” (verify)。</li>
<li><strong>避免命令扩散</strong>: 增加标志比增加新子命令更轻量。</li>
<li><strong>文档可更新</strong>: 可以通过更新go mod verify 的文档来清晰地说明其扩展后的功能范围。</li>
</ul>
<p>需要注意的是，该提案主要解决的是<strong>模块作者</strong>验证<strong>自身发布</strong>的问题，与验证项目<strong>依赖项</strong>是否在源头（如GitHub）被篡改（例如<a href="https://github.com/golang/go/issues/66653">Issue #66653</a>讨论的情况）是不同的问题，尽管它们都属于Go模块供应链安全的一部分。</p>
<h2>4. 小结</h2>
<p><a href="https://github.com/golang/go/issues/33502#issuecomment-2755907453">go mod verify -tag提案的接受</a>是Go模块生态系统在安全性方面迈出的又一重要步伐。它为模块作者提供了一个简单、官方的工具来关闭发布流程中的一个关键信任缺口，增强了从代码编写到模块分发的端到端完整性保证。</p>
<p>虽然具体的实现细节仍在进行中 (由 Issue #68669 跟踪)，但Go开发者可以期待在未来的Go版本中获得这一实用功能。这不仅有助于提升个别模块的安全性，也将进一步巩固整个Go生态系统的供应链安全基础。</p>
<h2>5. 参考资料</h2>
<ul>
<li>Go Issue #68669: <a href="https://github.com/golang/go/issues/68669">https://github.com/golang/go/issues/68669</a> &#8211; https://github.com/golang/go/issues/68669</li>
<li>相关变更CL: <a href="https://go.dev/cl/596097">https://go.dev/cl/596097</a> &#8211; https://go.dev/cl/596097</li>
</ul>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/03/28/go-mod-verify-tag/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>代码提交者的代码评审通关指南[译]</title>
		<link>https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review/</link>
		<comments>https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review/#comments</comments>
		<pubDate>Thu, 10 Oct 2024 22:13:00 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[changelist]]></category>
		<category><![CDATA[cl]]></category>
		<category><![CDATA[Codereview]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[conflict]]></category>
		<category><![CDATA[feature]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[GoogleStyleGuide]]></category>
		<category><![CDATA[Google软件工程]]></category>
		<category><![CDATA[patch]]></category>
		<category><![CDATA[pb]]></category>
		<category><![CDATA[PR]]></category>
		<category><![CDATA[Programmar]]></category>
		<category><![CDATA[refactor]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[Test]]></category>
		<category><![CDATA[代码评审]]></category>
		<category><![CDATA[冲突]]></category>
		<category><![CDATA[切斯特顿的栅栏]]></category>
		<category><![CDATA[变更]]></category>
		<category><![CDATA[审查]]></category>
		<category><![CDATA[惯例]]></category>
		<category><![CDATA[标准]]></category>
		<category><![CDATA[标签]]></category>
		<category><![CDATA[测试]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[补丁]]></category>
		<category><![CDATA[重构]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4336</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review Google在软件工程领域对IT界做出了卓越的贡献，从《Google软件工程》，到Google Style Guides，再到The Change Author’s Guide。这些实践参考不仅提升了软件工程的标准，也为全球IT行业的发展提供了宝贵的资源和指导。由于Go是Google开源的，其cl review基本上是遵循了Google内部的标准和实践，可以帮助开发人员更快地完成审核并获得更高质量的结果。因此在这篇文章中，我翻译一下The Change Author’s Guide，供大家参考。 The Change Author’s Guide分为三部分，由于每一部分篇幅都不多，这里就放在一起了。本次翻译是基于Google Engineering Practices Documentation的commit 3bb3ec25b3b0199f4940b1aa75f0ac5c5753301c进行的。 注：Google内部使用的术语CL代表“变更列表(changelist)”，指的是一个自包含的更改，该更改已经提交到版本控制系统或正在进行代码评审。其他组织通常称之为“变更”、“补丁”或“拉取请求(PR)”。 1. 编写良好的CL描述 CL描述是变更的公开记录，重要的是它能够传达以下信息： 做了什么 变更？这应该总结主要的变化，使读者在不需要阅读整个CL的情况下了解正在发生的变化。 为什么要做出这些变更？作为作者，你在做出这个变更时有什么背景？以及你做出的那些在源代码中无法反映出来的决策？等等。 CL描述将成为我们版本控制历史的一部分，未来可能会被数百人阅读。 未来的开发人员将根据描述搜索你的CL。未来某人可能因为对其相关性有模糊的记忆而寻找你的变更，但没有具体细节。如果所有重要信息都在代码中而非描述中，他们将更难找到你的CL。 而且，在他们找到CL后，是否能够理解为什么做出这个变更？阅读源代码可能会揭示软件在做什么，但可能不会揭示其存在的原因，这可能会使未来的开发人员更难知道他们是否可以移动切斯特顿的栅栏(Chesterton&#8217;s fence)。 译注：切斯特顿的栅栏是一种启发式方法，由G.K.切斯特顿提出，旨在告诫人们在改变任何系统之前，应先了解该系统存在的原因和功能，否则可能会造成更大的问题。 一个编写良好的CL描述将帮助这些未来的工程师——有时，也包括你自己！ 1.1 第一行(first line) 简短总结所做的内容。 使用完整句子，以命令的形式书写。 后面跟一个空行。 CL描述的第一行应该是对具体做了什么的简短总结，后面跟一个空行。这是出现在版本控制历史摘要中的内容，因此应该提供足够的信息，使未来的代码搜索者无需阅读你的CL或其整个描述就能理解你的CL实际上做了什么，或与其他CL的不同之处。也就是说，第一行应该独立存在，让读者更快地浏览代码历史。 尽量保持第一行简短、重点突出且切中要点。清晰性和对读者的实用性应是最重要的。 按照传统，CL描述的第一行应该是一个完整的句子，并以命令形式书写（即祈使句）。例如，应该说“Delete the FizzBuzz RPC and replace it with the new system.”，而不是“Deleting the FizzBuzz [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-cl-author-guide-to-getting-through-code-review-1.png" alt="" /></p>
<p><a href="https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review">本文永久链接</a> &#8211; https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review</p>
<p>Google在软件工程领域对IT界做出了卓越的贡献，从《<a href="https://book.douban.com/subject/35838155/">Google软件工程</a>》，到<a href="https://google.github.io/styleguide/">Google Style Guides</a>，再到<a href="https://google.github.io/eng-practices/review/developer/">The Change Author’s Guide</a>。这些实践参考不仅提升了软件工程的标准，也为全球IT行业的发展提供了宝贵的资源和指导。由于Go是Google开源的，其cl review基本上是遵循了Google内部的标准和实践，可以帮助开发人员更快地完成审核并获得更高质量的结果。因此在这篇文章中，我翻译一下The Change Author’s Guide，供大家参考。</p>
<p>The Change Author’s Guide分为三部分，由于每一部分篇幅都不多，这里就放在一起了。本次翻译是基于<a href="https://github.com/google/eng-practices">Google Engineering Practices Documentation</a>的commit 3bb3ec25b3b0199f4940b1aa75f0ac5c5753301c进行的。</p>
<blockquote>
<p>注：Google内部使用的术语CL代表“变更列表(changelist)”，指的是一个自包含的更改，该更改已经提交到版本控制系统或正在进行代码评审。其他组织通常称之为“变更”、“补丁”或“拉取请求(PR)”。</p>
</blockquote>
<hr />
<h2>1. 编写良好的CL描述</h2>
<p>CL描述是变更的公开记录，重要的是它能够传达以下信息：</p>
<ul>
<li>做了<strong>什么</strong> 变更？这应该总结主要的变化，使读者在不需要阅读整个CL的情况下了解正在发生的变化。</li>
<li><strong>为什么</strong>要做出这些变更？作为作者，你在做出这个变更时有什么背景？以及你做出的那些在源代码中无法反映出来的决策？等等。</li>
</ul>
<p>CL描述将成为我们版本控制历史的一部分，未来可能会被数百人阅读。</p>
<p>未来的开发人员将根据描述搜索你的CL。未来某人可能因为对其相关性有模糊的记忆而寻找你的变更，但没有具体细节。如果所有重要信息都在代码中而非描述中，他们将更难找到你的CL。</p>
<p>而且，在他们找到CL后，是否能够理解<strong>为什么</strong>做出这个变更？阅读源代码可能会揭示软件在做什么，但可能不会揭示其存在的原因，这可能会使未来的开发人员更难知道他们是否可以移动<a href="https://abseil.io/resources/swe-book/html/ch03.html#understand_context">切斯特顿的栅栏(Chesterton&#8217;s fence)</a>。</p>
<blockquote>
<p>译注：切斯特顿的栅栏是一种启发式方法，由G.K.切斯特顿提出，旨在告诫人们在改变任何系统之前，应先了解该系统存在的原因和功能，否则可能会造成更大的问题。</p>
</blockquote>
<p>一个编写良好的CL描述将帮助这些未来的工程师——有时，也包括你自己！</p>
<h3>1.1 第一行(first line)</h3>
<ul>
<li>简短总结所做的内容。</li>
<li>使用完整句子，以命令的形式书写。</li>
<li>后面跟一个空行。</li>
</ul>
<p>CL描述的<strong>第一行</strong>应该是对具体做了什么的简短总结，后面跟一个空行。这是出现在版本控制历史摘要中的内容，因此应该提供足够的信息，使未来的代码搜索者无需阅读你的CL或其整个描述就能理解你的CL实际上做了什么，或与其他CL的不同之处。也就是说，第一行应该独立存在，让读者更快地浏览代码历史。</p>
<p>尽量保持第一行简短、重点突出且切中要点。清晰性和对读者的实用性应是最重要的。</p>
<p>按照传统，CL描述的第一行应该是一个完整的句子，并以命令形式书写（即祈使句）。例如，应该说“Delete the FizzBuzz RPC and replace it with the new system.”，而不是“Deleting the FizzBuzz RPC and replacing it with the new system.”，不过，你不必将其余的描述写成祈使句。</p>
<h3>1.2 主体信息要丰富</h3>
<p>第一行应该是简短且重点突出的摘要，而其余的描述应详细说明并包括读者理解变更列表所需的任何补充信息。它可能包括对正在解决的问题的简要描述，以及为什么这是最佳方法。如果该方法有任何不足之处，应该指出。如果有相关信息也要列出，包含背景信息，如错误编号、基准测试结果和设计文档链接等。</p>
<p>如果你包含外部资源的链接，请考虑由于访问限制或保留政策，未来读者可能无法看到这些链接。在可能的情况下，包含足够的上下文，以便审查者和未来读者理解CL。</p>
<p>即使是小的CL也值得关注细节。将CL放在上下文中。</p>
<h3>1.3 不好的CL描述</h3>
<p>“Fix bug”是一个不充分的CL描述。什么bug？你做了什么来修复它？其他类似的不好的描述包括：</p>
<ul>
<li>“Fix build.”</li>
<li>“Add patch.”</li>
<li>“Moving code from A to B.”</li>
<li>“Phase 1.”</li>
<li>“Add convenience functions.”</li>
<li>“kill weird URLs.”</li>
</ul>
<p>其中一些都是取自真实的CL描述。虽然简短，但它们没有提供足够的有用信息。</p>
<h3>1.4 良好的CL描述</h3>
<p>以下是一些好的CL描述示例。</p>
<h4>1.4.1 功能变更</h4>
<p>示例：</p>
<pre><code>RPC: Remove size limit on RPC server message freelist.

Servers like FizzBuzz have very large messages and would benefit from reuse. Make the freelist larger, and add a goroutine that frees the freelist entries slowly over time, so that idle servers eventually release all freelist entries.
</code></pre>
<p>第一行描述了CL实际做了什么。其余的描述谈论了正在解决的问题、为什么这是一个好的解决方案以及有关具体实现的更多信息。</p>
<h4>1.4.2 重构</h4>
<p>示例：</p>
<pre><code>Construct a Task with a TimeKeeper to use its TimeStr and Now methods.

Add a Now method to Task, so the borglet() getter method can be removed (which was only used by OOMCandidate to call borglet's Now method). This replaces the methods on Borglet that delegate to a TimeKeeper.

Allowing Tasks to supply Now is a step toward eliminating the dependency on Borglet. Eventually, collaborators that depend on getting Now from the Task should be changed to use a TimeKeeper directly, but this has been an accommodation to refactoring in small steps.

Continuing the long-range goal of refactoring the Borglet Hierarchy.
</code></pre>
<p>第一行描述了CL做了什么以及这是如何与过去不同的。其余的描述谈论了具体实现、CL的背景、解决方案并不理想以及可能的未来方向。它还解释了为什么这个变更被做出。</p>
<h4>1.4.3 需要一些上下文的小CL</h4>
<p>示例：</p>
<pre><code>Create a Python3 build rule for status.py.

This allows consumers who are already using this as in Python3 to depend on a rule that is next to the original status build rule instead of somewhere in their own tree. It encourages new consumers to use Python3 if they can, instead of Python2, and significantly simplifies some automated build file refactoring tools being worked on currently.
</code></pre>
<p>第一句描述了实际的变更。其余的描述解释了为什么这个变更被做出，并给审查者提供了大量的上下文信息。</p>
<h3>1.5 使用标签(tags)</h3>
<p>标签是手动输入的label，可用于对CL进行分类。这些标签可能由工具支持，也可能只是团队惯例。</p>
<p>例如：</p>
<ul>
<li>“[tag]“</li>
<li>“[a longer tag]“</li>
<li>“#tag”</li>
<li>“tag:”</li>
</ul>
<p>使用标签是可选的。</p>
<p>添加标签时，考虑它们是否应该在CL描述的主体中或第一行中。限制在第一行中使用标签的数量，因为这可能会模糊内容。</p>
<p>以下是带标签和不带标签的示例：</p>
<pre><code>// Tags are okay in the first line if kept short.
[banana] Peel the banana before eating.

// Tags can be inlined in content.
Peel the #banana before eating.

// Tags are optional.
Peel the banana before eating.

// Multiple tags are acceptable if kept short.
#banana #apple: Assemble a fruit basket.

// Tags can go anywhere in the CL description.
&gt; Assemble a fruit basket.
&gt;
&gt; #banana #apple
</code></pre>
<pre><code>// Too many tags (or tags that are too long) overwhelm the first line.
//
// Instead, consider whether the tags can be moved into the description body
// and/or shortened.
[banana peeler factory factory][apple picking service] Assemble a fruit basket.
</code></pre>
<h3>1.6 生成的CL描述</h3>
<p>有些CL是由工具生成的。只要有可能，它们的描述也应该遵循此处的建议。也就是说，它们的第一行应该简短、重点突出且独立，CL描述主体应包含有助于审查者和未来代码搜索者理解每个CL效果的信息细节。</p>
<h3>1.7 提交CL前审查描述</h3>
<p>CL在审查过程中可能会发生重大变化。在提交CL前审查CL描述是值得的，可以确保描述仍然真实反映CL的内容。</p>
<h2>2. 小型CL</h2>
<h3>2.1 为什么要写小型的CL？</h3>
<p>小而简单的CL有以下优点：</p>
<ul>
<li>审查速度更快。审查者更容易找到几分钟的时间来审查小CL，而不是腾出30分钟的时间来审查一个大CL。</li>
<li>审查更彻底。 对于大变更，审查者和作者往往会因大量详细评论反复交换而感到沮丧，有时甚至会错过或忽略重要点。</li>
<li>引入错误的可能性更小。由于你所做的更改较少，因此你和审查者更容易有效地推理CL的影响，并查看是否引入了错误。</li>
<li>被拒绝时浪费的工作更少。 如果你写了一个巨大的CL，然后审查者表示整体方向错误，你就浪费了很多工作。</li>
<li>更容易合并。 处理一个大CL需要很长时间，因此在合并时会遇到许多冲突，你将不得不频繁合并。</li>
<li>更容易设计良好。 完善小变更的设计和代码质量要比完善大变更的所有细节容易得多。</li>
<li>审查阻塞更少。 发送自包含的整体变更部分允许你在等待当前CL审查时继续编码。</li>
<li>回滚更简单。 大CL更可能涉及在初始CL提交和回滚CL之间更新的文件，从而增加回滚的复杂性（中间的CL可能也需要回滚）。</li>
</ul>
<p>请注意，审查者有权仅因为变更过大而直接拒绝你的变更。通常，他们会感谢你的贡献，但会要求你以某种方式将其拆分为一系列较小的变更。在你已经编写完变更后拆分它可能会花费很多时间，或者需要大量时间来争论审查者为什么应该接受你的大变更。因此，最好一开始就写小型CL。</p>
<h3>2.2 多小算小？</h3>
<p>一般而言，CL的合适大小是<strong>一个自包含的变更</strong>。这意味着：</p>
<ul>
<li>CL进行最小变更，只解决<strong>一件事</strong>。这通常只是一个功能的一部分，而不是一次性完成整个功能。一般来说，最好宁可编写太小的CL，也不要编写太大的CL。与你的审核者合作找出可接受的尺寸。</li>
<li>CL应该包含相关的测试代码。</li>
<li>审查者理解CL所需的一切（除未来开发外）都应包含在CL中，比如本CL的描述、现有代码库或他们已经审查过的CL。</li>
<li>系统在CL被检查入库后仍能良好工作，适用于其用户和开发人员。</li>
<li>CL不应小到其含义难以理解。如果你添加了一个新的API，应该在同一个CL中包含对该API的使用方法，以便审查者更好地理解API将如何使用。这也能防止未使用的API被提交。</li>
</ul>
<p>没有关于“过大”的硬性规则。100行通常是合理的CL大小，而1000行通常被认为过大，但这取决于审查者的判断。变更涉及的文件数量也会影响其“大小”。在一个文件中的200行变更可能是可以接受的，但变更分布在50个文件中的话通常会被认为过大。</p>
<p>请记住，尽管你从开始编写代码的那一刻起就与代码密切相关，审查者通常没有上下文。对你来说合适大小的CL可能对审查者来说会是难以接受的。若有疑问，写比你认为需要的更小的CL。审查者很少抱怨收到的CL太小。</p>
<h3>2.3 大型CL什么时候可以？</h3>
<p>在某些情况下，大变更并不那么糟糕：</p>
<ul>
<li>通常可以将删除整个文件视为仅一行变更，因为审查者审核它所花费的时间很少。</li>
<li>有时，大CL是由你完全信任的自动重构工具生成的，审查者的工作只是验证并确认他们确实想要这个变更。这些CL可以更大，尽管上述一些注意事项（例如合并和测试）仍然适用。</li>
</ul>
<h3>2.4 高效地编写小型CL</h3>
<p>如果你编写了一个小型CL，然后等待审查者批准它，再写下一个CL，那么你将浪费很多时间。因此，你需要找到一种方法，在等待审查时不会阻塞自己。这可能涉及同时处理多个项目，找到愿意立即可用的审查者，进行面对面审查，进行配对编程，或者以某种方式拆分你的CL，以便你能够立即继续工作。</p>
<h3>2.5 拆分CL</h3>
<p>如果存在多个相互依赖的CL时，我们通常有必要在深入编码之前从高层次考虑如何拆分和组织这些CL。</p>
<p>除了使你作为作者更容易管理和组织CL外，这也让你的代码审查者更容易，从而使你的代码审查更高效。</p>
<p>以下是将工作拆分为不同CL的一些策略。</p>
<h4>2.5.1 将多个变更堆叠在一起</h4>
<p>拆分CL的一种方法是编写一个小CL，发送审查，然后立即开始编写一个基于第一个CL的另一个CL。大多数版本控制系统都允许你以某种方式做到这一点。</p>
<h4>2.5.2 按文件拆分</h4>
<p>另一种拆分CL的方法是按文件分组，这些文件需要不同的审查者，但其他方面是自包含的变更。</p>
<p>例如：你发送一个CL用于对protocol buffer修改，另一个CL用于对使用该proto的代码的更改。你必须在code CL之前提交proto CL，但它们可以同时接受审查。如果这样做，你可能想通知两组审查者你编写的另一个CL，以便他们了解你的变更的上下文。</p>
<p>另一个例子：你发送一个CL用于代码变更，另一个用于使用该代码的配置或实验；如果有必要，这也更容易回滚，因为配置/实验文件有时比代码变更更快地推送到生产环境。</p>
<h4>2.5.3 横向拆分</h4>
<p>考虑创建共享代码或存根，以帮助隔离技术栈各层之间的变更。这不仅有助于加快开发速度，还鼓励层之间的抽象。</p>
<p>例如：你创建了一个计算器应用程序，其中有客户端、API、服务和数据模型层。共享的proto signature可以将服务层和数据模型层相互抽象。类似地，API存根可以将客户端代码的实现与服务代码分开，使它们能够独立演进。类似的思路也可以应用于更细粒度的函数或类级别的抽象。</p>
<h4>2.5.4 纵向拆分</h4>
<p>与分层的横向方法相对应，你可以将代码拆分为更小、全栈、垂直的功能。这些功能中的每一个都可以独立并行实现。这使得一些轨道能够继续前进，而其他轨道则在等待审查或反馈。</p>
<p>回到我们在横向拆分所举的计算器示例。你现在想支持新的运算符，如乘法和除法。你可以通过将乘法和除法实现为独立的纵向特性或子功能来拆分，尽管它们可能有一些重叠，例如共享按钮样式或共享验证逻辑。</p>
<h4>2.5.5 横向和纵向拆分</h4>
<p>为了进一步发展，你可以结合这些方法并制定一个实施计划，其中每个单元都是独立的CL。从模型（底部）开始，逐渐推进到客户端：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-cl-author-guide-to-getting-through-code-review-2.png" alt="" /></p>
<h3>2.6 将重构与功能变更分开</h3>
<p>通常最好将重构与功能变更或错误修复分开。例如，移动和重命名一个类应该与修复该类中的错误放在不同的CL中。这样，审查者更容易理解每个CL引入的变更。</p>
<p>不过，小的清理工作，例如修复局部变量名称，可以包含在功能变更或错误修复CL中。开发人员和审查者需判断何时重构的规模过大，以至于将其包含在当前CL中会使审查更加困难。</p>
<h3>2.7 将相关的测试代码放在同一个CL中</h3>
<p>CL应该包括相关的测试代码。请记住，这里的“小”指的是CL应该聚焦且不是单纯的行数问题。</p>
<p>所有谷歌的变更都需要测试。</p>
<p>添加或更改逻辑的CL应该伴随新的或更新的测试，以验证新行为。纯重构CL（不打算改变行为）也应有测试覆盖；理想情况下，这些测试已经存在，但如果没有，你应添加它们。</p>
<p>独立的测试修改可以先放入单独的CL，类似于重构准则。这包括：</p>
<ul>
<li>用新测试验证预先存在的提交代码。</li>
</ul>
<p>确保重要逻辑被测试覆盖。增加对受影响代码后续重构的信心。例如，如果你想重构没有测试覆盖的代码，提交测试CL可以在提交重构CL之前可以验证受测行为在重构前后是否保持不变。</p>
<ul>
<li>重构测试代码（例如，引入助手函数）。</li>
<li>引入更大的测试框架代码（例如，集成测试）。</li>
</ul>
<h3>2.8 不要破坏构建</h3>
<p>如果你有多个相互依赖的CL，你需要找到一种方法，在每个CL提交后确保整个系统保持正常工作。否则，你可能会在CL提交之间破坏所有同事的构建，影响大家几分钟（或在稍后的CL提交中出现意外问题时，甚至更长时间）。</p>
<h3>2.9 无法做到足够小</h3>
<p>有时你会遇到CL必须很大的情况。这种情况很少发生。练习编写小CL的作者几乎总能找到将功能分解为一系列小变更的方法。</p>
<p>在编写大CL之前，请考虑是否可以先进行仅重构的CL，以便为更干净的实现铺平道路。与你的团队成员交谈，看看是否有人对如何将功能实现为小CL发表看法。</p>
<p>如果所有这些选项都失败（这应该非常少见），那么请提前获得审查者的同意，以审核大CL，以便他们对即将到来的内容有所警觉。在这种情况下，预计审查过程会比较漫长，要警惕不要引入错误，并更加细致地编写测试。</p>
<h2>3. 如何处理审查者的意见</h2>
<p>当你将代码提交（CL）发送审查时，审查者可能会对你的代码提出多个意见。以下是一些处理审查者意见的有用建议。</p>
<h3>3.1 不要把它视为针对个人</h3>
<p>审查的目标是维护我们的代码库和产品的质量。当审查者对你的代码提出批评时，请将其视为他们试图帮助你、代码库和谷歌的一种方式，而不是对你或你能力的个人攻击。</p>
<p>有时，审查者可能会感到沮丧，并在评论中表达这种沮丧。虽然对于审查者来说，这不是一个好的做法，但作为开发人员，你应该对此有所准备。问问问自己：“审查者想要向我传达的建设性意见是什么？”然后按照他们实际所说的那样进行操作。</p>
<p><strong>绝不要对代码审查意见做出愤怒的回应。</strong> 这是一种严重违反职业礼仪的行为，将在代码审查工具中留下永久记录。如果你太愤怒或烦恼而无法友好地回应，请离开电脑一段时间，或做些其他事情，直到你冷静下来再礼貌地回复。</p>
<p>一般来说，如果审查者没有以建设性和礼貌的方式提供反馈，请当面与他们解释。如果无法面对面或视频通话，那么可以私下发一封邮件给他们。以友好的方式解释你不喜欢的地方以及希望他们做出怎样的改变。如果他们在这次私人讨论中以非建设性的方式回应，或者没有达到预期效果，请酌情上报给你的经理。</p>
<h3>3.2 修正代码</h3>
<p>如果审查者表示他们不理解你代码中的某些内容，你的第一反应应该是澄清代码本身。如果代码无法澄清，请添加代码注释，解释代码存在的原因。如果某个注释似乎没有意义，你再在代码审查工具中做解释。</p>
<p>如果审查者不理解你的某段代码，未来其他读者也可能无法理解。写一条在代码审查工具中的回应并不能帮助未来的代码读者，但澄清代码或添加代码注释则能帮助他们。</p>
<h3>3.3 协作思考</h3>
<p>编写代码变更（CL）可能需要大量工作。最终将其发送审查，感觉一切都完成了，可能会很令人满意，但收到要求更改的评论时也可能会感到沮丧，尤其是当你不同意这些评论时。</p>
<p>在这样的时刻，请花点时间退后一步，考虑审查者是否提供了有价值的反馈，能帮助代码库和谷歌。你首先要问自己，“我理解审查者所要求的吗？”</p>
<p>如果你无法回答这个问题，请向审查者寻求澄清。</p>
<p>然后，如果你理解评论但不同意，重要的是要协作思考，而不是对抗性或防御性思考：</p>
<pre><code>Bad: “No, I’m not going to do that.”
</code></pre>
<pre><code>Good: "I went with X because of [these pros/cons] with [these tradeoffs]
My understanding is that using Y would be worse because of [these reasons].
Are you suggesting that Y better serves the original tradeoffs, that we should
weigh the tradeoffs differently, or something else?"
</code></pre>
<p>请记住，<strong><a href="https://chromium.googlesource.com/chromium/src/+/master/docs/cr_respect.md">礼貌和尊重</a>始终应放在首位</strong>。如果你不同意审查者的观点，请寻找协作的方式：寻求澄清、讨论优缺点，并解释为什么你处理事情的方法更适合代码库、用户和/或谷歌。</p>
<p>有时，你可能知道一些审查者不知道的关于用户、代码库或CL的信息。在适当的地方修复代码，并与审查者进行讨论，提供更多上下文。通常，你可以根据技术事实与审查者达成某种共识。</p>
<h3>3.4 解决冲突</h3>
<p>解决冲突的第一步始终是尝试与审查者达成共识。如果无法达成共识，请参阅<a href="https://google.github.io/eng-practices/review/reviewer/standard.html">代码审查标准</a>，其中提供了在这种情况下应遵循的原则。</p>
<hr />
<p><a href="https://public.zsxq.com/groups/51284458844544">Gopher部落知识星球</a>在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻) &#8211; https://gopherdaily.tonybai.com</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
<li>Gopher Daily归档 &#8211; https://github.com/bigwhite/gopherdaily</li>
<li>Gopher Daily Feed订阅 &#8211; https://gopherdaily.tonybai.com/feed</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2024, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>图解git原理的几个关键概念</title>
		<link>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/</link>
		<comments>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/#comments</comments>
		<pubDate>Tue, 07 Apr 2020 15:10:37 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[blob]]></category>
		<category><![CDATA[branch]]></category>
		<category><![CDATA[clone]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[hash]]></category>
		<category><![CDATA[hub]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MerkleTree]]></category>
		<category><![CDATA[progit]]></category>
		<category><![CDATA[pull]]></category>
		<category><![CDATA[push]]></category>
		<category><![CDATA[SHA-1]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[tag]]></category>
		<category><![CDATA[Tree]]></category>
		<category><![CDATA[trunk]]></category>
		<category><![CDATA[upstream]]></category>
		<category><![CDATA[vcs]]></category>
		<category><![CDATA[以太网]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[分支]]></category>
		<category><![CDATA[哈希值]]></category>
		<category><![CDATA[标签]]></category>
		<category><![CDATA[版本控制]]></category>
		<category><![CDATA[版本管理]]></category>
		<category><![CDATA[集线器]]></category>
		<category><![CDATA[默克尔树]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2884</guid>
		<description><![CDATA[git是那个“爱骂人”的Linux之父Linus Torvalds继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作/git/，而不是/dʒit/。 在诞生十余载后(2005年发布第一版)，git毫无争议地成为了程序员版本管理工具的首选，它改变了全世界程序员的代码版本管理和生产协作的模式，极大促进了开源软件运动的发展。进化到今天的git已经成为了一个比较复杂的工具，多数程序员都将目光聚焦在如何记住这些命令并用好这些命令，对这些复杂命令行背后的原理却知之不多，虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。 关于git原理的文章在互联网上也呈现出“汗牛充栋”之势，有些文章“蜻蜓点水”，有些文章“事无巨细”，看后似乎都无法让我满意。结合自己对git原理的学习，我觉得多数人把握住git运作机制的几个关键概念即可，于是就有了这篇文章，我努力尝试给大家讲清楚。 一. 我就是仓库，我拥有全部 我们首先要明确一个git与先前的版本管理工具（主要是subversion）的不同。下面是使用subversion版本管理工具时，程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式： 图：subversion代码生产和协作模式 众所周知，subversion是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样，所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录；而进行版本管理操作或者与他人进行协作的前提也是：中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”：svn中心仓库就像集线器本身，每个程序员节点就像连接到集线器上的主机；当一个程序员提交(commit)代码到中心仓库时，其他程序员不能提交，否则会出现冲突；如果中心仓库挂掉了，那么整个版本管理过程也将停止，程序员节点间无法进行协作，这就像集线器(hub)挂掉后，所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。 如果我们使用git，我们是不需要“集线器”的： 图：git代码生产和协作模式 如上图所示，git号称分布式版本管理系统，本质上是没有像subversion中那个所谓的“中心仓库”的。每个程序员都拥有一个本地git仓库，而不仅仅是一份代码拷贝，这个仓库就是一个独立的版本管理节点，它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上，程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下，任意两个git仓库之间可以进行点对点的协作，这种协作无需中间协调者(中心仓库)参与。 二. github实现了基于git网络协作的控制平面 git实现了分布式版本管理系统，每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题：如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信)，那么这个分布式网络还缺少一个“控制平面”。 而github恰恰给出了一份git分布式网络控制平面的实现：托管、发现、控制&#8230;。其名称中含有的“hub”字样让我们想起了上面的“hub模式”： 图：github：git分布式网络控制平面的实现 我们看到在github的git协作模式实践中，引入了“中心仓库”的概念，各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同，这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”，也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。 三. objects是个筐，什么都往里面装 上面都是从“宏观”谈git的一些与众不同的理念，而git原理，其实是从这一节才真正开始的^_^。 我们知道：每个git仓库的所有数据都存储在仓库顶层路径下的.git目录下： $tree -L 1 -F . ├── COMMIT_EDITMSG ├── HEAD ├── config ├── description ├── hooks/ ├── index ├── info/ ├── logs/ ├── objects/ └── refs/ 5 directories, 5 files 而在这些目录和文件中，又以objects路径下的数据内容最多，也最为重要。在git的设计中，objects目录就是一个“筐”，git的核心对象(object)都往里面“装”。 图：git核心数据对象类型与objects目录 从上图中，我们看到objects中存储的最主要的有三类对象：blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系，我们通过一个例子来做一下“对号入座”。 我们在一个目录下建立git-internal-repo-demo目录，进入该目录，执行下面命令创建一个git仓库： [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-0.png" alt="img{512x368}" /></p>
<p><a href="https://git-scm.com/">git</a>是那个“爱骂人”的<a href="https://www.kernel.org/">Linux</a>之父<a href="https://github.com/torvalds">Linus Torvalds</a>继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作<strong>/git/</strong>，而不是<strong>/dʒit/</strong>。</p>
<p>在诞生十余载后(2005年发布第一版)，<strong>git</strong>毫无争议地成为了程序员版本管理工具的首选，它改变了全世界程序员的<strong>代码版本管理和生产协作的模式</strong>，极大促进了开源软件运动的发展。进化到今天的<strong>git</strong>已经成为了一个比较复杂的工具，多数程序员都将目光聚焦在如何记住这些命令并用好这些命令，对这些复杂命令行背后的原理却知之不多，虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。</p>
<p>关于git原理的文章在互联网上也呈现出“汗牛充栋”之势，有些文章“蜻蜓点水”，有些文章“事无巨细”，看后似乎都无法让我满意。结合自己对git原理的学习，我觉得多数人把握住<strong>git运作机制</strong>的几个关键概念即可，于是就有了这篇文章，我努力尝试给大家讲清楚。</p>
<h2>一. 我就是仓库，<strong>我拥有全部</strong></h2>
<p>我们首先要明确一个git与先前的版本管理工具（主要是<a href="https://subversion.apache.org/">subversion</a>）的不同。下面是使用<a href="https://tonybai.com/tag/svn">subversion</a>版本管理工具时，程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-1.png" alt="img{512x368}" /></p>
<p><center>图：subversion代码生产和协作模式</center></p>
<p>众所周知，subversion是<strong>基于中心版本仓库进行版本管理协作的版本管理工具</strong>。就像上图中那样，所有开发人员开始生产代码的前提是<strong>必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录</strong>；而进行版本管理操作或者与他人进行协作的前提也是：<strong>中心版本仓库必须始终可用</strong>。这有点像以太网的“半双工的集线器(hub)模式”：svn中心仓库就像集线器本身，每个程序员节点就像连接到集线器上的主机；当一个程序员提交(commit)代码到中心仓库时，其他程序员不能提交，否则会出现冲突；如果中心仓库挂掉了，那么整个版本管理过程也将停止，程序员节点间无法进行协作，这就像集线器(hub)挂掉后，所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。</p>
<p>如果我们使用git，我们是<strong>不需要“集线器”</strong>的：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-2.png" alt="img{512x368}" /></p>
<p><center>图：git代码生产和协作模式</center></p>
<p>如上图所示，git号称分布式版本管理系统，本质上是没有像subversion中那个所谓的“中心仓库”的。<strong>每个程序员都拥有一个本地git仓库</strong>，而不仅仅是一份代码拷贝，这个仓库就是一个<strong>独立的版本管理节点</strong>，它拥有程序员进行代码生产、版本管理、与其他程序员协作的<strong>全部信息</strong>。即便在一台没有网络连接的机器上，程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下，任意两个git仓库之间可以进行点对点的协作，这种协作无需中间协调者(中心仓库)参与。</p>
<h2>二. github实现了基于git网络协作的<strong>控制平面</strong></h2>
<p>git实现了分布式版本管理系统，每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个<strong>分布式git版本管理网络</strong>。这样的一个分布式网络存在着与普通分布式系统的类似的问题：如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信)，那么这个分布式网络还缺少一个<strong>“控制平面”</strong>。</p>
<p>而<a href="https://github.com">github</a>恰恰给出了一份git分布式网络控制平面的实现：托管、发现、控制&#8230;。其名称中含有的“hub”字样让我们想起了上面的“hub模式”：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-3.png" alt="img{512x368}" /></p>
<p><center>图：github：git分布式网络控制平面的实现</center></p>
<p>我们看到在github的git协作模式实践中，引入了“中心仓库”的概念，各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同，这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”，也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。</p>
<h2>三. <strong>objects</strong>是个筐，什么都往里面装</h2>
<p>上面都是从“宏观”谈git的一些与众不同的理念，而git原理，其实是从这一节才真正开始的^_^。</p>
<p>我们知道：每个git仓库的所有数据都存储在仓库顶层路径下的<strong>.git</strong>目录下：</p>
<pre><code>$tree -L 1 -F
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks/
├── index
├── info/
├── logs/
├── objects/
└── refs/

5 directories, 5 files

</code></pre>
<p>而在这些目录和文件中，又以<strong>objects</strong>路径下的数据内容最多，也最为重要。在git的设计中，<strong>objects目录就是一个“筐”，git的核心对象(object)都往里面“装”</strong>。<br />
<img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-4.png" alt="img{512x368}" /></p>
<p><center>图：git核心数据对象类型与objects目录</center></p>
<p>从上图中，我们看到objects中存储的最主要的有三类对象：blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系，我们通过一个例子来做一下“对号入座”。</p>
<p>我们在一个目录下建立git-internal-repo-demo目录，进入该目录，执行下面命令创建一个git仓库：</p>
<pre><code>➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) ✗ $git init .
Initialized empty Git repository in /Users/tonybai/Test/git/git-internal-repo-demo/.git/

</code></pre>
<p>这是一个处于初始状态的git仓库，我们看看存储git仓库数据的<strong>.git</strong>目录下的结构：</p>
<pre><code>➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) $tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files

</code></pre>
<p>这个时候，<strong>objects这个筐还是空的</strong>！我们这就为仓库添点内容：</p>
<pre><code>$mkdir -p cmd/demo

在cmd/demo目录下添加main.go文件，内容如下:

// cmd/demo/main.go
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

</code></pre>
<p>接下来我们使用git add将cmd/demo目录加入到stage区：</p>
<pre><code>$git add .

$git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached &lt;file&gt;..." to unstage)

    new file:   cmd/demo/main.go

</code></pre>
<p>这时我们来看一下objects这个筐是否有变化：</p>
<pre><code>├── objects
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── info
│   └── pack

</code></pre>
<p>我们有一个object已经被装入到“筐”中了。我们看到objects目录下是一些以哈希值命名的文件和目录，其中目录由两个字符组成，是每个object hash值的前两个字符。hash值后续的字符串用于命名对应的object文件。在这里我们的object的hash值(实质是sha-1算法)为3e759ef88951df9b9b07077a7ec01f96b8e659b3，于是这个对象就被放入名为3e的目录下，对应的object文件为759ef88951df9b9b07077a7ec01f96b8e659b3。</p>
<p>我们使用git提供的<strong>低级命令</strong>查看一下这个object究竟是什么，其中git cat-file -t查看object的类型，git cat-file -p查看object的内容：</p>
<pre><code>$git cat-file -t 3e759ef889
blob

$git cat-file -p 3e759ef889
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}
</code></pre>
<p>我们看到objects这个筐中多了一个blob类型的对象，对象内容就是前面main.go文件中内容。</p>
<p>接下来，我们提交一下这次变更：</p>
<pre><code>$git commit -m"first commit" .
[master (root-commit) 3062e0e] first commit
 1 file changed, 7 insertions(+)
 create mode 100644 cmd/demo/main.go

</code></pre>
<p>再来看看<strong>.git/objects</strong>中的变化：</p>
<pre><code>├── objects
│   ├── 1f
│   │   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
│   ├── 30
│   │   └── 62e0ebad9415b704e96e5cee1542187b7ed571
│   ├── 3d
│   │   └── 2045367ea40c098ec5c7688119d72d97fb09a5
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── 40
│   │   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
│   ├── info
│   └── pack

</code></pre>
<p>我们看到筐里被一下子新塞入4个object。我们分别看看新增的4个object类型和内容都是什么：</p>
<pre><code>$git cat-file -t 1f51fe448a
tree
$git cat-file -p 1f51fe448a
100644 blob 3e759ef88951df9b9b07077a7ec01f96b8e659b3    main.go

$git cat-file -t 3062e0ebad
commit
$git cat-file -p 3062e0ebad
tree 406d08e1159e03ae82bcdbe1ad9f076a04a41e2b
author Tony Bai &lt;bigwhite.cn@aliyun.com&gt; 1586243612 +0800
committer Tony Bai &lt;bigwhite.cn@aliyun.com&gt; 1586243612 +0800

first commit

$git cat-file -t 3d2045367e
tree
$git cat-file -p 3d2045367e
040000 tree 1f51fe448aacc69c0f799def9506e61ed3eb60fa    demo

$git cat-file -t 406d08e115
tree
$git cat-file -p 406d08e115
040000 tree 3d2045367ea40c098ec5c7688119d72d97fb09a5    cmd

</code></pre>
<p>这里我们看到了另外两种类型的object被加入“筐”中：commit和tree类型。objects这个筐里目前有了5个object，我们不考虑git是以何种格式存储这些object的，我们想知道的是这几个object的关系是什么样的。请看下一小节^_^。</p>
<h2>四. 每个<strong>commit</strong>都是一个git仓库的快照</h2>
<p>要理清objects“筐”中各object间的关系，就必须要把握住一个关键概念：“每个<strong>commit</strong>都是git仓库的一个快照” &#8211; 以一个commit为入口，我们能将当时objects下面的所有object联系在一起。因此，上面5个object中的那个commit对象就是我们分析各object关系的入口。我们根据上述5个object的内容将这5个object的关系组织为下面这幅示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-5.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系</center></p>
<p>通过上图我们看到：</p>
<ul>
<li>
<p>commit是对象关系图的入口；</p>
</li>
<li>
<p>tree对象用于描述目录结构，每个目录节点都会用一个tree对象表示。目录间、目录文件间的层次关系会在tree对象的内容中体现；</p>
</li>
<li>
<p>每个commit都会有一个root tree对象；</p>
</li>
<li>
<p>blob对象为tree的叶子节点，它的内容即为文件的内容。</p>
</li>
</ul>
<p>上面仅是一次commit后的关系图，为了更清晰的看到多个commit对象之间关系，我们再来对git repo进行一次变更提交:</p>
<pre><code>我们创建pkg/foo目录：

$mkdir -p pkg/foo

然后创建文件pkg/foo/foo.go，其内容如下：

// pkg/foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("this is foo package")
}

</code></pre>
<p>提交这次变更：</p>
<pre><code>$git add pkg
$git commit -m"add package foo" .
[master 6f7f08b] add package foo
 1 file changed, 7 insertions(+)
 create mode 100644 pkg/foo/foo.go
</code></pre>
<p>下面是提交变更后的“筐”内的对象：</p>
<pre><code>$tree objects
objects
├── 1f
│   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
├── 29
│   └── 3ae375dcef1952c88f35dd4d2a1d4576dea8ba
├── 30
│   └── 62e0ebad9415b704e96e5cee1542187b7ed571
├── 3d
│   └── 2045367ea40c098ec5c7688119d72d97fb09a5
├── 3e
│   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
├── 40
│   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
├── 65
│   └── 5dd3aae645813dc53834ebfa8d19608c4b3905
├── 6e
│   └── e873d9c7ca19c7fe609c9e1a963df8d000282b
├── 6f
│   └── 7f08b14168beb114c3cc099b8dc1c09ccd4739
├── cc
│   └── 9903a33cb99ae02a9cb648bcf4a71815be3474
├── info
└── pack

12 directories, 10 files

</code></pre>
<p>object已经多到不便逐一分析了。但我们把握住一点：<strong>commit是分析关系的入口</strong>。我们通过commit的输出或commit log(git log)可知，新增的commit对象的hash值为6f7f08b141。我们还是以它为入口分析新增object的关系以及它们与之前已存在的object的关系：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-6.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系1</center></p>
<p>从上图我们看到：</p>
<ul>
<li>
<p>git新创建tree对象对应我们新建的pkg目录以及其子目录；</p>
</li>
<li>
<p>cmd目录下的子目录和文件内容并未改变，因此这次commit所对应的root tree对象(293ae375dc)直接使用了已存在的cmd目录对应的对象(3d2045367e);</p>
</li>
<li>
<p>新commit对象会将第一个commit对象作为parent，这样多个commit对象之间构成一个单向链表。</p>
</li>
</ul>
<p>上面的两个提交都是新增内容，我们再来提交一个commit，这次我们对已有文件内容做变更：</p>
<pre><code>将cmd/demo/main.go文件内容变更为如下内容：

// cmd/demo/main.go
package main

import (
    "fmt"

    "github.com/bigwhite/foo"
)

func main() {
    fmt.Println("hello, git")
    foo.Foo()
}

提交变更：

$git commit -m"call foo.Foo in main" .
[master 2f14635] call foo.Foo in main
 1 file changed, 6 insertions(+), 1 deletion(-)

</code></pre>
<p>和上面的分析方法一样，我们通过最新commit对应的hash值2f146359b4对新对象和现存对象的关系进行分析：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-7.png" alt="img{512x368}" /></p>
<p><center>图：commit、tree、blob对象之间的关系2</center></p>
<p>如上图，第三次变更提交后，我们看到：</p>
<ul>
<li>
<p>由于main.go文件变更，git重建了main.go blob对象、demo、cmd tree对象</p>
</li>
<li>
<p>由于pkg目录、其子目录布局、子目录下文件内容没有改变，于是新commit对象对应的root tree对象直接“复用”了上一次commit的pkg tree对象。</p>
</li>
<li>
<p>新commit对象加入commit对象单向链表，并将上一次的commit对象作为parent。</p>
</li>
</ul>
<p>我们看到沿着最新的commit对象(2f146359b4)，我们能获取当前仓库的最新结构布局以及各个blob对象的最新内容，即最新的一个快照！</p>
<h2>五. object是不可变的，默克尔树(Merkle Tree)判断变化</h2>
<p>从上面的三次变更，我们看到无论哪种对象object，<strong>一旦放入到objects这个“筐”就是不可变的(immutable)</strong>。即便是第三次commit对main.go进行了修改，git也只是根据main.go的最新内容<strong>创建一个新的blob对象</strong>，而不是修改或替换掉第一版main.go对应的blob对象。</p>
<p>对应目录的tree object亦是如此。如果某目录下的二级目录发生变化或目录下的文件内容发生改变，git会新生成一个对应该目录的tree对象，而不是去修改原先已存在的tree对象。</p>
<p>实际上，git tree对象的组织本身就是一棵<a href="http://en.wikipedia.org/wiki/Merkle_tree">默克尔树(Merkle Tree)</a>。</p>
<p>默克尔树是一类基于哈希值的二叉树或多叉树，其叶子节点上的值通常为数据块的哈希值，而非叶子节点上的值，是将该节点的所有孩子节点的组合结果的哈希值。默克尔树的特点是，底层数据的任何变动，都会传递到其父亲节点，一直到树根。</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-merkle-tree.png" alt="img{512x368}" /></p>
<p><center>图：默克尔树(图片来自网络)</center></p>
<p>以上图为例：我们自下向上看，D0、D1、D2和D3是叶子节点包含的数据。N0、N1、N2和N3是叶子节点，它们是将数据（也就是D0、D1、D2和D3）进行hash运算后得到的hash值；继续往上看，N4和N5是中间节点，N4是N0和N1经过hash运算得到的哈希值，N5是N2和N3经过hash运算得到的哈希值。（注意，hash值计算方法：把相邻的两个叶子结点合并成一个字符串，然后运算这个字符串的哈希）。最后，Root节点是N4和N5经过hash运算后得到的哈希值，这就是这颗默克尔树的根哈希。当N0包含的数据发生变化时，根据默克尔树的节点hash值形成机制，我们可以快速判断出：<strong>N0、N4和root节点会发生变化</strong>。</p>
<p>对应git来说，叶子节点对应的就是每个文件的hash值，tree对象对应的是中间节点。因此，通过默克尔树(Merkle Tree)的特性，我们可以快速判断哪些对象对应的目录或文件发生了变化，应该重新创建对应的object。我们还以上面的第三次commit为例：</p>
<p><img src="https://tonybai.com/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-8.png" alt="img{512x368}" /></p>
<p><center>图：通过默克尔树(Merkle Tree)的特性判断哪些对象发生变化需要重新创建</center><br />
如上图所示，第三次commit是因为cmd/demo/main.go内容发生了变化，根据merkle tree特性，我们可以快速判断红色的object会随之发生变化。于是git会自底向上逐一创建这些新对象：main.go文件对应的blob对象以及demo、cmd以及根节点对应的tree对象。</p>
<h2>六. branch和tag之所以轻量，因为它们都是“指针”</h2>
<p>使用subversion时，创建branch或打tag使用的是svn copy命令。svn copy执行的就是真实的文件拷贝，相当于将trunk下的目录和文件copy一份放到branch或tag下面，建立一个trunk的副本，这样的操作绝对是“超重量级”的。如果svn仓库中的文件数量庞大且size很大，那么svn copy执行起来不仅速度慢，而且还会在svn server上占用较大的磁盘存储空间，因此使用svn时，打tag和创建branch是要“谨慎”的。</p>
<p>而git的branch和tag则极为轻量，我们来给上面例子中的仓库创建一个dev分支：</p>
<pre><code>$git branch dev

</code></pre>
<p>我们看看.git下有啥变化：</p>
<pre><code>.

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags

</code></pre>
<p>我们看到.git/refs/heads下面多出了一个dev文件，我们查看一下该文件的内容：</p>
<pre><code>$cat refs/heads/dev
2f146359b475909f2fdcdef046af3431c8077282

$git log --oneline

2f14635 (HEAD -&gt; master, dev) call foo.Foo in main
6f7f08b add package foo
3062e0e first commit

</code></pre>
<p>对比发现，dev文件中的内容恰是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。</p>
<p>我们再来给repo打一个tag：</p>
<pre><code>$git tag v0.0.1

</code></pre>
<p>同样，我们来查看一下.git目录下的变化：</p>
<pre><code>└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags
        └── v0.0.1

</code></pre>
<p>我们看到在refs/tags下面增加一个名为v0.0.1的文件，查看其内容：</p>
<pre><code>$cat refs/tags/v0.0.1
2f146359b475909f2fdcdef046af3431c8077282

</code></pre>
<p>和dev分支文件一样，它的内容也是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。</p>
<p>可见，使用git创建分支或tag仅仅是创建了一个指向某个commit对象的<strong>“指针”</strong>，这与subversion的副本操作相比，简直不能再轻量了。</p>
<p>前面说过，一个commit对象都是一个git仓库的快照，切换到(git checkout xxx)某个branch或tag，就是将本地工作拷贝切换到commit对象所代表的仓库快照的状态。当然也会将commit对象组成的单向链表的head指向该commit对象，这个head即.git/HEAD文件的内容。</p>
<h2>七. 小结</h2>
<p>到这里，git原理的几个关键概念就交代完了，再回顾一下：</p>
<ul>
<li>
<p>和subversion这样的集中式版本管理工具最大的不同就是每个程序员节点都是git仓库，拥有全部开发、协作所需的全部信息，完全可以脱离“中心节点”；</p>
</li>
<li>
<p>如果说git聚焦于数据平面的功能，那么github则是一个基于git网络协作的<strong>控制平面</strong>的实现；</p>
</li>
<li>
<p><strong>objects</strong>是个筐，什么都往里面装。git仓库的核心数据都存在.git/objects下面，主要类型包括：blob、tree和commit；</p>
</li>
<li>
<p>每个<strong>commit</strong>都是一个git仓库的快照，记住commit对象是分析对象关系的入口;</p>
</li>
<li>
<p>git是基于数据内容的hash值做等值判定的，object是不可变的，默克尔树(Merkle Tree)用来快速判断变化。</p>
</li>
<li>
<p>branch和tag因为是“指针”，因此创建、销毁和切换都非常轻量。</p>
</li>
</ul>
<h2>八. 参考资料</h2>
<ul>
<li>
<p><a href="https://git-scm.com/book/en/v2">Pro Git v2</a> &#8211; https://git-scm.com/book/en/v2</p>
</li>
<li>
<p><a href="https://www.cnblogs.com/kisun168/p/11408346.html">git介绍</a> &#8211; https://www.cnblogs.com/kisun168/p/11408346.html</p>
</li>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/53750883">git内部原理</a> &#8211; https://zhuanlan.zhihu.com/p/53750883</p>
</li>
<li>
<p><a href="https://www.jianshu.com/p/72f9f8c9c47e">git仓库内部结构</a> &#8211; https://www.jianshu.com/p/72f9f8c9c47e</p>
</li>
</ul>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>也谈Commit log</title>
		<link>https://tonybai.com/2013/05/09/also-talk-about-commit-log/</link>
		<comments>https://tonybai.com/2013/05/09/also-talk-about-commit-log/#comments</comments>
		<pubDate>Thu, 09 May 2013 09:18:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Chinglish]]></category>
		<category><![CDATA[Commit]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Mercurial]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[pre-commit-hook]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Subversion]]></category>
		<category><![CDATA[svn]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[模板]]></category>
		<category><![CDATA[版本控制]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1264</guid>
		<description><![CDATA[在版本控制工具大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如Subversion、Git、Mercurial等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&#8220;commit context&#8221;。在每次commit时，这些上下文信息只能通过commit log来体现。 一、Commit Context 今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。 前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。 commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。 [example1 - Linux Kernel] audit: catch possible NULL audit buffers It&#39;s possible for audit_log_start() to return NULL.&#160; Handle it in the various callers. Signed-off-by: Kees Cook [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="http://tonybai.com/2011/02/18/put-everything-under-version-control/">版本控制工具</a>大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如<a href="http://subversion.apache.org">Subversion</a>、<a href="http://git-scm.com">Git</a>、<a href="http://mercurial.selenic.com/‎">Mercurial</a>等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为&ldquo;commit context&rdquo;。在每次commit时，这些上下文信息只能通过commit log来体现。</p>
<p><b>一、Commit Context</b></p>
<p>今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。</p>
<p>前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。</p>
<p>commit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与<b>项目过程、质量保证流程以及项目使用的一些工具系统</b>有 关联。我们来看两个知名开源项目的commit log样例吧。</p>
<p><i>[example1 - Linux Kernel]</i></p>
<p><font face="Courier New">audit: catch possible NULL audit buffers<br />
	It&#39;s possible for audit_log_start() to return NULL.&nbsp; Handle it in the<br />
	various callers.</font></p>
<p><font face="Courier New">Signed-off-by: Kees Cook <a class="moz-txt-link-rfc2396E" href="mailto:keescook@chromium.org">&lt;keescook@chromium.org&gt;</a><br />
	Cc: Al Viro <a class="moz-txt-link-rfc2396E" href="mailto:viro@zeniv.linux.org.uk">&lt;viro@zeniv.linux.org.uk&gt;</a><br />
	Cc: Eric Paris <a class="moz-txt-link-rfc2396E" href="mailto:eparis@redhat.com">&lt;eparis@redhat.com&gt;</a><br />
	Cc: Jeff Layton <a class="moz-txt-link-rfc2396E" href="mailto:jlayton@redhat.com">&lt;jlayton@redhat.com&gt;</a><br />
	Cc: &quot;Eric W. Biederman&quot; <a class="moz-txt-link-rfc2396E" href="mailto:ebiederm@xmission.com">&lt;ebiederm@xmission.com&gt;</a><br />
	Cc: Julien Tinnes <a class="moz-txt-link-rfc2396E" href="mailto:jln@google.com">&lt;jln@google.com&gt;</a><br />
	Cc: Will Drewry <a class="moz-txt-link-rfc2396E" href="mailto:wad@google.com">&lt;wad@google.com&gt;</a><br />
	Cc: Steve Grubb <a class="moz-txt-link-rfc2396E" href="mailto:sgrubb@redhat.com">&lt;sgrubb@redhat.com&gt;</a><br />
	Cc: Andrea Arcangeli <a class="moz-txt-link-rfc2396E" href="mailto:aarcange@redhat.com">&lt;aarcange@redhat.com&gt;</a><br />
	Signed-off-by: Andrew Morton <a class="moz-txt-link-rfc2396E" href="mailto:akpm@linux-foundation.org">&lt;akpm@linux-foundation.org&gt;</a><br />
	Signed-off-by: Linus Torvalds <a class="moz-txt-link-rfc2396E" href="mailto:torvalds@linux-foundation.org">&lt;torvalds@linux-foundation.org&gt;</a></font></p>
<p>这是<a href="https://www.kernel.org">Linux Kernel</a>项目的一个commit log的内容。从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的内核模块范围audit<br />
	- 修改的原因summary: to catch possible NULL audit buffers<br />
	- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表<br />
	- 这个patch由Who sign-off的。</p>
<p>将mail list放入到commit log中，这是Linux Kernel开发过程规范所要求的，同样也是质量保证的一个方法。在《<a href="http://tonybai.com/2012/03/27/how-to-participate-linux-community-section-1/">如何加入Linux内核开发社区</a>》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。</p>
<p><i>[example2 - Apache Subversion]</i></p>
<p><font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font></p>
<p><font face="Courier New">* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c<br />
	&nbsp; (simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_first_creds,<br />
	&nbsp;&nbsp; ssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked<br />
	&nbsp;&nbsp;&nbsp; and we are in interactive mode but have no unlock prompt function, don&#39;t<br />
	&nbsp;&nbsp;&nbsp; throw a &quot;GNOME Keyring is locked and we are non-interactive&quot; error;<br />
	&nbsp;&nbsp;&nbsp; instead, continue without unlocking it, so that the unlocking may be<br />
	&nbsp;&nbsp;&nbsp; handled by the default GNOME Keyring unlock dialog box.</font></p>
<p>这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：</p>
<p>- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c，包括括号中的函数名列表， 这个显然更为细致。<br />
	- 修改的原因summary: <font face="Courier New">Fix issue #3498 &#8211; Subversion password stores freeze Eclipse</font><br />
	- 这个patch与问题跟踪系统的关联性 -<font face="Courier New">issue #3498</font>。</p>
<p>通过这个commit log，我们可以快速找到此patch对应的问题跟踪系统中的条目#3498，这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。</p>
<p>综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的&ldquo;全面&rdquo;是project-dependent的，是需要能够体现出涉及project的一切必要信息的：过程的、质量的、工具 的。</p>
<p><b>二、Commit log格式</b></p>
<p>Commit log没有放之四海而皆准的统一格式，而是project-dependent的。就我个人而言，我会在下面的几个问题上有纠结。</p>
<p><b><i>* 语言</i></b></p>
<p>不得不承认在创造编程语言方面，西方文化占了主导，语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式，但却是很少见到在源文件中使用非英语命名变量或函数的，这也影响了我在commit log中对语言的选择 &#8211; 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的，你用中文提交也是没有任何问题的，尤其是在国内商业项目中，使用中文描述起来，理解上快且歧义少。我是不反对用中文写commit log的，但反感的是中英文混合写commit log（有些人用中文，有些人用英文）。每当批量看commit log时，中英文混在一起，一点美感都没有了。</p>
<p>commit log不是给最终用户看的，而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利，精确全面地传达context。即便 应用是要发布给非洲人民，但若开发人员都是中国人，一样可以用中文编写commit log。</p>
<p><b><i>* 地道</i></b></p>
<p>说到&ldquo;地道&rdquo;，主要是针对你选择<b>外语</b>（大多数情况是英语）作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的，我们在用英语编写commit log时也要学会选用&ldquo;地道&rdquo;的词汇，远离<a href="http://en.wikipedia.org/wiki/Chinglish">Chinglish</a>。当然想立即做到&ldquo;地道&rdquo;也不是那么容易，毕竟我们一直以来就按照Chinglish的思维去学 习英语的，一个比较好的方式就是多看看知名开源项目（比如linux kernel）的commit log，看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少，看多了也就找猫画虎的学会了。</p>
<p><b><i>* 规范</i></b></p>
<p>&ldquo;没有规矩，不成方圆&rdquo;，无论是商业软件项目，还是大型开源项目，莫不如此。如果要想很好的传达commit context，一个设计规范，内容全面的commit log格式是必不可少的。我们无需从头做起，很多开源项目在这方面都已经有一些良好的实践，比如上面提到的linux kernel的commit log convention，再比如这里有Apache Subversion的<a href="http://subversion.apache.org/docs/community-guide/conventions.html#coding-style">Commit log要求</a>。TYPO3和FLOW3也有自己详细的<a href="http://wiki.typo3.org/CommitMessage_Format_(Git)">Commit log说明</a>。</p>
<p>制定规范时总体来说，注意以下几点：<br />
	&#8211; 格式简明扼要，只保留必要的项；<br />
	&#8211; 注意与项目过程、质量保证流程的结合，以及与第三方工具的关联（注意序号或ID的唯一性）；<br />
	&#8211; 对于规模较大的系统，可以考虑在log中体现影响的涉及的&ldquo;子模块&rdquo;或&ldquo;子目录&rdquo;名字或者逻辑功能的名字（比如前面linux kernel例子中的audit），这样便于快速定位本地commit的影响范畴。</p>
<p><b>三、Commit模板</b></p>
<p>如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成（比如问题跟踪系统、代码评审系统等）时，建议设置Commit log template(模板)以简化开发者commit log编写的工作。</p>
<p><b><i>* Subversion命令行客户端支持commit log模板</i></b></p>
<p>Subversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能：</p>
<p>- 创建commit log模板log.tmpl，放在特定目录下，本例中放在用户的$HOME目录下<br />
	- 添加并导出环境变量SVN_EDITOR<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">export SVN_EDITOR=&quot;rm svn-commit.tmp &amp;&amp; cp ~/log.tmpl svn-commit.tmp &amp;&amp; vi &quot;</font></p>
<p>svn commit时，svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令，而svn-commit.tmp文件已经被替换为我们的模板文件，开发者只需按模板填写内容，并保存退出即可。如果 commit成功，svn客户端会删除当前目录下的svn-commit.tmp，否则svn-commit.tmp不会被删除，这将导致下次再提交 时，svn客户端检测到svn-commit.tmp的存在，从而新建立一个svn-commit.2.tmp的新文件，导致模板失效，这也是这个方法的 一个瑕疵。</p>
<p><b><i>* Git命令行支持commit log模板</i></b></p>
<p>Git是目前very hot的分布式版本管理工具，起步晚，但起点高，因此已经内置了对模板的支持，只需将模板文件配置一下即可。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<font face="Courier New"> git config &#8211;global commit.template ~/log.tmpl</font></p>
<p><b>四、良好格式commit log</b><b>的实施</b></p>
<p>即便有了良好格式的commit log的模板定义，但就我经验而言，实施起来也还会遇到诸多问题。commit行为是客户端发起的，要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署<a href="http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/">pre-commit hook</a>，对提交的log格式进行检查，不符合模板格式的予以拒绝等。</p>
<p>对于与问题跟踪系统有关联的log格式，还要注意保持问题跟踪系统id或序号的唯一性，这显然是管理和过程方面的工作。</p>
<p>对于开源项目，一般merge到trunk需要owner的检查，所以反倒实施起来容易了些，只要有一篇内容丰富的 developer/community guide或convention之类的文档即可，多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的，为这些project提交patch前是要好好阅读这些文档的，不能坏了规矩^_^。&nbsp; &nbsp; &nbsp;<br />
	&nbsp;</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/05/09/also-talk-about-commit-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
