msg不是内部或外部命令_Git内部原理剖析,有比这还详细的吗?

c8b7bc11378e9b714ec5ceb756ec1710.png

.1. 为什么写这篇文章

写这篇文章的本意有二:

  1. 工作安排原因,常有同事询问我一些关于 Git 的问题,总觉得自己解释的不够透彻,因此觉得有必要深入了解一下。
  2. 目前中文的 Git 教程往往本末倒置, 一味从版本管理工具的角度去堆砌命令 ,而没有把握住Git的本质,导致读者知道的命令愈多,愈觉得 Git 复杂不友好。

本文中,笔者会通过实例演示+原理解释的方式进行剖析,并提出一些平时我们不易察觉的问题。

1.2. Git产生的背景

Git 诞生于2005年,当时 Linux 内核开发者可以免费使用 BitKeeper 作为源码管理工具,但是其作者认为部分开发者对 BitKeeper 进行逆向工程有悖原则,因而收回了使用权限,在这种危机时刻, Linus 再一次将个人英雄主义发挥到了极致,以主导设计开发了 Git 。根据这段背景我们需要意识到这么几个问题:

  1. Git 最早用来解决 Linux 内核的开发,因而其功能特点都是面向这种 参与者庞大且分散的协作开发模式设计的,企业的小团队可能体会不到 Git 的真正强大之处 。
  2. Git 的设计者和最早的使用者都是 Linux 内核开发者,比起 GUI 界面, 他们更喜欢命令行,更认同 Unix 的设计哲学 ,因而对于习惯了 GUI 界面的开发者(前端、移动端等)Git会显得十分“笨拙”。

理解这些背景对于我们认识 Git 十分重要,举一个例子,我们希望看到所有代码分支的最后提交时间和提交者,这种功能是高度定制的,如果 GUI 工具没有提供,那我们便无能为力,但是 Git 可以,通过 灵活的参数和 Linux 强大的工具集( grep 、 awk ) ,我们可以自动封装出这个命令并使用:

gs_branch_last_commit() {    git fetch --prune    git for-each-ref --sort='-committerdate' --format="%(refname:short) %09 %(authorname) %09 %(committerdate:relative)"          | grep  --line-buffered "origin"         | awk '{printf "%-50s%-25s%s %s %sn",$1,$2,$3,$4,$5}'}

最后需要强调的是, Git本意不是做一个版本管理工具,而是文件管理系统(Git is a content-addressable filesystem) ,正如Linux在早期的邮件中所述:

In many ways you can just see git as a filesystem - it’s content- addressable, and it has a notion of versioning, but I really really designed it coming at the problem from the viewpoint of a filesystemperson (hey, kernels is what I do), and I actually have absolutely zero interest in creating a traditional SCM system.

—Linus Torvalds

在读完本文后,相信读者能更深刻地理解这段话。

SCM(即 Software configuration management)是一种更广义的版本管理, Linus 更愿意直接将其解释为 Source Code Management。


1.3. SCM的三个问题

The Architecture of Open Source Applications (Volume 2) 中提到任何一个SCM软件都需要解决三个问题, 以保证软件在开发过程中任一时间的内容都可以被追溯,并使得不同开发者可以协作开发 。这三个问题是:

  1. 存储内容(Storing content)
  2. 追踪内容的变更(Tracking changes to the content (history including merge metadata))
  3. 向其他开发者分发内容及其变更(Distributing the content and history with collaborators)

Git也不例外,接下来本文将围绕这三个问题,并结合Git自身的一些特点,进行剖析。

2. 术语

由于读者可能对于Git的内部原理不甚熟悉,所以这里把专业词汇先列出来:

  • .git Directory & Working Directory : .git 目录是 Git 存储信息和操作信息的目录, Working Directory 是我们实际操作的目录。
  • Git Object : Git 对象,我们的文件、目录和提交记录都会以 Git Object 的格式存储在 .git 目录中。
  • Git Reference : Git 引用,我们的分支、远程分支、tag的索引都是已 Git Reference 的形式存储,本质是一个包含 SHA1 值的40个字符的16进制字符串。
  • SHA1 : 所有的文件的内容都会通过该算法计算出其(其实还有一个header) SHA1值作为Git 对象的文件名(其实就是数据库中的Key)。
  • plumbing & porcelain : Git的命令分类方式,前者是晦涩的底层命令,直接操作文件,后者是面向版本管理的高级命令。

3. Git 对象

3.1. .git目录

当我们在某个目录下执行 git init 命令时,该目录就会成为 Git 的一个工作目录(Working Directory),而该目录下面的 .git 目录则是 工作目录的全部历史在 Git 内的表示 。

