跳到主要内容

Git 概念与原理

掌握 Git 的使用有助于我们打开代码世界的大门。

学习 Git 的难点在于概念理解,Git 的概念是由一套完整的思维逻辑所构成。

问题

  • 如何理解、掌握 rebase 指令的使用?
  • 为什么要 commit 后再 push 这么啰嗦,而不能直接提交到中央仓库?
  • reset 这个指令为什么这么神奇,好多看起来并不相似的操作却都要用到它?它到底是用来干什么的?
  • revert 和 rebase 都可以撤销历史提交?它们的区别在哪?什么,你说 reset 也行?
  • 如何修改历史提交中的错误?
  • 误删 branch 怎么办?
  • merge 和 rebase 的区别?
  • reset 的几种实用用法?
  • 查找特定的提交记录
  • 什么是 SHA-1

Git 基本

版本控制系统

概念:

  • 版本控制系统:Version Control System - VCS
  • 中央式版本控制系统:Centralized VCS
  • 分布式版本控制系统:Distributed VCS

中央式:

  • 工作模型:
    • 有一个中央服务器存储所有版本数据
    • 开发者从中央服务器获取最新代码
    • 开发者修改后直接提交到中央服务器
  • 优点:
    • 管理简单,权限控制方便
    • 代码都在服务器上,安全性好
    • 学习成本低,容易上手
  • 缺点:
    • 必须联网才能工作
    • 中央服务器出现故障,所有人都无法工作
    • 服务器压力大,数据备份困难

分布式:

  • 工作模型:
    • 每个开发者都有完整的本地仓库
    • 可以在本地提交代码,形成版本历史
    • 可以从其他开发者处获取代码
    • 可以推送代码到远程仓库
    • 开发者之间可以建立对等连接
  • 优点:
    • 可在本地操作,速度快,无需联网
    • 可分步提交代码,便于 review 和回溯
  • 缺点:
    • 初次 clone 项目较慢
    • 本地存储占用较大

仓库体积问题:

  • 一般项目主要是文本代码,体积小且易压缩,推荐 DVCS(Git)
  • 游戏项目包含大量媒体文件难压缩,故多用 CVCS(SVN)

Git 安装

Git 配置(WIP)

Git 基本命令

利用 GitHub 创建项目,完成提交推送操作:

# 克隆仓库到本地
git clone [远程地址]

# 查看仓库日志
git log

# 查看仓库状态
git status

# 添加到版本控制
git add [文件名]

# 提交改动
git commit

# 推送改动
git push

假装同事

clone 一次远程仓库到本地,当做你的模拟同事的本地仓库(Git 的管理是目录级别,而不是设备级别)。

如果跟原仓库在同级目录,可以在 clone 命令加个参数:

git clone https://github.com/[accoutn-name]/[repo-name].git [自定义仓库名称]

拉取改动:

# 拉取改动到本地(影响本地文件)
git pull

# 抓取提交记录(不影响本地文件)
git fetch

push 冲突

冲突处理:

  1. pull 把远程仓库上的新内容取回到本地和本地合并
  2. 把合并后的本地仓库向远程仓库推送

冲突处理的过程,也称为“合并”的过程,对应的 Git 指令是 merge

git pull 指令内部实现是把远程仓库用 git fetch 取下来后再进行 merge 操作。

Git 进阶

HEAD、master 与 branch

引用(reference):

  • commit 的快捷方式:Git 提供了“引用”机制:使用固定的字符串作为引用,指向某个 commit,作为操作 commit 的快捷方式
  • 引用的本质就是一个个的字符串,可以是 commit 的 SHA-1 码,也可以是一个 branch(ref: refs/heads/feature3)

HEAD:当前 commit 的引用(可以理解为 Git 中一个独特的引用)。

branch:也是一种引用。

master:默认 branch。

branch 命令:

# 创建分支
git branch [分支名称]

# 切换分支
git checkout [分支名称]

# 创建并切换分支
git checkout -b [分支名称]

# 删除分支
git branch -d [分支名称]

# 强制删除分支
git branch -D [分支名称]

git clone 的本质:git checkout master

