有这么一个段子...

老杨一推代码, 所有的开发同事便都看著他笑, 有的叫道, 「 老杨, 你又把代码合丢了!」 他不回答, 对产品说, 「 追加两个需求, 只做性能优化。」 便排出一台MacBook Pro。 他们又故意的高声嚷道, 「 你推错分支了!」 老杨睁大眼睛说, 「 你怎么这样凭空污人清白……」, 「 什么清白? 我前天亲眼见你写Bug把流水线搞挂, 又改崩生产代码「。 老杨便涨红了脸, 额上的青筋条条绽出, 争辩道, 「 流水线报错不算错, 合代码挂掉的事能叫Bug吗?」 接连便是难懂的话, 什么「 Merge大法好, GitFlow卍解」, 什么「 拉代码不需要Rebase」 之类, 引得众人都哄笑起来: 店内外充满了快活的空气。

用 git 的, 谁没合丢过代码呢? (反正我是合丢过...)

好, 痛定思痛, 我决定从原理上理解Git, 所以决定写下这篇文章(才不会说是因为buddy叫我准备一场针对 git 的 session).

PS: git官网上有看都看不完的长篇大论, 甚至有人专门为理解git的一种workflow出书...这些都说明"看这篇文章就想把git搞透彻是不可能的"(因为写文章的人都没有把git搞透彻(再次苦笑)).

So: 这篇文章不会是一个体系完善的Git教程, 只会针对性地从原理上理解一些我们常用的Git命令, 最终目标就是能够在基本的多人合作的场景下正常工作, 同时对于复杂的场景能有用于检索资料的先验知识.

目录

  • 目录
  • Git的那些概念
    • Git的本质是什么?
    • Git中存储的对象
      • 1. blob对象
      • 2. tree对象
      • 3. commit对象
      • ref(引用)
  • Git的那些常用操作
    • file-level
    • commit/branch-level
    • repo-level
  • Git的那些冲突
    • 出现冲突的可能情形
    • 规避冲突的优选操作
    • 其他的奇技淫巧
      • 别名
      • 子模块(submodule)

Git的那些概念

Git的本质是什么?

Git的本质其实是复读机一个内容定址(content-addressable)文件系统, 它的核心部分是一个简单的键值对资料库。我们可以向该资料库写入值(object), 它会返回一个键(object的引用地址, SHA-1字元串), 通过该键可以在任意时刻再次检索值. 我们所操作的版本控制, 其实就是不断地向这个文件系统写入操作日志.

那么这些被写入的操作日志是什么呢?

Git中存储的对象

1. blob对象

对应的值是单个文件内容的快照, 这个快照不包括文件名。

2. tree对象

以树的形式记录的目录结构和文件的索引, 每个普通结点(有子级的结点)都是一个子级的tree对象的包裹体, 每个叶子结点(无子级的结点)都是一个blob对象的包裹体, 这些包裹体会附带文件(夹)的名称等元数据。

3. commit对象

对应的值包含一个数据集(通常称为Comments, 这个数据集包含父级commit的地址(SHA1值, 通常也被称作commitid)、作者以及提交message等信息)以及一个当前commit对应的变更的tree, tree的内容为当次提交的变动快照(没有发生变动的文件不会被加入快照)。

总结一下, 三者关系:

所以, 当存在多个提交时,

#bash
git init
echo "version 1" > test.txt
git add .
git commit -m "frist commit"

echo "version 2" > test.txt
echo "new file" > new.txt
git add .
git commit -m "second commit"

mkdir bak
echo "version 1" > bak/text.txt
git add .
git commit -m "third commit"

最终的整体结构应该是这样:

ref(引用)

  • branch: 指向某一系列提交之首的引用
  • HEAD: 指向目前工作基点提交的引用
  • tag:
    • lightweight tag: 指向任意提交的引用
    • annotated tag: 指向一个标签对象的引用(注:标签对象其实和上面提到的三种一样也是对象, 不过非常罕见, 结构类似commit, 不过内含数据不是指向变动快照的tree, 而是指向commit)

  • remote branch: 指向某伺服器端某一系列提交之首的引用

Git的那些常用操作

file-level

  • add @path: 标记文件的stage状态
  • reset @path: 取消文件的stage状态
    • --hard: 取消文件的stage状态并恢复其到unmodified状态

  • checkout @path: 等价于reset @path --hard

commit/branch-level

以下一些操作可以到learngitbranching上进行互动式演示, 接下来在讲解相关概念的时候会在这里做相关的命令演示, 小伙伴们可以在上面操作一下

注:

  1. 以下的所有commitid都可以换成branch(或者说ref)
  2. 以下的所有commitid都会是简称(例如c1等)