在开始Git内部原理的探索之旅前,我们有必要认识一下 .git 目录,我们在命令行或者 GUI 界面的各种操作,本质都是操作 .git 目录下的文件。现在我们通过 git init 创建一个仓库,并通过 tree 命令查看 .git 目录的结构:

$ tree .git.git├── HEAD├── branches├── 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    └── tags9 directories, 15 files

各个目录/文件的作用如下

文件

HEADconfigdescriptionindex

目录

hooksinfo/excludeobjectsrefs

其中 objects refs 将是本文剖析的重点,也是Git的核心。

3.2. plumbing 和 porcelain

在Git中,存在两种命令: plumbing 和 porcelain ,前者将Git作为一个文件管理系统,所有的命令都十分的底层、抽象,例如上文使用的 git for-each-ref 就是一个 plumbing 命令;对应的,后者将Git作为一个版本管理系统,所有的命令都更加抽象和直白,例如我们常见的 git add / git commit / git push 三兄弟。本文将基于常用的 porcelain 命令进行剖析,以揭示其背后的真实操作,并在某些时刻使用一些 plumbing 命令,以帮助读者从文件的角度进行更深刻的理解。

3.3. Git对象类型

前面说过,Git的本质是一个 content-addressable filesystem (基于内容寻址的文件系统)

既然是一个文件管理系统,那么文件有哪些类型呢?一般来说有 目录和文件 。对于 Git 来说,还需要记录变更,这也可以认为是一种特殊的文件类型。最后,Git还提供了一种tag类型的文件,它为某次提交提供了一个永久索引,因为对于一个SCM系统来说,记录某次重大改动(如版本发布)是十分有必要的。

在Git中,可以用一个有向无环图来表示Git对象的组织方式:

f76fe66eae10ca04680f3db1f652ed74.png

所以Git对象一共有四种类型:

blobtreecommittag

3.4. Git对象操作

那么这四种文件类型具体长什么样呢?这就不得不提Git的对象模型了,在Git中,所有的对象都会通过 zlib 压缩成一个文件名为其 SHA1 值的文件,为了更具体解释这句话,我们开始实验。首先为了方便后面的表示,我设计了一个命令,借助了两个 plumbing 命令: rev-list cat-file,用来打印所有Git对象的内容:

print_all_object() {    for object in `git rev-list --objects --all | cut -d ' ' -f 1`; do        echo 'SHA1: '$object        git cat-file -p $object        echo '^'    done}

第一步,初始化一个仓库并添加文件(为了模拟真实场景,这里用了多级目录):

echo "first file" > first.txtmkdir -p second/thirdecho "second file" > second/second.txtecho "third file" > second/third/third.txtgit add first.txtgit add secondgit commit -m "first commit"echo '~~~~'treeprint_all_object

此时工作目录如下:

.├── first.txt└── second    ├── second.txt    └── third        └── third.txt2 directories, 3 files

Git对象的内容如下:

SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9 #(1)tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao@tencent.com> 1575091820 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091820 +0800first commit^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8 #(2)100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9a #(3)first file^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450 #(4)100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8b #(5)second file^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc #(6)100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359 #(7)third file^
  1. commit 类型的 Git 对象,记录了 commit 指向的 tree 对象,以及提交者(author/committer)的信息
  2. tree 类型的 Git 对象,该对象包含一个名为 first.txt 的文件和一个名为 second 的目录
  3. blob 类型的 Git 对象,该对象的内容为 first file
  4. 同2
  5. 同3
  6. 同2
  7. 同3

可以看出, Git 的 commit 对象可以通过 SHA1 找到 tree 对象,tree 对象可以通过 SHA1 找到其他 tree 和 blob ,在Git中, SHA1就是Git对象的指针 。此外,Git 使用这种文本化的表示方式也符合Unix 一切皆文本的哲学。为了便于理解,上面的信息可以按照上文的模型画出对应的有向无环图(暂时省略了Git引用):

349f34895d8a3544ec34db115f1abcd4.png

接下来我们修改一个文件,再次提交

echo "first file modified" > first.txtgit add first.txtgit commit -m "second commit"echo '~~~~'print_all_object

内容如下:

SHA1: 67f9d83a9ef370c057accf103e6502d3c8a56048tree 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3parent b8a7759d225d7ca4952c57c9ba785a6692a075a9author vimerzhao <vimerzhao@tencent.com> 1575091920 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091920 +0800second commit^SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao@tencent.com> 1575091820 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091820 +0800first commit^SHA1: 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336first file modified^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8bsecond file^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359third file^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9afirst file^

注意commit增加了一个parent字段,此时图变成了:

6e75f799024f7ea5cc4119f601742850.png

接下来我们再删除一个文件,再次提交

rm second/second.txtgit add secondgit commit -m "third commit"git tag -a v3 -m "tag third commit"echo '~~~~'print_all_object

内容如下:

SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao@tencent.com> 1575091990 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091990 +0800third commit^SHA1: 67f9d83a9ef370c057accf103e6502d3c8a56048tree 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3parent b8a7759d225d7ca4952c57c9ba785a6692a075a9author vimerzhao <vimerzhao@tencent.com> 1575091920 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091920 +0800second commit^SHA1: b8a7759d225d7ca4952c57c9ba785a6692a075a9tree 12f38251f4b5858269b7b95b8b655c88bb4185d8author vimerzhao <vimerzhao@tencent.com> 1575091820 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091820 +0800first commit^SHA1: 825133937ceb744ad49a71883f70237a4a1dfc1dobject 78913509ce663ff1e58e47043b015283856779dctype committag v3tagger vimerzhao <vimerzhao@tencent.com> 1575091990 +0800tag third commit^SHA1: 6ac7a28caaf73725fc3383b916447e839e3c2d50100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree d4046dced1e51bbc931b845cfea1c529fec7256c second^SHA1: 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336first file modified^SHA1: d4046dced1e51bbc931b845cfea1c529fec7256c040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: d2895a749b806d7647a9622c71a03e0e3eace8dc100644 blob 667bb3858a056cc96e79c0c3b1edfb60135c2359 third.txt^SHA1: 667bb3858a056cc96e79c0c3b1edfb60135c2359third file^SHA1: 7b1fc0ae095fcadbd565737c2a957bdbeb9c4ee3100644 blob 491a7bb2dd1a1e5ba9e00440ba9f7dd25fa17336 first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: be554e60137e97e1e1e8e443552e0abd17db1450100644 blob 1c59427adc4b205a270d8f810310394962e79a8b second.txt040000 tree d2895a749b806d7647a9622c71a03e0e3eace8dc third^SHA1: 1c59427adc4b205a270d8f810310394962e79a8bsecond file^SHA1: 12f38251f4b5858269b7b95b8b655c88bb4185d8100644 blob 303ff981c488b812b6215f7db7920dedb3b59d9a first.txt040000 tree be554e60137e97e1e1e8e443552e0abd17db1450 second^SHA1: 303ff981c488b812b6215f7db7920dedb3b59d9afirst file^

注意多了一个tag类型的Git对象,指向第三次提交,此时图变成了:

744fbd63b79f1d9287f6a944f7929646.png

以上,我们演示了Git在进行常见的增删改的时候,背后发生的事情。

4. Git 引用

Git 创建分支的成本及其低廉。

Creating a branch is nothing more than just writing 40 characters to a file.

—Linus Torvalds

4.1. 分支

对于上面的仓库,我们此时看一下 HEAD 文件和 refs 目录下的内容,然后创建两个新的分支并提交一些内容:

cat .git/HEADcat .git/refs/heads/mastergit branch feature1git checkout -b feature2 # 创建两个新的分支echo "new feature2" > feature.txtgit add feature.txt && git commit -m "add feature"git checkout mastercat .git/refs/heads/master # 查看每个分支指向的提交cat .git/refs/heads/feature1cat .git/refs/heads/feature2

得到输出

78913509ce663ff1e58e47043b015283856779dc78913509ce663ff1e58e47043b015283856779dca5596a09a7e83f83bf713e81e7653fa652906090

(为了节省篇幅,后面不会贴出 print_all_object 的全部执行结果)

由此可以更新一下我们的有向无环图(为了突出重点,本章节不画出 Git 对象):

d00d906d8b229240d209129754ebf671.png

4.2. 合并

现在master分支做一次commit,执行一次merge

echo "anothre feature" > feature.txtgit add feature.txt && git commit -m "add another feature"git merge feature2

解冲突,然后提交

vim feature.txtgit add feature.txt && git commit -m "handle conflict"cat feature.txtprint_all_object

部分结果如下:

anothre featurenew feature2SHA1: 9c6eb61ba181a070e06d8c5767ea3bdca5f40558tree da90387f4018255a3a37ff2933a8810cf3eccc1fparent 8a1105568902f643a60165f3aa8067e8f16b9ce0parent a5596a09a7e83f83bf713e81e7653fa652906090author vimerzhao <vimerzhao@tencent.com> 1575092878 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092946 +0800handle conflict^SHA1: 8a1105568902f643a60165f3aa8067e8f16b9ce0tree c1ba2f6bb8f4412460347c204714a580155a1180parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao@tencent.com> 1575092846 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092846 +0800add another feature^SHA1: a5596a09a7e83f83bf713e81e7653fa652906090tree 715230bf7c2800c4e745151ce19859127ebbb4b0parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao@tencent.com> 1575092660 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092660 +0800add feature^SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao@tencent.com> 1575091990 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091990 +0800third commit^

注意,最后一次提交有两个 parent commit 。此时,有向无环图变为:

960069d7a9f0c3bab2c66e0eb0162e67.png

4.3. 变基

现在在分支feature1做一次提交,然后使用rebase的方式同步主干:

git checkout -b feature1echo "add feature1" > feature1.txtgit add feature1.txt && git commit -m "add feature1"git rebase mastercat .git/refs/heads/mastercat .git/refs/heads/feature1cat .git/refs/heads/feature2treeprint_all_object

部分输出为:

9c6eb61ba181a070e06d8c5767ea3bdca5f40558ce72867669c6f5ece8f2aef71476a458e502485da5596a09a7e83f83bf713e81e7653fa652906090.├── feature.txt├── feature1.txt├── first.txt└── second    └── third        └── third.txt2 directories, 4 filesSHA1: ce72867669c6f5ece8f2aef71476a458e502485dtree 9a004023f848b224cc79c7aa069738d24dfc8c27parent 9c6eb61ba181a070e06d8c5767ea3bdca5f40558author vimerzhao <vimerzhao@tencent.com> 1575093789 +0800committer vimerzhao <vimerzhao@tencent.com> 1575093789 +0800add feature1^SHA1: 9c6eb61ba181a070e06d8c5767ea3bdca5f40558tree da90387f4018255a3a37ff2933a8810cf3eccc1fparent 8a1105568902f643a60165f3aa8067e8f16b9ce0parent a5596a09a7e83f83bf713e81e7653fa652906090author vimerzhao <vimerzhao@tencent.com> 1575092878 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092946 +0800handle conflict^SHA1: 8a1105568902f643a60165f3aa8067e8f16b9ce0tree c1ba2f6bb8f4412460347c204714a580155a1180parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao@tencent.com> 1575092846 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092846 +0800add another feature^SHA1: a5596a09a7e83f83bf713e81e7653fa652906090tree 715230bf7c2800c4e745151ce19859127ebbb4b0parent 78913509ce663ff1e58e47043b015283856779dcauthor vimerzhao <vimerzhao@tencent.com> 1575092660 +0800committer vimerzhao <vimerzhao@tencent.com> 1575092660 +0800add feature^SHA1: 78913509ce663ff1e58e47043b015283856779dctree 6ac7a28caaf73725fc3383b916447e839e3c2d50parent 67f9d83a9ef370c057accf103e6502d3c8a56048author vimerzhao <vimerzhao@tencent.com> 1575091990 +0800committer vimerzhao <vimerzhao@tencent.com> 1575091990 +0800third commit^

需要注意的是,由于执行了rebase操作,commit ce7286 的 parent commit 并不是发生提交时的 commit( 789135 )了,而是master分支的最新commit( 9c6eb6 )此时有向无环图变为:

af614a5f9c60f78a31733a715272272e.png
原文链接: https://www. toutiao.com/i6775371052 143870467/

更多精彩内容关注专栏《java牛人集结地》~

java牛人集结地​zhuanlan.zhihu.com
d61259a7b770e962d4b3e381fe121b00.png

读者福利:

这边为从事java开发的程序员朋友们整理了一些高级架构学习资料,视频教程、面试集锦等。可以点击下方链接小方块免费领取(还有小姐姐哦~)

4cbbd64b089669da85521ef73fcc889a.png
JAVA高级架构学习资料,视频教程、面试集锦​shimo.im

2fdaaa713282c4f2e230e1c19c55efcf.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当你在命令行中运行"git"命令时,如果出现"git不是内部外部命令"的错误提示,通常表示你的系统没有正确配置Git环境变量。环境变量是操作系统用来查找可执行文件的路径。在Windows系统中,你需要手动将Git的安装路径添加到系统的环境变量中才能正常使用"git"命令。 为了解决这个问题,你可以按照以下步骤进行操作: 1.首先,打开控制面板并进入"系统"设置。 2.点击"高级系统设置",然后在弹出的窗口中点击"环境变量"按钮。 3.在"系统变量"部分,找到名为"Path"的变量,并点击"编辑"按钮。 4.在编辑系统变量的窗口中,点击"新建"按钮,并输入Git的安装路径。通常情况下,Git的安装路径是"C:\Program Files\Git"。 5.点击"确定"按钮保存修改,并关闭所有窗口。 6.重新启动命令行窗口,并再次尝试运行"git"命令,你应该能够成功执行了。 通过将Git的安装路径添加到系统的环境变量中,你就能够在任何位置运行"git"命令,而不会再出现"git不是内部外部命令"的错误提示了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [git不是内部或外部的命令是的解决.docx](https://download.csdn.net/download/weixin_38886068/12023930)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [‘git‘不是内部外部命令Git 的保姆级安装教程(保姆级教程)](https://blog.csdn.net/Thebest_jack/article/details/124610960)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值