push 的本质

  • push 是把当前的分支上传到远程仓库,并把这个 branch 的路径上的所有 commit 也一并上传
  • push 的时候,如果当前分支是一个本地创建的分支,需要指定远程仓库名和分支名,用 git push origin branch_name 的格式,而不能只用 git push;或者可以通过 git config 修改 push.default 来改变 push 时的行为逻辑
  • push 执行之后会上传当前分支,但不会上传 HEAD;远程仓库的 HEAD 是永远指向默认分支(即 master

merge:合并 commits

  • merge 的含义:从两个 commit 分叉的位置起,把目标 commit 的内容应用到当前 commit(HEAD 所指向的 commit),并生成一个新的 commit
  • merge 的适用场景:1)单独开发的 branch 用完了以后,合并回原先的 branch;2)git pull 的内部自动操作
  • merge 的三种特殊情况:
    • 冲突(Conflict):原因:当前分支和目标分支修改了同一部分内容,Git 无法确定应该如何合并;处理:解决冲突后手动 commit
    • HEAD 领先于目标 commit:Git 什么也不做,空操作
    • HEAD 落后于目标 commit:快速移动(fast-forward)

Feature Branching:最流行的工作流

Feature Branching 工作流:

  • 任何新的功能(feature)或 bug 修复全都新建一个 branch 来写
  • branch 写完后,合并到 master,然后删掉这个 branch

关于 Pull Request:

  • 不是 Git 的特性,而是一些 Git 仓库服务提供方(如 GitHub)所提供的一种便捷功能

关于 add

使用 add 可以把改动的东西放进暂存区。

add 添加的是文件改动,而不是文件名。

# 把所有改动放进暂存区
git add .

# 提交改动
git commit

实际上,VSCode, IntelliJIDEA 等 IDE 工具不需要 add,都是直接 commit。

看看我都改了什么

查看历史记录:

git log

查看详细历史记录:

git log -p

查看简要统计:

git log --stat

查看具体的 commit:show

看当前 commit:

git show

看任意一个 commit:

# git show [引用]
git show 5e68b0d8

看指定 commit 中的指定文件:

# git show [引用] 文件名
git show 5e68b0d8 list.txt

看未提交的内容:diff

对比暂存区和上一条提交:

git diff --staged

提示:--staged 有一个等价的选项叫做 --cached。这里所谓的「等价」,是真真正正的等价,它们的意思完全相同。

比对工作目录和暂存区:

git diff

比对工作目录和上一条提交:

git diff HEAD

总结:

  • 查看历史中的多个 commit:log
  • 查看具体某个 commit:show
  • 查看未提交的内容:diff

.gitignore —— 排除不想被管理的文件和目录

在 Git 中有一个特殊的文本文件:.gitignore。这个文本文件记录了所有你希望被 Git 忽略的目录和文件。

.gitkeep —— 提交空目录

Git 不能提交空目录,如果非要提交,可以在空目录中放一个 .gitkeep 文件。

Git 高级

不喜欢 merge 的分叉?用 rebase

如果你不喜欢 merge 后的分叉历史,可以用 rebase 来保持提交历史的线性。

rebase(变基):给 commit 重新设置基础点(也就是父 commit) —— 即把你指定的 commit 以及它所在的 commit 串,以指定的目标 commit 为基础,依次重新提交一次。

我的理解:把一个分支上的一段 commit 串摘下来(剔除),嫁接到另一个分支上(移动)

(这里应该有图示)

示例:

合并操作(merge):

git merge branch1

变基操作(rebase):

git checkout branch1
git rebase master

# 还要切回 master 再 merge 一下,把 master 移到最新的 commit
git checkout master
git merge branch1

为什么还要切回 master 再 merge 一下?

  • 因为如果在 master 执行 rebase,会导致 master 上最新 commit 被剔除掉,而如果远程仓已经这几个最新 commit 的话,就会导致无法 push
  • 为了避免和远程仓库发生冲突,一般不要从 master 向其它分支执行 rebase,而如果是非 master 分支,则可以直接 rebase

刚刚提交的代码,发现写错了怎么办?

一个地方写错,然后本地 commit 了,但还没 push 到远程仓库

解决:

# 替换上一次提交
git commit --amend