git clone # 初始化模拟仓库

  • checkout @commitid=null: 转移HEAD到指定提交

git checkout c0 # 将HEAD移动到c0(master保持不变, 此时HEAD为detached HEAD, 直接指向了commit而非某个ref)

  • commit: 提交
    • --amend: 与前一个提交合并提交(改写)

git commit #生成一个新提交
git commit --amend #改写基点提交, 当前worktree内容融合基点提交的内容重新生成一个提交

  • branch: 创建一个分支

git branch branch1 # 基于当前生成一个新分支
git checkout branch1 # 移动HEAD到这个新分支(branch1)

  • reset @commitid=null: 重置HEAD及其所指向的ref到指定提交
    • --hard: 抛弃reset过程中的所有文件变更()

git reset master # 重置HEAD及其所指向的ref到master所在提交
# 重置HEAD及其所指向的ref到"c2"提交
git reset c2

PS: 关于reset和check的区别

  • revert @commitid: 提交一次与某次提交的内容完全相反的提交

# 生成一次和c2相反的提交(抵消/还原c2提交并向前移动HEAD, 新版本的git还支持批量revert(git revert start..end)
git revert c2

  • cherry-pick @commitid: 将某个commit的变动叠加到HEAD所指向的commit上(会创建一个和之前的commit内容一样的提交, 不过两者具有不同的sha1值)

git cherry-pick c1 # 将c1提交的变动快照同步到当前HEAD位置, 新版本的git还支持批量cherry-pick(git cherry-pick start..end)

  • tag: 标记具有某种特殊意义的提交(里程碑)

git tag OnMerge # 给当前提交取名"OnMerge", 可以很方便地回滚到特定版本

  • merge @commitid: 将某一次提交的内容整合到HEAD(生成一次merge提交叠加在HEAD之上并移动HEAD)

git checkout master # 模拟其他人在master上进行了两次提交
git commit # 模拟一次提交c3
git commit # 再模拟一次提交c4
git checkout branch1 # 切回HEAD到branch1
git merge c3 # 单独合并c3的变动到当前HEAD
git reset c1
git merge master # 合并master上所有的变动到当前HEAD

  • rebase @commitid: 将某一次提交的内容整合到HEAD(把所有不在commitid对应提交所在提交链上的提交叠加在该提交上)
    • -i 互动式, 可以选择哪些提交要被rebase到指定位置后

git checkout master # 模拟其他人在master上进行了两次提交
git commit # 模拟一次提交c7
git commit # 再模拟一次提交c8
git checkout branch1 # 切回HEAD到branch1
git rebase c7 # 把当前分支的变更叠加到c7上
git rebase OnMerge # 撤销一下
git rebase master -i # 把当前分支的变更叠加到master上(效果等效于merge, 不过rebase假定自己的编辑都是基于master的)

git checkout master # 如果经常保持rebase, 那么当branch1需要被merge回master时, 会非常容易
git merge branch1 # 冲突已经在频繁的rebase中被解决的, 这里的merge都会是"快速前进"
# 可以说是多分支开发中的"单分支开发"

repo-level

  • fetch: 同步remote repo的状态
  • remote: 管理remote repo及相关分支, 几乎只需要单次配置, 不做赘述.
  • push: 将本地的commit推送到伺服器(默认不会推送tag)
    • push --tag: 连tag一起推送

Git操作实体的总体结构

Git的那些冲突

出现冲突的可能情形

  • 他人和自己编辑了对同一文件的编辑内容存在交叉行
  • 他人删除了自己编辑的文件
  • 他人和自己新增了同名的文件

规避冲突的优选操作

  • 代码稳定后尽快推送(尤其针对公共内容的改动)
  • 勤获取代码(Build前自动获取最新代码)

其他的奇技淫巧

别名

git config --global alias.co checkout

子模块(submodule)

# 在现有的git仓库内执行
git submodule add -b master https://example.com/another-repo.git another-repo # 将another-repo添加为当前git仓库的子模块, 并存放到another-repo路径下
git submodule update --remote # 从远端更新当前仓库的子模块

或著针对已经存在的子模块

# 在现有的git仓库内执行
git submodule add another-repo # 将another-repo目录添加为当前git仓库的子模块(前提是another-repo需要是一个git仓库), 不指定分支的时候, git会默认将当前HEAD所在的commit作为子模块(即子模块处于非track状态)
git push --recurse-submodules=<check|on-demand> # push之后子仓库状态即可同步, 其他团队成员可以通过git submodule update --remote来更新本地子模块, check9默认)为循环检测子模块, on-demand为仅提交当前仓库的一级子模块

EOF

推荐阅读:

相关文章