Git
参考:
https://git-scm.com/docs
https://missing.csail.mit.edu/
https://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html
...
第 6 课:Git
本节课的讲法是先大致讲清 Git 的数据模型(实现),再讲操作命令。个人认为很受用,强烈推荐。这里仅记录 Git 数据模型等内部相关的东西,关于 Git 的使用参见这里。
一个数据模型的例子如下:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, content = "hello world")
|
+- baz.txt (blob, content = "git is wonderful")blob、tree、commit、object、reference
注:这些东西都可以在 .git 目录中探索。
在 Git 中,文件被称作 blob。目录被称作 tree。commit 包含了父 commit,提交信息、作者、以及本次提交的 tree 等信息。object 则可以是前三者的任意一个。进一步,对每个 object 进行哈希,用哈希值代表每个 object。注意 object 是不可变的,而 reference 是一个映射(指针),字符串(例如 master)映射到 object 的哈希值。所以,一个仓库可以看作是一堆 object 与一堆 reference(即objects 与 references),Git 命令大多数是在操作 object 或者 reference,具体地说,是增加 object ;增加/删除/修改 reference(即改变其指向)。伪代码如下:
workspace、stage、version
以下参杂个人理解:这三者实际上都是一个版本/快照,而所谓的快照基本上等同于一个 tree/commit 对象。
workspace 表示工作区,也就是 .git 目录以外的所有内容,workspace 可以看作是一个快照;
stage 是为了方便用户使用的一个机制,比如说开发了一个新特性,将其放入缓冲区,之后再增加了一些调试代码,那么提交时可以不提交调试代码。stage 中的信息被保存在了 .git/INDEX 文件内,stage 可以看作是一个快照;
version 则是历史提交的版本,因此实际上是若干个快照。
许多命令例如:git add,git diff,git restore 实际上就是利用上述三者之一修改/比较另外一个或多个快照。
branch、HEAD
每个 branch 都是一条直线的提交序列(一系列的 commit 对象),每个 branch 都有一个名字,例如 master、dev 等,它们指向其中的一次提交。git commit 命令实际上的作用是生成一个 commit 对象后,将生成的 commit 对象的父亲设置为当前的分支名指向的提交,而后将分支名指向的对象设置为新生成的对象,伪代码如下:
而 HEAD 指的是当前状态下的最近一次的提交。通常情况下,HEAD 总是与某个分支名的指向相同。但在某些情况下,HEAD 指向的提交不与任何的分支名的指向一致,称为 detached 的状态,例如使用类似如下的方式切换分支:
本质上而言,分支名与 HEAD 都是 reference 对象中的元素,存储的都是一个特定的 commit_id。而整个版本库存放的东西无外乎是一堆 commit 对象(每个 commit 对象包含指向其父亲的指针以及一个 tree 对象)。
.git 目录
.git 目录依次执行
得到如下目录(省略了一些目录及文件)
注释项代表该条命令运行之后生成了该文件。除了 objects 目录以及 index 文件外,其余均为文本文件。为了获取 objects 目录及 index 文件的内容,可以使用以下两行命令:
结论:
git add命令只会生成 blob 对象git commit命令会同时生成 tree 和 commit 对象HEAD指向某个分支名,git checkout <分支名>会同时修改HEAD及index的内容,并且切换分支时git status的结果必须为clean,否则无法执行。
object 的 hash 值
一个 blob 对象的hash值的计算过程如下:
假定 readme.txt 文件内容为123,它被 git add 的时候,objects 目录下会增加一个以二进制序列命名的文件
一共40位(加密算法为SHA-1),其中前两位为目录名,后38位为文件名
使用 python 可以用如下方式计算出来:
术语
stage/cache/index/缓冲区/暂存区都是指的同一个东西
Git 命令简介
所有与 Git 有关的命令最好用 git bash 打开。当然,将 git.exe 所在目录加入到了 path 环境变量后,使用普通的 shell(例如:cmd, powershell, bashrc 等)基本也都没问题。所有的 Git 命令除了 git init 与 git clone 外,一般都要在 .git 的同级目录下执行。
git init
其效果是为当前目录建立一个 .git 目录,正常情况下不要去修改这个文件夹下的任何内容,可以这样理解:之后的每一条以 git 开头的命令执行后,git.exe 会依据命令内容对 .git 目录下的文件或依据 .git 目录内的文件对工作区进行修改。注意:
git init 命令不一定需要在空目录,是否在空目录下执行产生的 .git 目录内容是一样的。
若 .git 目录已存在, 使用 git init 命令的结果会重新初始化,一般不会这样用
git config
设置提交commit信息时的用户名及邮箱
备注: 此项设定与远程代码库的拉取/推送权限无关
设置http协议远程库记住密码
git log
以下命令用于显示所有的提交信息
git reflog
git status
输出分为两部分:
Changes not staged for commit: 显示工作区相对暂存区的变动记录Changes to be committed: 显示暂存区相对最近一次提交的变动记录
用下面的命令可以简化输出:
左边的 M 表示工作区文件修改了(相对最近一次提交)并且放入了暂存区, 右边的 M 表示工作区文件修改了但是没有放入暂存区。此处表示 README 修改了,但是还没有使用 git add README 命令将其放入暂存区;lib/simplegit.rb 被修改后放入了工作区,之后未被修改过;Makefile 在工作区被修改后放入了暂存区,而后工作区又做了修改。
左边的 A 表示 lib/git.rb 是工作区相对最近一次提交新增的文件,并已经放入了暂存区。
?? 表示 LICENSE.txt 是工作区相对最近一次提交新增的文件,但没有放入暂存区中。
git diff
如果需要查看两个 commit 中某个文件的差异,可以使用如下
例子及解释如下
source_commit_id 的行用
-进行标识,target_commit_id 的行用+进行标识,没有变化的行用空格进行标识。显示方式按差异块的形式呈现:
表示的是 source_commit_id 的第 [6, 6+12-1] 行的内容与 target_commit_id 的第 [6, 6+8-1] 行的内容有差异。
git add/rm
git add 的作用是为工作区产生变化的文件生成 blob,并将这些文件添加至暂存区
如果
filename在工作区存在,且与暂存区中的内容不一致或暂存区中没有该文件。具体执行过程为:首先为filename创建一个 object (blob)放在.git/objects下,之后将该 object 放入暂存区。如果
filename在工作区中不存在,且在暂存区中存在,那么效果等同于git rm <filename>。具体执行过程为:将暂存区中相应的 object 删除
git rm 的主要作用是在暂存区中删除文件,可以通过不同的参数选择是否也删除工作区中的文件
git commit
git commit 命令表示将当前的暂存区放入版本库中,前者会打开 Git 默认的文本编辑器(可以使用 git config 进行设置)供开发者添加描述信息,在公司里开发项目推荐用这种方式。后者一般用于添加简略的描述信息,适用于不那么正式的个人项目中使用。
git reset
git reset 操作当前分支的三种操作如下
下面分别说明上面三条命令在做什么,首先假定当前分支为 foo,那么此时 HEAD 也指向了 foo。
git reset --soft HEAD^表示的意思是将foo的指针指向次新一次的提交,而HEAD依然指向foogit reset --mixed HEAD^表示的意思是在上一步的基础上,使用foo指向的提交更新暂存区git reset --hard HEAD^表示在执行前两步后,用暂存区的内容再更新工作区,这样三个区域便完全一致了
备注:这里有一个用法可以用在代码审查中,不确定是否为最佳实践。
VSCode中工作区相对于暂存区的修改在代码行的左侧有 gutter indicators 进行标识,现在假设原始仓库的地址为:https://github.com/base/project,而开发者foo将此代码库进行了 fork 操作,仓库地址为:https://github.com/foo/project。经过代码修改过,首先更新了自己仓库的 feature 分支,之后提出 Pull Request 合并至原始仓库的 dev 分支。此时可以用如下方式在本地看出代码修改了哪些部分
git branch
显示已有分支
创建删除分支
为分支设定 upstream 分支,在 Git 中,每个分支至多只能有一个 upstream 分支。注意:这里的 upstream 与数据模型中的 parent 是不同的概念。设定后可以不加参数地使用 git pull/push/fetch。
类似地,可以使用如下命令来新建本地分支,并与远程分支关联。所谓建立关联, 就是关联后可以不加参数(不指定远程分支名)地使用 git pull/push/fetch
git stash
git stash 用于暂存一些文件,但不进行提交。参见例 4。
git checkout/switch/restore
git checkout 命令用于切换分支以及文件的版本切换作用
git checkout 用于切换分支
本质上是修改 HEAD 指向的 commit_id
注意两条命令只有一些微妙的区别:第一条命令如果切换后 HEAD 与现有的某个分支名的指向一致,则 HEAD 的状态为非 detached 状态,否则为 detached 状态;第二条命令切换后必然处于 detached 状态。所谓 detached 状态指的是切换后相当于处于匿名分支上,如果在匿名分支上发生了提交,之后又切换到别的分支,如果还想切换回匿名分支,那么只能用 commit_id 来切换回去。
当工作目录发生了修改,但此时希望切换到另一个分支,并且要切换到的分支与当前工作目录的版本存在冲突时,使用 git checkout 命令将会失败,此时如果使用
那么等效于先丢弃工作区的全部修改,再进行分支切换
git checkout 用于文件版本切换
较新版本的 Git 引入了两个命令将 checkout 的两大功能进行了分离。其中 git switch 用于分支切换,git restore 用于文件的版本切换。
git switch
git restore
git merge
分支合并的一般流程为:
git rebase(待研究)
原理
从合并的文件上看与 merge 效果一样,但提交历史有了改变。
假定分支情况为:
使用 git rebase 的流程为:
效果是 dev 分支的提交历史变为
所谓 rebase 的直观含义是将 dev 的“基” 从 c2 修改为了 master分支的 c5。使用变基得到的另一个好处是切换回 master 分支后将 dev 分支合进来就不用解决冲突(不产生任何 git object,仅仅是修改了 git ref)
rebase 与 merge 的区别
上述过程如果只用 merge,流程为
效果是
实际上,merge c5 和 c7 的过程为,对 c5 和 c7 对应的 tree 进行合并,解决冲突后使用 git add 命令时,会得到一些新的 blob,使用 git merge --continue 时,会用 .git/INDEX(暂存区)里对应的 tree 写入 .git/objects 目录,并得到一个新的 commit 对象 c8,也写入 .git/objects 目录。注意:c8 这个 commit 对象的 parent 有两个,即 c5 和 c7。
而 rebase 的过程为:对 c5 和 c7 进行合并后得到的 commit 对象 c8 的 parent 仅有 c5 一个。另外,还会产生一个新的 commit 对象 c6’,其提交信息与 c6 一致,但其 parent 与 c6 不同,并且所对应的 tree 对象也有所不同。
git cherry-pick
上述命令的作用是,将 <commit-id> 相对于它前一次提交的修改,作用到当前分支 dev 上,并形成一次新的提交。注意:假设 <commit-id> 对应于另一个分支例如 master,新的提交依旧可能与 master 分支有合并冲突。
将commit-id2相对于commit-id1的连续多个修改作用于当前分支
git clone
上述两条命令内部的详细过程为:两者都会将 .git 中的所有内容(远程仓库的所有分支)下载到本地。下载后,第二条命令本地的默认分支 dev 由远程分支 origin/dev 产生。
git remote
git submodule
参考:pro-git 7.11
参考:git-tower(一个不错的教程,感觉比 pro-git 还要清晰)
有时候,项目开发时需要引入另一个项目,并希望同时保留两个项目各自的提交历史,此时需要使用 git submodule 命令
例如:在项目 a 中需要引入 https://github.com/example/b.git 作为子模块,可以使用:
此时,a/ 目录下会多出一个 .gitmodules 文件,并且多出一个 a/b 文件夹,文件目录类似如下:
此时 a/.git 目录不记录 b 目录的具体修改(只关心在对 a 提交时 a/b 的 commit id 是否发生变化)。git 命令在 a/ 目录与 a/b 目录下分别只对 a/.git 与 a/b/.git 进行修改,并且只关心各自部分的文件修改历史。
如果需要克隆一个带有 submodule 的仓库,可以使用如下几种方式进行:
git fetch/pull/push
git pull 的具体行为是:首先 git fetch 指定远程分支的更新,之后在指定的本地分支上进行 git merge 的操作,如果当前的 HEAD 刚好位于指定的本地分支上,则移动 HEAD 的指向到 merge 后的位置。
git fetch 命令只会将远程分支的修改更新到 .git 目录内部,但不会新建分支,也不会对本地的工作目录进行修改。
参考 stackoverflow 问答,可以按如下方式拉取所有远程分支的更新:
git bundle
bundle 文件用于离线传输 git objects. 使用上可以参考 文档 中的例子, 但由于实际使用时与例子中的需求不完全匹配, 所以这里复述一下文档中的例子(部分), 并加以补充. bundle 文件可以理解为一个离线的 repo, 可以将其视为受限的“远程仓库”, 进行 git remote add, git pull, git fetch, 但与真正的远程仓库的重要区别是: 不能对 bundle 文件进行 git push.
假定场景是这样: 在网络环境 1 的机器 A 上有一个仓库 R1, 在网络环境 2 上有一个机器 B, 在时刻 T1, 将 R1 通过某种方式复制去了机器 B 的 R2, 随后 R1 继续做了修改/提交, R2 可能也继续做了修改/提交. (可以理解 R1 是某个 GitHub 上的开源项目, R2 为公司在确认开源协议后, 决定基于此项目做二次开发). 现在在 T2 时刻, 希望将 R1 在 T1 时刻之后的所有修改 (git objects) 保存在一个文件 F 中, 然后将 F 拷贝至机器 B, 然后 R2 仓库按需从 F 中 cherry-pick/merge/fetch 等.
文档 中的解决方案
T1 时刻同步
此步骤是 T1 时刻将 R1 “复制” 到机器 B, 首先在机器 A 上操作
将 file.bundle 通过某种方式复制进
T2 时刻同步
上述描述的 T2 时刻同步的做法是与 T1 时刻同步方式匹配的, 实际情况中 T1 时刻的同步还存在这几种情况:
将整个 R1 仓库打包为一个压缩包, 传入内网后解压以获取 R2 仓库
R1 仓库本身也有远程库例如 GitHub, 而 R1 仓库在 T1 至 T2 期间, 使用
git fetch或git pull更新 git objects 时, 有许多远程分支remotes/origin/xx没有相对应的本地分支xx
这种情况下, T2 时刻的同步也需要做些适当的调整, 这里有一个示例, 但需要确保自己知道每一步在做啥:
这个例子需要理解这个 stackoverflow问答 以及这个 简书博客
T1 时刻同步
将整个 R1 仓库打包为一个压缩包, 传入内网后解压以获取 R2 仓库
T2 时刻同步
备注: 参考这个 blog, git lfs 文件似乎没法使用 git bundle 做迁移
* git cat-file
查看 .git/objects 目录下的文件内容
* git ls-files
* git ls-remote
输出
git lfs
使用如下方式对大文件进行特殊管理(由 git lfs 管理)
git clone 时可以选择是否下载大文件
git clean
慎用
常用命令备忘录
不切换分支拉取远程代码与本地分支做fast-forward合并
http协议记住密码
删除分支
疑难杂症
一般使用
代理问题
关于代理引发的 git clone 失败问题,参考链接
重置代理
根据实际端口情况修改
一次性使用
网络问题
暂未找到解决方法
终端显示分支
在 terminal 中提示当前所在分支,将如下代码段加入到 ~/.bashrc 中即可
http 免密
详例
注意, 测试2.1与2.2表示的是在测试1的基础上尝试两种做法的结果
例 1(待清晰化)
本例着重解释了git status/diff/restore三个命令
测试 1
总结:
git status命令的输出分为两部分:
Changes not staged for commit: 显示工作区相对暂存区的变动记录Changes to be committed: 显示暂存区相对最近一次提交的变动记录
相应地, git diff命令地解释如下:
测试 2
总结: 接下来, 在操作之前先解释git restore的两种用法
测试 2.1
测试 2.2
测试 2.3
总结: git restore还有第三种用法如下
测试如下:
测试 2.3.1
测试2.3.2
根据上面的总结, 解释如下过程:
例 2(待补充)
本例着重解释 git rm 命令
备注: 本机已经准备好了如下环境
例 3(待删减)
由于被各种命令搞晕, 于是决定干脆打开.git目录一探究竟, 难免会有许多错误, 待日后修改
当所有操作仅限于本地时, .git目录大概长这样:
以下为一个完整的测试(我们主要关注logs, objects, refs文件夹以及index, HEAD, ORIG_HEAD文件的变化)
step 1
注意: 此时logs文件夹与ORIG_HEAD未被创建, objects目录下还没有字节码文件
HEAD文件的内容为
refs/heads/master文件还未被创建
step 2
在工作区增加一个文件show_git.py
此时, objects目录下新增了一个./74/ba2162d599d4e44dcf3d7811cbbd84d43e911d字节码文件(对应于新增的show_git.py文件), index文件夹也做了更新.
refs/heads/master文件还未被创建
备注: objects目录下计算出的16进制值只与文件内容与文件名有关
step 3
此时.git目录的变化为:
log目录被创建, 目录结构如下master与HEAD的文件内容如下
若此时使用
git flag pretty=oneline命令, 输出结果为:index目录做更新refs目录下的heads/master文件被创建, 内容如下注意: 这个值大概与时间有关(怀疑是uuid)
objects目录新增了两个文件7b/9a4ced4ae9fc215a7cbb2e1c52b6fce706e051,92/628406280a94f4efbdcbf59dcb60a5b44ab124不知道为什么要新增两个文件, 有一个疑惑是
git reflog命令需要的输出保存在哪, 是否与这个有关?HEAD文件内容保持不变
例 4(git stash 与 git merge 综合实例)
适用场景如下,例如:master 分支为线上分支,现在需要开发一个新功能,则基于该分支创建一个 dev 分支,但还没修改完毕并且不想提交时,发现 master 分支上出现了 bug,需紧急修复。此时直接使用 git branch master 会报错,此时可以使用 git stash 命令将改动的文件暂存,这样便可以正常切换分支。完整过程如下
例 5 (代码库同步)
场景设定: 公司内网与 GitHub 网络不通, 公司内网存在类似于 Gitlab 的代码管理服务器, 数据文件可从外网上传至内网服务器. 现在希望对 GitHub 项目手动与内网项目同步.
方案1 (不合适, 也没有真正实践过)【待确认!!!】
参考:
https://github.com/git-lfs/git-lfs/issues/2342
步骤如下:
第一次同步
外网需保留
xx.git目录(以增量更新)内网需保留
xx.git目录(以增量更新),xx目录如果不需要可以删除内网需保留
<TARGET-URL>(以增量更新)
第二次同步(更新)
例 6: bare repo 与正常 repo 转换
参考: stackoverflow
正常 repo -> bare repo
bare repo -> 正常 repo
Git 合作模式
模式一:
master 分支只用作合并,且合并过程自动完成,无需解决冲突。dev 分支用做开发人员的公共基库,各开发人员(例如:f1,f2 分支)完成相应的开发后,在 dev 分支上完成手动解决冲突后的合并。最后将 dev 分支合并至 master 分支。
GitHub Flow
在 Github 上为他人项目贡献代码的工作流如下:
在 GitHub 网页的功能在官方原始仓库新建 issue
首先 fork 官方原始仓库到自己的个人远程仓库
git clone 个人远程仓库至本地仓库, 务必从希望最终被合入的官方分支新建分支进行开发
如果官方原始仓库发生修改, 则使用 GitHub 网页的功能将个人远程仓库与官方原始仓库同步
git pull/fetch 将个人远程仓库的更新内容拉至本地仓库
本地仓库的新建分支与官方分支合并
使用 GitHub 网页的功能提 PR 请求, PR 请求关联 issue
Git hooks
hooks 通常译为“钩子”,Git hooks 本质上是位于 .git/hooks 下的一些脚本,它们会在特定的事件触发时(也就是某些特定的命令被执行时)被自动运行,例如:执行 git commit 命令时。其文件名是固定的(对应着相应的事件),git 默认为每个仓库都提供了默认的 hooks,它们的扩展名均为 .sample,如果需要启用 hooks,只需要将相应脚本的扩展名删除即可。hooks 的特点是在 git clone 时,这些脚本不会被克隆下来,另外默认 hooks 的语言为 shell 脚本,但也可以使用其他脚本语言例如 Python,只需要修改文件的 shebang 行即可。
一个看起来还不错的教程。
注意:不要为了加 hooks 而加 hooks,它只是一个工具。
github、gitlab
Github 对比同一仓库的两个分支: https://github.com/username/Repo_A/compare/29ea234..84ae245
Git Internals
git reset 与 git checkout
git add 的时候,将新增加的文件内容加入至 .git/objects 目录,而不增加 tree 类型的 object。在 git commit 的时候才创建 tree 类型的 object,并将其添加至 .git/objects 目录。
git reset 与 git checkout 的区别:
git reset 的调用方式有如下几种
其中前三条命令的执行逻辑是依次进行如下三步:
将当前的 branch 指向 , HEAD 依旧指向 branch, 因此最终也指向
使用 中的内容覆盖暂存区的内容
使用暂存区的内容覆盖工作区的内容
备注: 只有第三种被认为是危险的
而后两条命令的执行逻辑是:
使用 中的覆盖暂存区的版本
使用暂存区的版本覆盖工作区的内容
git checkout 的调用方式有如下几种
第一条命令的执行逻辑是:
将HEAD指针本身指向, 将暂存区的内容改为中的内容, 将中的内容与工作区的内容合并(如果有冲突, 命令本身会报错)
特殊目录与文件
.git: Git 仓库.github: github 网页端的 issue, pull requests 等模板.gitattribute: Git-LFS 管理的文件信息.gitignore: git 忽略文件
Last updated
Was this helpful?