"amend" 是“修正”的意思。在提交时,如果加上 --amend 参数,Git 不会在当前 commit 上增加 commit,而是会把当前 commit 里的内容和暂存区(stageing area)里的内容合并起来后创建一个新的 commit,用这个新的 commit 把当前 commit 替换掉。所以 commit --amend 做的事就是它的字面意思:对最新一条 commit 进行修正。

思考:已经提交到远程仓库就不适用了。

写错的不是最新的提交,而是倒数第二个?

如果不是最新的 commit 写错,就不能用 commit --amend 来修复了,而是要用 rebase。不过需要给 rebase 也加一个参数:-i

rebase -i = rebase --interactive = 交互式 rebase

所谓“交互式 rebase”,就是在 rebase 的操作执行之前,你可以指定要 rebasecommit 链中的每一个 commit 是否需要进一步修改。

那么你就可以利用这个特点,进行一次“原地 rebase“。

操作:

  1. 执行 git rebase -i 目标 commit
  2. 在编辑界面中指定需要操作的 commit 以及操作类型
  3. 操作完成之后用 git rebase --continue 来继续 rebase 过程
# 查看下提交记录
git log

# 开启交互式 rebase 过程(跳到一个新界面)
git rebase -i HEAD^^

# 修改写错的 commit
git commit --amend

# 继续 rebase 过程
git rebase --continue

提示:Git 偏移符号:

  • ^:在 commit 后面加一个或 n 个 ^,表示可以把 HEAD 所指向的 commit 往回偏移 n 个
  • ~:同理,向前偏移 n 个

思考:这样做有什么好处?复杂度提升,只是为了可以减少一个新 commit?

比错还错,想直接丢弃刚写的提交?

写完回头看了看,你觉得“不行这得重新写“。那么你可以用 reset --hard 来撤销这条 commit:

# 强制重置到上一个 commit
git reset --hard HEAD^

不过,被撤销的提交并没有消失,只是你不再用到它了。

如果你在撤销之前记下了它的 SHA-1,那么还是也通过 SHA-1 来找到它。

git reset --hard HEAD~2
git reset --hard HEAD^^
git reset --hard ardsd123(SHA1 值)

想丢弃的也不是最新的提交?

假如有一个 commit ,你在刚把它写完的时候并没有觉得它不好,可是在之后又写了几个提交以后,你突然灵光一现:“哎呀,那个 commit 不该写,我要撤销!”

不是最新的提交,就不能用 reset --hard 来撤销了。这种情况的撤销,就要用之前介绍过的一个指令:交互式 rebase ——rebase -i

除了用交互式 rebase ,你还可以用 rebase --onto 来更简便地撤销提交。

--onto 参数后面有三个附加参数:

  • 目标 commit
  • 起点 commit(注意:rebase 的时候会把起点排除在外)
  • 终点 commit
# 删除最近一次提交
git reset --hard HEAD^

# 显示最近三次调教,手动删除其中一次提交或pick改为d
git rebase -i HEAD~3

# 移除commit4,commit5转移到commit3之后
git rebase --onto commit3 commit4 commit5

代码已经 push 上去了才发现写错?

代码 push 到中央仓库后发现 commit 写错了,分两种情况:

  • 错误在自己的 branch
  • 错误已合并到 master

如果错误在自己的 branch:

  • 可以用前面的方法修改或删除错误的 commit,然后重新 push。但由于本地修改了已有 commit,直接 push 会失败
  • 这时不要先 pull,而是强制 push

如果错误已合并到 master:

  • 作为团队协作,你不能用上面强制 push 方法(一些团队禁止任何 force push),而是用撤销 revert
  • 用法很简单,你希望撤销哪个 commit,就把它填在后面:
git revert HEAD^

提示:撤销是 Git 自动帮你完成回退工作,所以你要提交,这样历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit

reset 的本质 —— 不止可以撤销提交

reset 的本质是移动 HEAD 及其指向的 branch 的位置,而不是撤销 commit。

reset --hard HEAD^ 通过把 HEAD 和 branch 一起移动到父 commit,从而实现了「撤销」的效果。

Git 的历史只能往回看,不能向未来看,所以把 HEAD 和 branch 往回移动,就能起到撤回 commit 的效果。

所以同理,reset --hard 不仅可以撤销提交,还可以用来把 HEAD 和 branch 移动到其他的任何地方。

git reset --hard branch2

不同参数:

