蚊章汇 - Julia Evans
type: channel
link: https://linuxcn.bwsl.wang/author/795/
description: Julia Evans
items: Git 提交是差异、快照还是历史记录？
items: 使用 NFS 将 Git 提交记录显示成文件目录
items: 解读那些令人困惑 Git 术语
items: Git 分支：直觉与现实
items: Git 的遴选和撤销操作是如何利用三路合并的
items: 我的一些 nix 学习经验：安装和打包
items: 一些被忽略的 Git 知识
items: 从零开始，运用 Ruby 语言创建一个 DNS 查询
items: 在 Git 仓库中，文件究竟被存储在哪里？
items: Linux 下“Hello World”的幕后发生了什么

Git 提交是差异、快照还是历史记录？
type: item
date: 2024-01-22T02:47:00+08:00
description: 大家好！我一直在慢慢摸索如何解释 Git 中的各个核心理念（提交、分支、远程、暂存区），而提交这个概念却出奇地棘手。要明白 Git 提交是如何实现的对我来说相当简单（这些都是确定的！我可以直接查看！），但是要弄清楚别人是怎么看待提交的却相当困难。所以，就像我最近一直在做的那样，我在 Mastodon 上问了一些问题。大家是怎么看待 Git 提交的？我进行了一个 非常不科学的调查，询问大家是怎么看待 Git 提交的：是快照、差异，还是所有之前提交的列表？（当然，把它看作这三者都是合理的，但我很好奇人们的 主要 观点）。这是调查结果：结果是：51% 差异42% 快照4% 所有之前的提交的历史记录3% “其他”我很惊讶差异和快照两个选项的比例如此接近。人们还提出了一些有趣但相互矛盾的观点，比如 “在我看来，提交是一个差异，但我认为它实际上是以快照的形式实现的” 和 “在我看来，提交是一个快照，但我认为它实际上是以差异的形式实现的”。关于提交的实际实现方式，我们稍后再详谈。在我们进一步讨论之前：我们的说 “一个差异” 或 “一个快照” 都是什么意思？什么是差异？我说的“差异”可能相当明显：差异就是你在运行 git show COMMIT_ID 时得到的东西。例如，这是一个 rbspy 项目中的拼写错误修复：diff --git a/src/ui/summary.rs b/src/ui/summary.rs index 5c4ff9c..3ce9b3b 100644 --- a/src/ui/summary.rs +++ b/src/ui/summary.rs @@ -160,7 +160,7 @@ mod tests { "; let mut buf: Vec<u8> = Vec::new(); - stats.write(&mut buf).expect("Callgrind write failed"); + stats.write(&mut buf).expect("summary write failed"); let actual = String::from_utf8(buf).expect("summary output not utf8"); assert_eq!(actual, expected, "Unexpected summary output"); } 你可以在 GitHub 上看到它： https://github.com/rbspy/rbspy/commit/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b什么是快照？我说的 “快照” 是指 “当你运行 git checkout COMMIT_ID 时得到的所有文件”。Git 通常将提交的文件列表称为 “树”（如“目录树”），你可以在 GitHub 上看到上述提交的所有文件：https://github.com/rbspy/rbspy/tree/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b（它是 /tree/ 而不是 /commit/）“Git 是如何实现的”真的是正确的解释方式吗？我最常听到的关于学习 Git 的建议大概是 “只要学会 Git 在内部是如何表示事物的，一切都会变得清晰明了”。我显然非常喜欢这种观点（如果你花了一些时间阅读这个博客，你就会知道我 喜欢 思考事物在内部是如何实现的）。但是作为一个学习 Git 的方法，它并没有我希望的那么成功！通常我会兴奋地开始解释 “好的，所以 Git 提交是一个快照，它有一个指向它的父提交的指针，然后一个分支是一个指向提交的指针，然后……”，但是我试图帮助的人会告诉我，他们并没有真正发现这个解释有多有用，他们仍然不明白。所以我一直在考虑其他方案。但是让我们还是先谈谈内部实现吧。Git 是如何在内部表示提交的 —— 快照在内部，Git 将提交表示为快照（它存储每个文件当前版本的 “树”）。我在 在一个 Git 仓库中，你的文件在哪里？ 中写过这个，但下面是一个非常快速的内部格式概述。这是一个提交的表示方式：$ git cat-file -p 24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b tree e197a79bef523842c91ee06fa19a51446975ec35 parent 26707359cdf0c2db66eb1216bf7ff00eac782f65 author Adam Jensen <adam@acj.sh> 1672104452 -0500 committer Adam Jensen <adam@acj.sh> 1672104890 -0500 Fix typo in expec…

使用 NFS 将 Git 提交记录显示成文件目录
type: item
date: 2024-01-09T00:43:21+08:00
description: 大家好！某天，我突发奇想 —— 是否能把 Git 存储库制作成一个 FUSE 文件系统，然后把所有的提交记录做成文件夹呢？答案是肯定的！有 giblefs、 GitMounter 和用于 Plan 9 号的 git9。但在 Mac 上使用 FUSE 实在很烦人 —— 你需要安装一个内核扩展，但由于安全的原因，Mac OS 上安装内核扩展看起来越来越难了。此外，我还有一些想法，希望能用与这些项目不同的方式来组织文件系统。因此，我想在 Mac OS 上尝试 FUSE 以外的挂载文件系统的方法会很有趣，因此我创建了一个名为 git-commit-folders 的项目来做这个事。它可以同时使用 FUSE 和 NFS（至少在我的电脑上），WebDav 的实现起来还有点问题。这个项目很有实验性（我不确定这究竟是一个有用的软件，还是一个思考 Git 如何工作的有趣玩具），但写起来很有趣，我自己也很喜欢在小型存储库中使用它，下面是我在写这个项目时遇到的一些问题。目标：像文件夹一样显示提交记录我做这个事的主要目的是给大家一些启发：Git 核心是如何运行的。总结来说，Git 提交记录实际上和文件夹非常类似 —— 每个 Git 提交都包含一个目录，其中 列出了文件，这个目录也可以有子目录，依此类推。只是为了节省磁盘空间，Git 提交实际上并不是以文件夹的形式实现的。而在 git-commit-folders，所有的提交记录实际上看起来就是一个文件夹，如果你想浏览历史提交记录，你可以像浏览文件系统一样浏览它们！例如如果你像查看我的博客的初始提交记录，你可以如下操作：$ ls commits/8d/8dc0/8dc0cb0b4b0de3c6f40674198cb2bd44aeee9b86/ README 其他之后的提交记录，如下：$ ls /tmp/git-homepage/commits/c9/c94e/c94e6f531d02e658d96a3b6255bbf424367765e9/ _config.yml config.rb Rakefile rubypants.rb source 分支是符号链接通过 git-commit-folders 挂载的文件系统中，提交是唯一真正的文件夹 —— 其他一切（分支、标签等）都是提交记录的符号链接。这反映了 Git 底层的工作方式。$ ls -l branches/ lr-xr-xr-x 59 bork bazil-fuse -> ../commits/ff/ff56/ff563b089f9d952cd21ac4d68d8f13c94183dcd8 lr-xr-xr-x 59 bork follow-symlink -> ../commits/7f/7f73/7f73779a8ff79a2a1e21553c6c9cd5d195f33030 lr-xr-xr-x 59 bork go-mod-branch -> ../commits/91/912d/912da3150d9cfa74523b42fae028bbb320b6804f lr-xr-xr-x 59 bork mac-version -> ../commits/30/3008/30082dcd702b59435f71969cf453828f60753e67 lr-xr-xr-x 59 bork mac-version-debugging -> ../commits/18/18c0/18c0db074ec9b70cb7a28ad9d3f9850082129ce0 lr-xr-xr-x 59 bork main -> ../commits/04/043e/043e90debbeb0fc6b4e28cf8776e874aa5b6e673 $ ls -l tags/ lr-xr-xr-x - bork 31 Dec 1969 test-tag -> ../commits/16/16a3/16a3d776dc163aa8286fb89fde51183ed90c71d0 这个并不能完全呈现 Git 的所有工作机理（相比简单的类似文件夹的提交，还有很多复杂的细节），但是我希望大家对“每个提交如同一个文件夹，里面有你的旧版本代码”有一个直观的认识。这么做有什么好处呢？在我深入介绍它的实现之前，我想说下为什么把 Git 提交记录变成拥有文件夹的文件系统很有用。我的很多项目最终都没有真正使用过（比如 dnspeep），但我发现自己在做这个项目的时候确实使用到了一些。目前为止我发现主要用处是：查找已经删除的函数 - 可以用 grep someFunction branch_histories/main/*/commit.go 查找它的旧版本快速查看其他分支的一个文件并从其拷贝…

解读那些令人困惑 Git 术语
type: item
date: 2023-12-08T04:07:12+08:00
description: 我正在一步步解释 Git 的方方面面。在使用 Git 近 15 年后，我已经非常习惯于 Git 的特性，很容易忘记它令人困惑的地方。因此，我在 Mastodon 上进行了调查：你有觉得哪些 Git 术语很让人困惑吗？我计划写篇博客，来解读 Git 中一些奇怪的术语，如：“分离的 HEAD 状态”，“快速前移”，“索引/暂存区/已暂存”，“比 origin/main 提前 1 个提交”等等。我收到了许多有洞见的答案，我在这里试图概述其中的一部分。下面是这些术语的列表：HEAD 和 “heads”“分离的 HEAD 状态”在合并或变基时的 “ours” 和 “theirs”“你的分支已经与 'origin/main' 同步”HEAD^、HEAD~、HEAD^^、HEAD~~、HEAD^2、HEAD~2.. 和 ...“可以快速前移”“引用”、“符号引用”refspecs“tree-ish”“索引”、“暂存的”、“已缓存的”“重置”、“还原”、“恢复”“未跟踪的文件”、“追踪远程分支”、“跟踪远程分支”检出reflog合并、变基和遴选rebase –onto提交更多复杂的术语我已经尽力讲解了这些术语，但它们几乎覆盖了 Git 的每一个主要特性，这对一篇博客而言显然过于繁重，所以在某些地方可能会有一些粗糙。HEAD 和 “heads”有些人表示他们对 HEAD 和 refs/heads/main 这些术语感到困惑，因为听起来像是一些复杂的技术内部实现。以下是一个快速概述：“heads” 就是 “分支”。在 Git 内部，分支存储在一个名为 .git/refs/heads 的目录中。（从技术上讲，官方 Git 术语表 中明确表示分支是所有的提交，而 head 只是最近的提交，但这只是同一事物的两种不同思考方式）HEAD 是当前的分支，它被存储在 .git/HEAD 中。我认为，“head 是一个分支，HEAD 是当前的分支” 或许是 Git 中最奇怪的术语选择，但已经设定好了，想要更清晰的命名方案已经为时已晚，我们继续。“HEAD 是当前的分支” 有一些重要的例外情况，我们将在下面讨论。“分离的 HEAD 状态”你可能已经看到过这条信息：$ git checkout v0.1 You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. [...] （消息译文：你处于 “分离 HEAD” 的状态。你可以四处看看，进行试验性的更改并提交，你可以通过切换回一个分支来丢弃这个状态下做出的任何提交。）这条信息的实质是：在 Git 中，通常你有一个已经检出的 “当前分支”，例如 main。存放当前分支的地方被称为 HEAD。你做出的任何新提交都会被添加到你的当前分支，如果你运行 git merge other_branch，这也会影响你的当前分支。但是，HEAD 不一定必须是一个分支！它也可以是一个提交 ID。Git 会称这种状态（HEAD 是提交 ID 而不是分支）为 “分离的 HEAD 状态”例如，你可以通过检出一个标签来进入分离的 HEAD 状态，因为标签不是分支如果你没有当前分支，一系列事情就断链了：git pull 根本就无法工作（因为它的全部目的就是更新你的当前分支）除非以特殊方式使用 git push，否则它也无法工作git commit、git merge、git rebase 和 git cherry-pick 仍然可以工作，但它们会留下“孤儿”提交，这些提交没有连接到任何分支，因此找到这些提交会很困难你可以通过创建一个新的分支或切换到一个现有的分支来退出分离的 HEAD 状态在合并或变基中的 “ours” 和 “theirs”遇到合并冲突时，你可以运行 git checkout --ours file.txt 来选择 “ours” 版本中的 file.txt。但问题是，什么是 “ours”，什么是 “theirs” 呢？我总感觉此类术语混淆不清，也因此从未用过 git checkout --ours，但我还是查找相关资料试图理清。在合并的过程中，这是如何运作的：当前分支是 “ours”，你要合并进来的分支是 “theirs”，这样看来似乎很合理。$ git checkout merge-into-ours # 当前分支是 “ours” $ git merge fro…

Git 分支：直觉与现实
type: item
date: 2023-12-01T08:41:12+08:00
description: 你好！我一直在投入写作一本关于 Git 的小册，因此我对 Git 分支投入了许多思考。我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。这使我开始思考：直觉上的分支概念可能是什么样，以及它如何与 Git 的实际操作方式区别开来？在这篇文章中，我想简洁地讨论以下几点内容：我认为许多人可能有的一个直觉性的思维模型Git 如何在内部实现分支的表示（例如，“分支是对提交的指针”）这种“直觉模型”与实际操作方式之间的紧密关联直觉模型的某些局限性，以及为何它可能引发问题本文无任何突破性内容，我会尽量保持简洁。分支的直观模型当然，人们对分支有许多不同的直觉。我自己认为最符合“苹果树的一个分支”这一物理比喻的可能是下面这个。我猜想许多人可能会这样理解 Git 分支：在下图中，两个红色的提交就代表一个“分支”。我认为在这个示意图中有两点很重要：分支上有两个提交分支有一个“父级”（main），它是这个“父级”的分支虽然这个观点看似合理，但实际上它并不符合 Git 对于分支的定义 — 最重要的是，Git 并没有一个分支的“父级”的概念。那么，Git 又是如何定义分支的呢？在 Git 里，分支是完整的历史在 Git 中，一个分支是每个过去提交的完整历史记录，而不仅仅是那个“分支”提交。因此，在我们上述的示意图中，所有的分支（main 和 branch）都包含了 4 次提交。我创建了一个示例仓库，地址为：https://github.com/jvns/branch-example。它设置的分支方式与前图一样。现在，我们来看看这两个分支：main 分支包含了 4 次提交：$ git log --oneline main 70f727a d f654888 c 3997a46 b a74606f a mybranch 分支也有 4 次提交。最后两次提交在这两个分支里都存在。$ git log --oneline mybranch 13cb960 y 9554dab x 3997a46 b a74606f a 因此，mybranch 中的提交次数为 4，而不仅仅是 2 次“分支”提交，即 13cb960 和 9554dab。你可以用以下方式让 Git 绘制出这两个分支的所有提交：$ git log --all --oneline --graph * 70f727a (HEAD -> main, origin/main) d * f654888 c | * 13cb960 (origin/mybranch, mybranch) y | * 9554dab x |/ * 3997a46 b * a74606f a 分支以提交 ID 的形式存储在 Git 的内部，分支会以一种微小的文本文件的形式存储下来，其中包含了一个提交 ID。这就是我一开始提及到的“技术上正确”的定义。这个提交就是分支上最新的提交。我们来看一下示例仓库中 main 和 mybranch 的文本文件：$ cat .git/refs/heads/main 70f727acbe9ea3e3ed3092605721d2eda8ebb3f4 $ cat .git/refs/heads/mybranch 13cb960ad86c78bfa2a85de21cd54818105692bc 这很好理解：70f727 是 main 上的最新提交，而 13cb96 是 mybranch 上的最新提交。这样做的原因是，每个提交都包含一种指向其父级的指针，所以 Git 可以通过追踪这些指针链来找到分支上所有的提交。正如我前文所述，这里遗漏的一个重要因素是这两个分支间的任何关联关系。从这里能看出，mybranch 是 main 的一个分支——这一点并没有被表明出来。既然我们已经探讨了直观理解的分支概念是如何不成立的，我接下来想讨论的是，为何它在某些重要的方面又是如何成立的。人们的直观感觉通常并非全然错误我发现，告诉人们他们对 Git 的直觉理解是“错误的”的说法颇为流行。我觉得这样的说法有些可笑——总的来说，即使人们关于某个题目的直觉在某些方面在技术上不精确，但他们通常会有完全合理的理由来支持他们的直觉！即使是“不正确的”模型也可能极其有用。现在，我们来讨论三种情况，其中直觉上的“分支”概念与我们实际在操作中如何使用 Git 非常相符。变基操作使用的是“直观”的分支概念现在，让我们回到最初的图片。当你在 main 上对 mybranch 执行 变基 rebase 操作时，它将取出“直观”分支上的提交（只有两个红色的提交）然后将它们应用到 main 上。执行结果就是，只有两次提交（x 和 y）被复制。以下是相关操作的样子：$ git switch mybranch $ git rebase main $ git…

Git 的遴选和撤销操作是如何利用三路合并的
type: item
date: 2023-11-14T18:32:40+08:00
description: 大家好！几天前，我尝试向其他人解释 Git 遴选（git cherry-pick）的工作原理，结果发现自己反而更混淆了。我原先以为 Git 遴选是简单地应用一个补丁，但当我真正这样尝试时，却未能成功！因此，接下来我们将谈论我原来以为的遴选操作（即应用一个补丁），这个理解为何不准确，以及实际上它是如何执行的（进行“三路合并”）。尽管本文的内容有些深入，但你并不需要全部理解才能有效地使用 Git。不过，如果你（和我一样）对 Git 的内部运作感到好奇，那就跟我一起深入探讨一下吧！遴选操作并不只是应用一个补丁我先前理解的 git cherry-pick COMMIT_ID 的步骤如下：首先是计算 COMMIT_ID 的差异，就如同执行 git show COMMIT_ID --patch > out.patch 这个命令然后是将补丁应用到当前分支，就如同执行 git apply out.patch 这个命令在我们详细讨论之前，我想指出的是，虽然大部分情况下这个模型是正确的，如果这是你的认知模型，那就没有问题。但是在一些细微的地方，它可能会错，我觉得这个疑惑挺有意思的，所以我们来看看它究竟是如何运作的。如果我在存在合并冲突的情况下尝试进行“计算差异并应用补丁”的操作，下面我们就看看具体会发生什么情况：$ git show 10e96e46 --patch > out.patch $ git apply out.patch error: patch failed: content/post/2023-07-28-why-is-dns-still-hard-to-learn-.markdown:17 error: content/post/2023-07-28-why-is-dns-still-hard-to-learn-.markdown: patch does not apply 这一过程无法成功完成，它并未提供任何解决冲突或处理问题的方案。而真正运行 git cherry-pick 时的实际情况却大为不同，我遭遇到了一处合并冲突:$ git cherry-pick 10e96e46 error: could not apply 10e96e46... wip hint: After resolving the conflicts, mark them with hint: "git add/rm <pathspec>", then run hint: "git cherry-pick --continue". 因此，看起来 “Git 正在应用一个补丁”这样的理解方式并不十分准确。但这里的错误信息确实标明了 “无法应用 10e96e46”，这么看来，这种理解又不完全是错的。这到底是怎么回事呢？那么，遴选到底是怎么执行的呢？我深入研究了 Git 的源代码，主要是想了解 cherry-pick 是如何工作的，最终我找到了 这一行代码：res = do_recursive_merge(r, base, next, base_label, next_label, &head, &msgbuf, opts); 所以，遴选实际上就是一种……合并操作？这有些出乎意料。那具体都合并了什么内容？如何执行这个合并操作的呢？我意识到我对 Git 的合并操作并不是特别理解，于是我上网搜索了一下。结果发现 Git 实际上采用了一种被称为 “三路合并” 的合并方式。那这到底是什么含义呢？Git 的合并策略：三路合并假设我要合并下面两个文件，我们将其分别命名为 v1.py 和 v2.py。def greet(): greeting = "hello" name = "julia" return greeting + " " + name def say_hello(): greeting = "hello" name = "aanya" return greeting + " " + name 在这两个文件间，存在两处不同：def greet() 和 def say_helloname = "julia" 和 name = "aanya"我们应该选择哪个呢？看起来好像不可能有答案！不过，如果我告诉你，原始的函数（我们称之为 base.py）是这样的：def say_hello(): greeting = "hello" name = "julia" return greeting + " " + name 一切似乎变得清晰许多！在这个基础上，v1 将函数的名字更改为 greet，v2 将 name = "aanya"。因此，合并时，我们应该同时做出这两处改变：def greet(): greeting = "hello" name = "aanya" return…

我的一些 nix 学习经验：安装和打包
type: item
date: 2023-10-31T05:54:00+08:00
description: 最近，我首次尝试了 Mac。直至现在，我注意到的最大缺点是其软件包管理比 Linux 差很多。一段时间以来，我对于 homebrew 感到相当不满，因为每次我安装新的软件包时，它大部分时间都花在了升级上。于是，我萌生了试试 nix 包管理器的想法！公认的，nix 的使用存在一定困惑性（甚至它有自己单独的编程语言！），因此，我一直在努力以最简洁的方式掌握使用 nix，避开复杂的配置文件管理和新编程语言学习。以下是我至今为止学习到的内容, 敬请期待如何进行：使用 nix 安装软件包为一个名为 paperjam 的 C++ 程序构建一个自定义的 nix 包用 nix 安装五年前的 hugo 版本如同以往，由于我对 nix 的了解还停留在入门阶段，本篇文章可能存在一些表述不准确的地方。甚至我自己也对于我是否真的喜欢上 nix 感到模棱两可 —— 它的使用真的让人相当困惑！但是，它帮我成功编译了一些以前总是难以编译的软件，并且通常来说，它比 homebrew 的安装速度要快。nix 为何引人关注？通常，人们把 nix 定义为一种“声明式的包管理”。尽管我对此并不太感兴趣，但以下是我对 nix 的两个主要欣赏之处：它提供了二进制包（托管在 https://cache.nixos.org/ 上），你可以迅速下载并安装对于那些没有二进制包的软件，nix 使编译它们变得更容易我认为 nix 之所以擅长于编译软件，主要有以下两个原因：在你的系统中，可以安装同一库或程序的多个版本（例如，你可能有两个不同版本的 libc）。举个例子，我当前的计算机上就存在两个版本的 node，一个位于 /nix/store/4ykq0lpvmskdlhrvz1j3kwslgc6c7pnv-nodejs-16.17.1，另一个位于 /nix/store/5y4bd2r99zhdbir95w5pf51bwfg37bwa-nodejs-18.9.1。除此之外，nix 在构建包时是在隔离的环境下进行的，只使用你明确声明的依赖项的特定版本。因此，你无需担心这个包可能依赖于你的系统里的其它你并不了解的包，再也不用与 LD_LIBRARY_PATH 战斗了！许多人投入了大量工作，来列出所有包的依赖项。在本文后面，我将给出两个例子，展示 nix 如何使我在编译软件时遇到了更小的困难。我是如何开始使用 nix 的下面是我开始使用 nix 的步骤：安装 nix。我忘记了我当时是如何做到这一点，但看起来有一个官方安装程序 和一个来自 zero-to-nix.com 的 非官方安装程序。在 MacOS 上使用标准的多用户安装卸载 nix 的 教程 有点复杂，所以选择一个卸载教程更为简单的安装方法可能值得。把 ~/.nix-profile/bin 添加到我的 PATH用 nix-env -iA nixpkgs.NAME 命令安装包就是这样。基本上，是把 nix-env -iA 当作 brew install 或者 apt-get install。例如，如果我想安装 fish，我可以这样做：nix-env -iA nixpkgs.fish 这看起来就像是从 https://cache.nixos.org 下载一些二进制文件 - 非常简单。有些人使用 nix 来安装他们的 Node 和 Python 和 Ruby 包，但我并没有那样做 —— 我仍然像我以前一样使用 npm install 和 pip install。一些我没有使用的 nix 功能有一些 nix 功能/工具我并没有使用，但我要提及一下。我最初认为你必须使用这些功能才能使用 nix，因为我读过的大部分 nix 教程都讨论了它们。但事实证明，你并不一定要使用它们。NixOS（一个 Linux 发行版）nix-shellnix flakeshome-managerdevenv.sh我不去深入讨论它们，因为我并没真正使用过它们，而且网上已经有很多详解。安装软件包nix 包在哪里定义的？我认为 nix 包主仓库中的包是定义在 https://github.com/NixOS/nixpkgs/。你可以在 https://search.nixos.org/packages 查找包。似乎有两种官方推荐的查找包的方式：nix-env -qaP NAME，但这非常缓慢，并且我并没有得到期望的结果nix --extra-experimental-features 'nix-command flakes' search nixpkgs NAME，这倒是管用，但显得有点儿冗长。并且，无论何种原因，它输出的所有包都以 legacyPackages 开头我找到了一种我更喜欢的从命令行搜索 nix 包的方式：运行 nix-env -qa '*' > nix-pa…

一些被忽略的 Git 知识
type: item
date: 2023-10-25T20:23:24+08:00
description: 我一直在慢慢地撰写关于 Git 工作原理的文章。尽管我曾认为自己对 Git 非常了解，但像往常一样，当我尝试解释某事的时候，我又学到一些新东西。现在回想起来，这些事情都不算太令人吃惊，但我以前并没有清楚地思考过它们。事实是:“索引”、“暂存区” 和 -cached 是一回事隐匿文件就是一堆提交并非所有引用都是分支或标签合并提交不是空的下面我们来详细了解这些内容。“索引”、“暂存区” 和 -cached 是一回事当你运行 git add file.txt，然后运行 git status，你会看到类似以下的输出：$ git add content/post/2023-10-20-some-miscellaneous-git-facts.markdown $ git status Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: content/post/2023-10-20-some-miscellaneous-git-facts.markdown 人们通常称这个过程为“暂存文件”或“将文件添加到暂存区”。当你使用 git add 命令来暂存文件时，Git 在后台将文件添加到其对象数据库（在 .git/objects 目录下），并更新一个名为 .git/index 的文件以引用新添加的文件。Git 中的这个“暂存区”事实上有 3 种不同的名称，但它们都指的是同一个东西（即 .git/index 文件）：git diff --cachedgit diff --staged.git/index 文件我觉得我早该早点认识到这一点，但我之前并没有，所以在这里提醒一下。隐匿文件就是一堆提交当我运行 git stash 命令来保存更改时，我一直对这些更改究竟去了哪里感到有些困惑。事实上，当你运行 git stash 命令时，Git 会根据你的更改创建一些提交，并用一个名为 stash 的引用来标记它们（在 .git/refs/stash 目录下）。让我们将此博客文章隐匿起来，然后查看 stash 引用的日志：$ git log stash --oneline 6cb983fe (refs/stash) WIP on main: c6ee55ed wip 2ff2c273 index on main: c6ee55ed wip ... some more stuff 现在我们可以查看提交 2ff2c273 以查看其包含的内容：$ git show 2ff2c273 --stat commit 2ff2c273357c94a0087104f776a8dd28ee467769 Author: Julia Evans <julia@jvns.ca> Date: Fri Oct 20 14:49:20 2023 -0400 index on main: c6ee55ed wip content/post/2023-10-20-some-miscellaneous-git-facts.markdown | 40 ++++++++++++++++++++++++++++++++++++++++ 毫不意外，它包含了这篇博客文章。这很合理！实际上，git stash 会创建两个独立的提交：一个是索引提交，另一个是你尚未暂存的改动提交。这让我感到很振奋，因为我一直在开发一款工具，用于快照和恢复 Git 仓库的状态（也许永远不会发布），而我提出的设计与 Git 的隐匿实现非常相似，所以我对自己的选择感到满意。显然 stash 中的旧提交存储在 reflog 中。并非所有引用都是分支或标签Git 文档中经常泛泛地提到 “引用”，这使得我有时觉得很困惑。就个人而言，我在 Git 中处理 “引用” 的 99% 时间是指分支或 HEAD，而剩下的 1% 时间是指标签。事实上，我以前完全不知道任何不是分支、标签或 HEAD 的引用示例。但现在我知道了一个例子—— stash 是一种引用，而它既不是分支也不是标签！所以这太酷啦！以下是我博客的 Git 仓库中的所有引用（除了 HEAD）：$ find .git/refs -type f .git/refs/heads/main .git/refs/remotes/origin/HEAD .git/refs/remotes/origin/main .git/refs/stash 人们在本帖回复中提到的其他一些参考资料：refs/notes/*，来自 git notesrefs/pull/123/head 和 refs/pull/123/head` 用于 GitHub 拉取请求（可通过 git fetch ori…

从零开始，运用 Ruby 语言创建一个 DNS 查询
type: item
date: 2023-10-24T23:50:37+08:00
description: 大家好！前段时间我写了一篇关于“如何用 Go 语言建立一个简易的 DNS 解析器”的帖子。那篇帖子里我没写有关“如何生成以及解析 DNS 查询请求”的内容，因为我觉得这很无聊，不过一些伙计指出他们不知道如何解析和生成 DNS 查询请求，并且对此很感兴趣。我开始好奇了——解析 DNS 能 花多大功夫？事实证明，编写一段 120 行精巧的 Ruby 语言代码组成的程序就可以做到，这并不是很困难。所以，在这里有一个如何生成 DNS 查询请求，以及如何解析 DNS 响应报文的速成教学！我们会用 Ruby 语言完成这项任务，主要是因为不久以后我将在一场 Ruby 语言大会上发表观点，而这篇博客帖的部分内容是为了那场演讲做准备的。?（我尽量让不懂 Ruby 的人也能读懂，我只使用了非常基础的 Ruby 语言代码。）最后，我们就能制作一个非常简易的 Ruby 版本的 dig 工具，能够查找域名，就像这样：$ ruby dig.rb example.com example.com 20314 A 93.184.216.34 整个程序大概 120 行左右，所以 并不 算多。（如果你想略过讲解，单纯想去读代码的话，最终程序在这里：dig.rb。）我们不会去实现之前帖中所说的“一个 DNS 解析器是如何运作的？”，因为我们已经做过了。那么我们开始吧！如果你想从头开始弄明白 DNS 查询是如何格式化的，我将尝试解释如何自己弄明白其中的一些东西。大多数情况下的答案是“用 Wireshark 去解包”和“阅读 RFC 1035，即 DNS 的规范”。生成 DNS 查询请求步骤一：打开一个 UDP 套接字我们需要实际发送我们的 DNS 查询，因此我们就需要打开一个 UDP 套接字。我们会将我们的 DNS 查询发送至 8.8.8.8，即谷歌的服务器。下面是用于建立与 8.8.8.8 的 UDP 连接，端口为 53（DNS 端口）的代码。require 'socket' sock = UDPSocket.new sock.bind('0.0.0.0', 12345) sock.connect('8.8.8.8', 53) 关于 UDP 的说明关于 UDP，我不想说太多，但是我要说的是，计算机网络的基础单位是“ 数据包 packet ”（即一串字节），而在这个程序中，我们要做的是计算机网络中最简单的事情：发送 1 个数据包，并接收 1 个数据包作为响应。所以 UDP 是一个传递数据包的最简单的方法。它是发送 DNS 查询最常用的方法，不过你还可以用 TCP 或者 DNS-over-HTTPS。步骤二：从 Wireshark 复制一个 DNS 查询下一步：假设我们都不知道 DNS 是如何运作的，但我们还是想尽快发送一个能运行的 DNS 查询。获取 DNS 查询并确保 UDP 连接正常工作的最简单方法就是复制一个已经正常工作的 DNS 查询！所以这就是我们接下来要做的，使用 Wireshark （一个绝赞的数据包分析工具）。我的操作大致如下：打开 Wireshark，点击 “ 捕获 capture ” 按钮。在搜索栏输入 udp.port == 53 作为筛选条件，然后按下回车。在我的终端运行 ping example.com（用来生成一个 DNS 查询）。点击 DNS 查询（显示 “Standard query A example.com”）。 （“A”：查询类型；“example.com”：域名；“Standard query”：查询类型描述）右键点击位于左下角面板上的 “ 域名系统（查询） Domain Name System (query) ”。点击 “ 复制 Copy ” ——> “ 作为十六进制流 as a hex stream ”。现在 b96201000001000000000000076578616d706c6503636f6d0000010001 就放到了我的剪贴板上，之后会用在我的 Ruby 程序里。好欸！步骤三：解析 16 进制数据流并发送 DNS 查询现在我们能够发送我们的 DNS 查询到 8.8.8.8 了！就像这样，我们只需要再加 5 行代码：hex_string = "b96201000001000000000000076578616d706c6503636f6d0000010001" bytes = [hex_string].pack('H*') sock.send(bytes, 0) # get the reply reply, _ = sock.recvfrom(1024) puts reply.unpack('H*') [hex_string].pack('H*') 意思就是将我们的 16 位字符串转译成一个字节串。此时我们不…

在 Git 仓库中，文件究竟被存储在哪里？
type: item
date: 2023-09-17T07:02:30+08:00
description: 大家好！今天我和一个朋友讨论 Git 的工作原理，我们感到奇怪，Git 是如何存储你的文件的？我们知道它存储在 .git 目录中，但具体到 .git 中的哪个位置，各个版本的历史文件又被存储在哪里呢？以这个博客为例，其文件存储在一个 Git 仓库中，其中有一个文件名为 content/post/2019-06-28-brag-doc.markdown。这个文件在我的 .git 文件夹中具体的位置在哪里？过去的文件版本又被存储在哪里？那么，就让我们通过编写一些简短的 Python 代码来探寻答案吧。Git 把文件存储在 .git/objects 之中你的仓库中，每一个文件的历史版本都被储存在 .git/objects 中。比如，对于这个博客，.git/objects 包含了 2700 多个文件。$ find .git/objects/ -type f | wc -l 2761 注意：.git/objects 包含的信息，不仅仅是 “仓库中每一个文件的所有先前版本”，但我们暂不详细讨论这一内容。这里是一个简短的 Python 程序（find-git-object.py），它可以帮助我们定位在 .git/objects 中的特定文件的具体位置。import hashlib import sys def object_path(content): header = f"blob {len(content)}\0" data = header.encode() + content sha1 = hashlib.sha1() sha1.update(data) digest = sha1.hexdigest() return f".git/objects/{digest[:2]}/{digest[2:]}" with open(sys.argv[1], "rb") as f: print(object_path(f.read())) 此程序的主要操作如下：读取文件内容计算一个头部（blob 16673\0），并将其与文件内容合并计算出文件的 sha1 校验和（此处为 e33121a9af82dd99d6d706d037204251d41d54）将这个 sha1 校验和转换为路径（如 .git/objects/e3/3121a9af82dd99d6d706d037204251d41d54）运行的方法如下：$ python3 find-git-object.py content/post/2019-06-28-brag-doc.markdown .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54 术语解释：“内容寻址存储”这种存储策略的术语为“ 内容寻址存储 content addressed storage ”，它指的是对象在数据库中的文件名与文件内容的哈希值相同。内容寻址存储的有趣之处就是，假设我有两份或许多份内容完全相同的文件，在 Git 的数据库中，并不会因此占用额外空间。如果内容的哈希值是 aabbbbbbbbbbbbbbbbbbbbbbbbb，它们都会被存储在 .git/objects/aa/bbbbbbbbbbbbbbbbbbbbb 中。这些对象是如何进行编码的？如果我尝试在 .git/objects 目录下查看这个文件，显示的内容似乎有一些奇怪：$ cat .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54 x^A<8D><9B>}s<E3>Ƒ<C6><EF>o|<8A>^Q<9D><EC>ju<92><E8> <9C><9C>*<89>j<FD>^... 这是怎么回事呢？让我们来运行 file 命令检查一下：$ file .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54 .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54: zlib compressed data 原来，它是压缩的！我们可以编写一个小巧的 Python 程序—— decompress.py，然后用 zlib 模块去解压这些数据：import zlib import sys with open(sys.argv[1], "rb") as f: content = f.read() print(zlib.decompress(content).decode()) 让我们来解压一下看看结果：$ python3 decompress.py .git/objects/8a/e33121a9af82dd99d6d706d037204251d…

Linux 下“Hello World”的幕后发生了什么
type: item
date: 2023-08-29T22:36:53+08:00
description: 今天我在想 —— 当你在 Linux 上运行一个简单的 “Hello World” Python 程序时，发生了什么，就像下面这个？print("hello world") 这就是在命令行下的情况：$ python3 hello.py hello world 但是在幕后，实际上有更多的事情在发生。我将描述一些发生的情况，并且（更重要的是）解释一些你可以用来查看幕后情况的工具。我们将用 readelf、strace、ldd、debugfs、/proc、ltrace、dd 和 stat。我不会讨论任何只针对 Python 的部分 —— 只研究一下当你运行任何动态链接的可执行文件时发生的事情。0、在执行 execve 之前要启动 Python 解释器，很多步骤都需要先行完成。那么，我们究竟在运行哪一个可执行文件呢？它在何处呢？1、解析 python3 hello.pyShell 将 python3 hello.py 解析成一条命令和一组参数：python3 和 ['hello.py']。在此过程中，可能会进行一些如全局扩展等操作。举例来说，如果你执行 python3 *.py ，Shell 会将其扩展到 python3 hello.py。2、确认 python3 的完整路径现在，我们了解到需要执行 python3。但是，这个二进制文件的完整路径是什么呢？解决办法是使用一个名为 PATH 的特殊环境变量。自行验证：在你的 Shell 中执行 echo $PATH。对我来说，它的输出如下：$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 当执行一个命令时，Shell 将会依序在 PATH 列表中的每个目录里搜索匹配的文件。对于 fish（我的 Shell），你可以在 这里 查看路径解析的逻辑。它使用 stat 系统调用去检验是否存在文件。自行验证：执行 strace -e stat bash，然后运行像 python3 这样的命令。你应该会看到如下输出：stat("/usr/local/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory) stat("/usr/local/bin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory) stat("/usr/sbin/python3", 0x7ffcdd871f40) = -1 ENOENT (No such file or directory) stat("/usr/bin/python3", {st_mode=S_IFREG|0755, st_size=5479736, ...}) = 0 你可以观察到，一旦在 /usr/bin/python3 找到了二进制文件，搜索就会立即终止：它不会继续去 /sbin 或 /bin 中查找。对 execvp 的补充说明如果你想要不用自己重新实现，而运行和 Shell 同样的 PATH 搜索逻辑，你可以使用 libc 函数 execvp（或其它一些函数名中含有 p 的 exec* 函数）。3、stat 的背后运作机制你可能在思考，Julia，stat 到底做了什么？当你的操作系统要打开一个文件时，主要分为两个步骤：它将 文件名 映射到一个包含该文件元数据的 inode它利用这个 inode 来获取文件的实际内容stat 系统调用只是返回文件的 inode 内容 —— 它并不读取任何的文件内容。好处在于这样做速度非常快。接下来让我们一起来快速了解一下 inode。（在 Dmitry Mazin 的这篇精彩文章 《磁盘就是一堆比特》中有更多的详细内容）$ stat /usr/bin/python3 File: /usr/bin/python3 -> python3.9 Size: 9 Blocks: 0 IO Block: 4096 symbolic link Device: fe01h/65025d Inode: 6206 Links: 1 Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2023-08-03 14:17:28.890364214 +0000 Modify: 2021-04-05 12:00:48.000000000 +0000 Change: 2021-06-22 04:22:50.936969560 +0000 Birth: 2021-06-22 04:22:50.924969237 +00…