
关于版本控制 - Git
关于版本控制
很久之前在团队的分享,整理一下分享给大家!
什么是版本控制
从字面上理解,就是记录文件或项目的修改历史的系统,方便追踪文件的变更,例如软件开发的代码文件、文档撰写中的文档、设计过程中的图形文件,这些文件会不断被修改。版本控制能够记录谁在什么时候对文件做了什么修改,这样可以方便地回溯到之前的某个版本,或者查看文件是如何逐步演变的。
人为版本控制
将版本控制数据存储在电脑上,真“人工智能”。例如,使用简单的文件复制来保存不同的版本。可以在自己的电脑上手动复制项目文件夹,并在文件名后面添加版本号(如 project_v1、project_v2 等)来记录不同的修改阶段。但是这种方法比较原始,对于复杂的项目和多人协作来说效率很低。
本地版本控制系统
其中最流行的一种叫做 RCS,现今许多计算机系统上都还看得到它的踪影。 RCS 的工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。
存在缺点:无法协同
集中化的版本控制系统
诸如 CVS、Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新
存在缺点:
- 中央服务器的单点故障,影响提交、丢失变更历史
分布式版本控制系统
在这类系统中,像 Git、Mercurial、Bazaar 以及 Darcs 等,客户端并不只提取最新版本的文件快照, 而是把代码仓库完整地镜像下来,包括完整的历史记录。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
本次主要分享大家十分熟悉的代码版本控制工具 - Git (偏原理分析)
Git 特点
基于差异
基于快照
- 直接记录快照,而非差异比较
- 近乎所有操作都是本地执行
- Git 保证完整性
- Git 一般只添加数据
Git 关键词
Workspace:工作区
Index / Stage:暂存区
Repository:仓库区(或本地仓库)
Remote:远程仓库
Git 状态 (git status)
- Untracked files: 未跟踪的文件
- Changes not staged for commit: 尚未暂存以备提交的变更
- Changes to be committed:要提交的变更
- Committed:
Git 工作流程
Git Remote init Demo
难以想象,上大一(2010 年)那会,一开始啥都不懂,根本没有 github/gitlab 等概念,多人协作开发就是找一台服务器,找个目录,执行一下以下命令,然后就开干,也没有明确的 git flow / github flow / gitlab flow 这些概念,居然还学起 code review,只能说那会 Too young, too simple,你无法想象我们的 code review 流程
我:我代码提交了,你帮忙 review 一下
同学 A: git pull -> 逐个 commit 看 diff -> 看到有问题的,写注释 -> 提交注释 -> 我看完了
我:git pull -> 搜索注释 -> 改代码 -> git push -> 我改好了
详细过程见演示动画
Git 目录结构
当我们通过 git pull
把代码(空项目)拉下来之后,我们可以看到以下目录
我们提交点内容,看看有什么变化
切换到第一个目录,拉取下最新代码,这个时候再看下 .git
发生了啥改变
先看看操作效果,再分析原理
COMMIT_EDITMSG
这个文件用于存储在执行git commit
操作时用户输入的提交消息(commit message)内容。当你使用git commit
命令后,Git 会打开一个文本编辑器让你输入提交消息,这些消息就存储在这个文件中。提交消息通常用于简要描述本次提交所做的更改,如 “docs: init” 等,这有助于其他开发人员理解代码变更的目的。
这里还涉及到是否增加 -m
的区别,如果可以,请不要使用 -m
,因为 git commit 支持更规范的 commit message 内容,包括三个部分:Header,Body 和 Footer,具体规范后续讲hook的时候会讲到,这里先略过
FETCH_HEAD
FETCH_HEAD 是 Git 中的一个引用文件,它记录了最近一次从远程仓库获取(fetch)操作的结果。具体来说,它包含了从远程仓库获取到的所有分支(或其他引用)的最新提交(commit)的信息。通常靠 FETCH_HEAD 的内容来快速确定远程仓库是否有新的提交
ORIG_HEAD
ORIG_HEAD
是一个引用(reference),它通常用于保存某些操作之前的HEAD
(当前分支引用)的位置。它是 Git 内部用于跟踪分支状态变化的一种机制。
- 合并(merge)操作中的备份,例如,你在
master
分支上执行git merge feature - branch
来合并feature - branch
,在合并操作开始前,HEAD
指向master
分支的当前提交,Git 会把这个master
分支原来的位置记录在ORIG_HEAD
中。这样做的好处是,如果合并过程中出现问题,比如产生了冲突并且你想要撤销这个合并操作,就可以通过git reset --hard ORIG_HEAD
命令快速地将HEAD
恢复到合并之前的状态,让分支回到合并操作之前的样子。 - 重置(reset)操作的参考点, 例如,如果你不小心执行了一个错误的
git reset
操作,导致分支指针移动到了一个不期望的位置,你可以利用ORIG_HEAD
来恢复之前的状态。 - 变基(rebase)操作的备份,例如,当你对一个分支进行变基操作(如
git rebase another - branch
),Git 会先将当前分支的原始HEAD
位置记录到ORIG_HEAD
中。变基操作可能会重写分支的提交历史,过程相对复杂,如果在变基过程中出现问题或者你对结果不满意,ORIG_HEAD
可以帮助你将分支恢复到变基之前的状态。
HEAD
HEAD 是 Git 中一个非常重要的指针,它代表当前工作目录中检出(checkout)的分支引用。简单来说,它指向你正在进行操作的分支的最新提交。例如,当你切换到master
分支并且在该分支上进行工作时,HEAD 就指向master
分支的最新提交。
hooks
提交工作流钩子
pre-commit:钩子在键入提交信息前运行,通常用来做代码格式化、代码检查、测试用例等等,通常我们会结合几个工具来实现
husky、lint-staged、prettier 来实现提交前进行代码格式化,最新版本的实现十分优雅,大致原理如下
- 通过修改
.git/config
下的hostsPath
配置,执行 husky 对于的 hooks,解决 .git/hooks 不提交问题 git commit
时,触发./husky/pre-commit
脚本,执行npx --no-install lint-staged --quiet
lint-staged
识别 stage 文件类型,并根据.lintstagedrc
配置,实现不同文件类型执行不同脚本逻辑
- 通过修改
prepare-commit-msg:钩子在启动提交信息编辑器之前,默认信息被创建之后运行,它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如提交信息模板、合并提交、压缩提交和修订提交等非常实用
- commit-msg:钩子在commit之后触发,可以用来在提交通过前验证项目状态或提交信息,进一步可以结合 changelog 生成插件,来生成发布日志
- post-commit:钩子在整个提交过程完成后运行,该钩子一般用于通知之类的事情
电子邮件工作流钩子
applypatch-msg
:由git am
命令调用的,它接收单个参数:包含请求合并信息的临时文件的名字。 如果脚本返回非零值,Git 将放弃该补丁。 你可以用该脚本来确保提交信息符合格式,或直接用脚本修正格式错误。- pre-applypatch :在
git am
运行期间被调用的,你可以用这个脚本运行测试或检查工作区。 如果有什么遗漏,或测试未能通过,脚本会以非零值退出,中断git am
的运行,这样补丁就不会被提交。 - post-applypatch:运行于提交产生之后,是在
git am
运行期间最后被调用的钩子。 你可以用它把结果通知给一个小组或所拉取的补丁的作者。 但你没办法用它停止打补丁的过程
其它客户端钩子
pre-rebase
:钩子运行于变基之前,以非零值退出可以中止变基的过程。 你可以使用这个钩子来禁止对已经推送的提交变基。 Git 自带的pre-rebase
钩子示例就是这么做的,不过它所做的一些假设可能与你的工作流程不匹配post-rewrite
:钩子被那些会替换提交记录的命令调用,比如git commit --amend
和git rebase
post-checkout
:git checkout 成功运行后, 你可以根据你的项目环境用它调整你的工作目录。 其中包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作- post-merge:在
git merge
成功运行后,你可以用它恢复 Git 无法跟踪的工作区数据,比如权限数据。 这个钩子也可以用来验证某些在 Git 控制之外的文件是否存在,这样你就能在工作区改变时,把这些文件复制进来。 - pre-push:钩子会在
git push
运行期间, 更新了远程引用但尚未传送对象时被调用。 它接受远程分支的名字和位置作为参数,同时从标准输入中读取一系列待更新的引用。 你可以在推送开始之前,用它验证对引用的更新操作
服务器端钩子
- pre-receive:处理来自客户端的推送操作时,最先被调用的脚本是 pre-receive。 它从标准输入获取一系列被推送的引用。如果它以非零值退出,所有的推送内容都不会被接受。 你可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制
- update:update 脚本和 pre-receive 脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 如果 update 脚本以非零值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新
- post-receive:挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与 pre-receive 相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器, 或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态, 所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间
index
Git索引是一个在你的工作目录和项目仓库间的暂存区(staging area),索引文件存储在.git/index中,是一个二进制文件
00000000 44 49 52 43 00 00 00 02 00 00 00 03 61 24 b5 ce |DIRC........a$..|
- 前四字节包含了『DIRC』(0x44495243),指『DirCache』,用于标识该文件是否是合法的索引文件
- 中间四字节包含了索引文件的版本,当前版本为『2』(0x00000002)
- 后面四字节为32位无符号整数,标识了索引的文件数目,该仓库内存储有3个文件,即文件数目为3(0x00000003)
- 最后四字节表示文件创建时间 1629795790 -> 61 24 b5 ce
经过所有内容的分析,大致可以得到类似的文件索引结构
info
├── info # 目录,包含一个全局性排除文件, 用以放置那些不希望被记录在 .gitignore 文件中的忽略模式 │ └── exclude .gitignore: 有一些文件不需要提交,将这些文件添加到.gitignore文件中
- 已经提交的内容,如何添加到 .gitignore
- 通过.gitignore 创建空目录
exclude: 本地仓库忽略,这里配置的忽略文件不会提交到代码库中,对团队里的其他人不会有影响,只影响自己本地仓库
logs
git 所有分支所有操作记录,包含已删除commit和reset记录,所以,我们通过 git reflog 可以还原任何操作
objects
存储对象的目录,本地仓库,git中对象分为三种:commit对象,tree对象(多叉树),blob对象;
git gc 前
git gc 后
refs
git 引用,记录索引值
篇幅原因,以下内容,下次继续!
- git 常见命令
- Plumbing Commands (底层命令)
- Porcelain Commands (上层命令)
- add Add file contents to the index
- am Apply a series of patches from a mailbox
- archive Create an archive of files from a named tree
- bisect Use binary search to find the commit that introduced a bug
- branch List, create, or delete branches
- bundle Move objects and refs by archive
- checkout Switch branches or restore working tree files
- cherry-pick Apply the changes introduced by some existing commits
- citool Graphical alternative to git-commit
- clean Remove untracked files from the working tree
- clone Clone a repository into a new directory
- commit Record changes to the repository
- describe Give an object a human readable name based on an available ref
- diff Show changes between commits, commit and working tree, etc
- fetch Download objects and refs from another repository
- format-patch Prepare patches for e-mail submission
- gc Cleanup unnecessary files and optimize the local repository
- gitk The Git repository browser
- grep Print lines matching a pattern
- gui A portable graphical interface to Git
- init Create an empty Git repository or reinitialize an existing one
- log Show commit logs
- merge Join two or more development histories together
- mv Move or rename a file, a directory, or a symlink
- notes Add or inspect object notes
- pull Fetch from and integrate with another repository or a local branch
- push Update remote refs along with associated objects
- range-diff Compare two commit ranges (e.g. two versions of a branch)
- rebase Reapply commits on top of another base tip
- reset Reset current HEAD to the specified state
- restore Restore working tree files
- revert Revert some existing commits
- rm Remove files from the working tree and from the index
- shortlog Summarize 'git log' output
- show Show various types of objects
- stash Stash the changes in a dirty working directory away
- status Show the working tree status
- submodule Initialize, update or inspect submodules
- switch Switch branches
- tag Create, list, delete or verify a tag object signed with GPG
- worktree Manage multiple working trees
- git 工作流
- git flow
- github flow
- gitlab flow
- 功能分支工作流
- 集中式工作流
参考资料:https://git-scm.com/