什么是 Git
简而言之,Git 是一种分布式版本控制系统。
和集中式的版本控制系统 SVN 进行比较,分析这两个关键词:
-
版本控制
SVN是增量式的版本控制,它不会把各个版本的副本都完整的保存下来,而只会记录下版本之间的差异(diff),然后按照顺序更新或者恢复特定版本的数据。这使得服务端的存储量会非常低。
而Git 是基于快照的版本控制,其实现原理是为每个文件计算一个 hash 值然后压缩存储到 .git/objects 目录内,也就是说不同版本的同一文件会在git中存在多个object。
-
分布式
SVN基于中心版本仓库,所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录;而进行版本管理操作或者与他人进行协作的前提也是:中心版本仓库必须始终可用。
对于Git而言,每个程序员都拥有一个本地git仓库,而不仅仅是一份代码拷贝,这个仓库就是一个独立的版本管理节点,它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上,程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下,任意两个git仓库之间可以进行点对点(比如Radicle)的协作,这种协作无需中间协调者(中心仓库)参与。
Git 是怎么实现的
Git 通过建立一个名为 .git 的隐藏文件夹,来存储相关的信息。
我们按照功能来依次分析
-
文件存储 objects
Object 是这个文件管理系统中最基本的单元,就代表了一个普通的文件,每一个被 git 管理的文件都会计算出一个 hash 值然后压缩放置于该目录中。同时一个提交和目录还有标签页被抽象成了对象的一种。
-
blob
压缩过的普通文件。
注意这里是根据文件内容进行 hash,也就是不同文件名内容相同的文件只会生成一个 object。
-
tree
目录,包含一个 entries 数组指向了当前目录下的文件或者其他目录,构成了一棵文件树。
文件的文件名是存储在 Tree 中的。
-
commit
版本,指向了属于该版本对应的目录以及所有的文件。同时还记录着提交人、时间、注释等其他信息。
commit 也包含了指向上一个 commit 的指针数组 parent,这样所有的 commit 就能串成一条链表。
注意 parent 可能会有多个,这也就意味着 commit 其实是一个网状结构,而不只是单纯的一条线。
-
tag
标签,和 commit 关联,用来标记里程碑。
-
-
引用 refs
-
heads
每一个文件都代表一个分支,文件里则是对应 commit 的hash值。
-
remotes
远端的分支
-
tags
轻量级 tag
-
-
工作区
就是一个目录路径,代表了你在当前实际可见的文件集合,区别于备份于.git目录中的文件。
-
暂存区 index
指向的文件是工作区的文件,跟工作区唯一的区别就是,只有被
git add
过的文件才会出现在 index 中被 git 托管。 -
HEAD
符号引用,指向目前所在的分支, 也就是 commit 的最尾端
-
日志 logs
日志,存储了各个分支的日志记录。
└─refs ├─heads // 本地分支 └─remotes // 远程分支 └─origin
-
钩子 hooks
用于在特定的重要动作发生时触发自定义脚本。
Git 命令到底做了什么
将SVN命令按照使用场景来分类
-
本地仓库
可以理解成 .git 目录
- git init
-
工作区和暂存区
-
git add
可以将一个文件或者目录加入到暂存区中,不过配置在
.gitignore
中的文件会被忽略 -
git rm
rm 用于删除一个文件,git rm 默认会将文件从暂存区和工作区都移除,可以加 –cached 选项只从暂存区移除。只移除工作区的话,直接使用操作系统提供的原生 rm 命令。
-
git commit
在调用 git commit 命令时,git 会将 index 引用的所有文件全部计算 hash 值,然后将当前指向的 commit 作为父节点,创建一个新的 commit 对象,然后让当前分支指向新创建的 commit 对象。
--amend
上面这条命令会将最后一次的提交信息载入到编辑器中供你修改 -
git reset
让当前分支 HEAD 指向指定的 commit,并且根据选项改动暂存区和工作区。
- soft: 暂存区和工作区都不重置,仅仅改一下 head 指向的 commit。
- mixed(默认): 暂存区会被重置,但是工作区不会。
- keep:暂存区和工作区都会重置,但是变更会被重新应用。
- hard: 暂存区和工作区都会重置到指定提交的状态。
-
-
分支
-
git branch
NOOP
列出所有分支[branch name]
新建分支-d -D
删除分支 -
git checkout / git switch
[branch name]
切换到某个新的分支-b
新增分支,然后切换过去
-
-
合并
-
git merge
将多个 commit 合并形成一个新的 commit。可以通过
git merge -s 策略名字
来强指定使用的策略类型-
Fast-forward
git 只需要将合并分支的ref指向最后一个 commit 节点。
Fast-forward是 git 在合并两个没有分叉的分支时的默认行为。
-
Recusive
通过算法寻找两个分支的最近公共祖先节点,再将找到的公共祖先节点作为base节点使用三向合并的策略来进行合并。
无法合并的内容需要用户来手动解决冲突。
-
Ours & Theirs 参数
在合并时我们可以带上
-Xours
,-Xtheirs
参数,表明合并遇到冲突时全部使用其中一方的更改。注意这两个参数只有遇到冲突时才会生效,这和Ours策略不一样。
-
Ours
无论有没有冲突,git会完全丢弃被合并分支上的内容,只保留合并分支的上的修改,只是在commit的记录上会保留另一个分支的记录。
-
Octopus
如果现在有多个分支需要合并,使用Recursive策略进行两两合并会产生大量的合并记录。
Octopus 在合并多个分支时只会生成一个合并记录,这也是git合并多个分支的默认策略。
--squash
合并时,将对应分支的多个commit合并成一个commit -
-
git rebase
对一个分支做「变基」操作。将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。
使用场景:
-
合并
commit
记录,保持分支整洁git rebase -i HEAD~4
-
减少分支合并的记录
# feature1 git rebase master
首先,
git
会把feature1
分支里面的每个commit
取消掉。其次,把上面的操作临时保存成
patch
文件,存在.git/rebase
目录下。然后,把
feature1
分支更新到最新的master
分支。最后,把上面保存的
patch
文件应用到feature1
分支上。
潜在风险:
多人协作时,如果你进行了rebase操作,可能导致其他人的commit丢失。
使用建议:
开发只属于自己的分支时尽量使用rebase,减少无用的commit合到主分支里,多人合作时尽量使用merge,一方面减少冲突,另一个方面也让每个人的提交有迹可循。
-
-
-
远程仓库
-
git push
尝试将本地分支的指针内容覆盖掉远端对应分支的指针 ORIGIN_HEAD,然后将本地指针指向的但是远端没有的对象,全部推送到远端。
默认仅在 fast-forward 状态下才可以合并,即git push 在远端指针不是本地指针的祖先时会拒绝覆盖。
而 –force,可以让 Git 不进行这个检查,直接覆盖远端对应 master 指针的内容。
-
git fetch
基于本地的 FETCH_HEAD 记录,比对本地的 FETCH_HEAD 与远程仓库的版本号,获得当前的远程分支的后续版本的数据。
-
git pull
默认情况下,git pull 相当于 git fetch + git merge
--rebase
相当于 git fetch + git rebase, 不会因为拉取代码而生成新的commit记录
-