# 强制重置:重置位置的同时,清空工作目录的所有改动
git reset --hard

# 软重置:重置位置的同时,保留工作目录和暂存区的内容,并把重置 HEAD 的位置所导致的新的文件差异放进暂存区
git reset --soft

# 默认重置:重置位置的同时,保留工作目录的内容,并清空暂存区
git reset --mix

checkout 的本质

checkout 的本质是签出指定的 commit,所以你不止可以切换 branch,也可以直接指定 commit 作为参数,来把 HEAD 移动到指定的 commit:

git checkout HEAD^^
git checkout master~5
git checkout 78a4bc
git checkout 78a4bc^

那么,checkout 和 reset 有什么不同?

  • checkout 和 reset 都可以切换 HEAD 的位置,它们除了有许多细节的差异外,最大的区别在于:reset 在移动 HEAD 时会带着它所指向的 branch 一起移动,而 checkout 不会。当你用 checkout 指向其他地方的时候,HEAD 和 它所指向的 branch 就自动脱离了
  • checkout 有一个专门用来只让 HEAD 和 branch 脱离而不移动 HEAD 的用法:
# 执行这行代码,Git 就会把 HEAD 和 branch 脱离,直接指向当前 commit
git checkout --detach

紧急情况:立即给我打个包,现在马上!

临时存放工作目录的改动:stash

stash 指令可以临时保存当前工作目录的内容到本地。保存后工作目录会恢复干净,你可以去处理其他临时工作,完成后再取回之前的改动继续工作。

当你需要暂时清理工作目录时,可以:

# 清空工作目录的改动,所有改动都被存了起来
git stash

# 取回之前存的东西
git stash pop

# 获取 stash 列表
git stash list

注意:没有被 track 的文件(即从来没有被 add 过的文件不会被 stash 起来,因为 Git 会忽略它们。如果想把这些文件也一起 stash,可以加上 -u 参数,它是 --include-untracked 的简写。就像这样:

git stash -u

branch 删过了才想起来有用?

branch 用完就删是好习惯,但有的时候,不小心手残删了一个还有用的 branch ,或者把一个 branch 删掉了才想起来它还有用,怎么办?

branch 是引用,,HEAD 也是引用,他们引用指向的都是某次 commit

reflog 是 "reference log" 的缩写,使用它可以查看 Git 仓库中的引用的移动记录。如果不指定引用,它会显示 HEAD 的移动记录。假如你误删了 branch1 这个 branch,那么你可以查看一下 HEAD 的移动历史:

git reflog

分析示例:

  • HEAD 的最后一次移动行为是「从 branch1 移动到 master」。而在这之后,branch1 就被删除了。所以它之前的那个 commit 就是 branch1 被删除之前的位置
  • 所以现在就可以切换回 c08de9a,然后重新创建 branch1 :
git checkout c08de9a
git checkout -b branch1

这样,你刚删除的 branch1 就找回来了。

reflog 默认查看 HEAD 的移动历史,除此之外,也可以手动加上名称来查看其他引用的移动历史,例如某个 branch:

git reflog master
# 或
git reflog main

tag —— 不可移动的 branch

tag 是一个和 branch 非常相似的概念,它和 branch 最大的区别是:tag 不能移动。所以在很多团队中,tag 被用来在关键版本处打标记用。

cherry-pick —— 把选中的 commit 一个个合并进来

cherry-pick 是一种特殊的合并操作,使用它可以点选一批 commits,按序合并。

实际上,很多时候选一批 commit 合并会报错,为什么?

git config —— Git 的设置

git config 可以对 Git 进行基础设置,包括用户名、邮箱和界面展示等。设置一次后就可以一直使用,非常方便。

Git Flow —— 复杂又高效的工作流

Git Flow 的机制非常完善,很适合大型团队的代码管理。不过由于它的概念比较复杂(虽然难度并不高),所以并不适合新手直接学习,而更适合在不断的自我研究中逐渐熟悉,或者在团队合作中慢慢掌握。

更多关于 Git Flow:Gitflow Workflow | Atlassian Git Tutorial

查找特定提交记录(WIP)

  • 本地:git log + grep
  • 远程:利用 Git 服务商的 commit 搜索功能

自建 Git 服务(WIP)

Git 好玩的用法(WIP)

资源(WIP)

参考