首页 git 高手跃迁
文章
X

git 高手跃迁

本文试图从应用和原理及设计层层多角度探究 git,帮助读者相对全面透彻的理解 git,并能 随心所欲高效地使用 git

俯视 git

本节将从应用中涉及到的顶层概念到底层实现原理,深入浅出地呈现 git 的蓝图。

基本流程

git 工作流 git 命令概览

基本概念

概念是设计的很重要的元素和精简描述,因此我们将从基本概念入手窥探 git 的顶层设计思想。 同时,了解了基本概念之后,能够更好地利用搜索引擎查找所需的命令。同时也有助于我们理解上面的基本流程图。

应用层面的概念

在用户使用git时都会涉及到以下这些应用层面的概念。这些概念往往会体现在常用的 git 命令的应用中。

  • 工作区(workspace):正常情况下能看到的目录(不包括隐藏文件,如同级目录下的 .git 隐藏目录), 也就是用户主动创建的目录,如通过系统命令 ls 或者文件管理器能够看到的目录。
  • 暂存区(stage/index)
    • 工作区下的隐藏 .git 目录下的 index 文件(每个git仓库只有一个), 因此也称为索引。
    • 数据暂时存放的区域,类似于工作区写入版本库前的缓存区。
    • 暂存区是独立于各个分支的。
    • 实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。 在这个虚拟工作区的目录树中,记录了文件名、文件的状态信息(时间戳、文件长度等), 文件的内容并不存储其中,而是保存在 git 对象库(.git/objects)中, 文件索引建立了文件和对象库中对象实体之间的对应关系。git 通过文件状态信息可以很高效地知道 哪些文件改变了。
  • 版本库(Repository):也称为本地仓库。
    • 在工作区中有一个隐藏文件 .git ,它不属于工作区,而是 git 版本库。
    • .git 目录下包括很多其他文件,其中重要的是暂存区(.git/index)、对象库(.git/objects)、 分支(master 分支和其他分支)、指向当前分支的指针 HEAD
  • 远程仓库(Remote):相对于本地仓库而言,该仓库在远程, 两者可以认为是各自的备份(但内容不一定时刻一致)
  • Commit objects 节点:每次提交都会产生一个节点,相关信息存储在对象库
  • heads:head 就是指向一个节点的索引。每个 head 都有自己的名字。默认情况下, 每个 Repository 都有一个叫做 master 的 head。一个Repository 可以有很多 heads(一个分支一个,位于.git/refs/heads)。 在任何时刻,只有一个 head 被当做“当前head”(当前分支)。这个 head 一般用大写字母 HEAD 表示(.git/HEAD)。HEAD 指向当前分支的最新的一次提交。
  • 分支:和 “head” 几乎是同义词
    • 一条 commit objects 节点的链条,链头(head)被命名为分支名(指向该分支的最新节点)
    • 各个分支可以有相同的节点,而所有分支的所有节点信息都存储在对象库中
    • 每一个分支都有一个head,每一个head都代表一个分支
    • Repository 是一棵树(也称为树对象,不同分支共用相同的节点,即一个节点可能在多条链上), “分支” 用来表示以 head 为叶子节点的所有祖先节点的序列, 而 “head” 仅仅表示 head 所指向的那一个节点,即分支中最近被提交的节点

git 目录结构

这里只是粗略的标注一下各个文件夹的作用。 如果想得到更为详细地官方解读,可在终端输入 git help gitrepository-layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  .git/
  ├── COMMIT_EDITMSG  # 记录最近一次提交的 commit 编辑信息
  ├── config          # 项目的配置信息,git config命令会改动它
  ├── description     # 项目的描述信息
  ├── HEAD            # 当前分支
  ├── hooks           # 系统默认钩子脚本目录
  │   ├── applypatch-msg.sample
  │   ├── commit-msg.sample
  │   ├── post-update.sample
  │   ├── pre-applypatch.sample
  │   ├── pre-commit.sample
  │   ├── prepare-commit-msg.sample
  │   ├── pre-push.sample
  │   ├── pre-rebase.sample
  │   └── update.sample
  ├── index           # 索引文件(暂存区), 二进制文件
  ├── info
  │   └── exclude
  ├── logs            # 各个refs的历史信息, 保存所有更新的引用记录
  │   ├── HEAD
  │   └── refs
  │       ├── heads
  │       │   ├── dev
  │       │   └── master
  │       └── remotes
  │           └── origin
  │               └── master
  ├── objects         # 可以视为 git 数据库; Git本地仓库的所有对象(commits, trees, blobs, tags)
  │   ├── 2d
  │   │   └── d2c71b69c837a7459f4bd9c9f7db6520731d06
  │   ├── 5c
  │   │   └── 7b8eda18a75e13d27c31e65a54b0abd7948510
  │   ├── 77
  │   │   └── cad3aecf7c2754231095598119979d62a1e1da
  │   ├── info
  │   └── pack
  └── refs           # 标识你项目里的每个分支指向了哪个提交(commit)
      ├── heads      # 目录有以各个本地分支名命名的文件,保存对应分支最新提交的ID
      │   ├── dev
      │   └── master
      ├── remotes    # 目录有以各个远程分支名命名的文件,保存对应分支最新提交的ID
      │   └── origin
      │       └── master
      └── tags       # 存储在开发过程中打的标签,里面的文件以标签名命令,文件内容为对应的ID

git 文件状态机

文件状态涉及到的的概念:

  • 未跟踪:就是新创建的文件没添加过一次,没有之前的快照记录
  • 已跟踪:个新创建的文件进行第一次git的添加,这样文件就有了快照记录后,这文件就转变成已跟踪状态了
  • 已修改状态:如果从远程仓库取出Git目录后,作了修改但还没有放到暂存区域,就是已修改状态
  • 暂存状态:如果对Git目录作了修改并已放入暂存区域,就属于已暂存状态
  • 提交状态:如果是 Git 目录中保存着的特定版本文件,也就是说将暂存区的文件提交到仓库中,就属于已提交状态 (git commit)。

文件状态机

底层概念

相对于应用层概念而言,大部分 git 用户是很少遇到底层概念。底层概念与git的内部实现紧密相关,也映射 出一套较为底层的命令。因此,要想理解git的底层原理,就很难避开这些底层概念。

对象库(.git/object)有四种类型:块(blob)、目录树(tree)、提交(commit)、标签(tag)。这四种原子对象构成了 git 高层数据结构的基础。

  • blob(块对象):在add操作后生成, blob 对象存储文件的时间内容,实际为工作空间的文件内容。具体为对文件内容使用zlib算法压缩,然后对得到的字节取hash算法。 因此相同的文件内容,得到的blob对象肯定是相同的
  • tree(树对象):在commit操作后生成, git目录树对象映射操作系统中工作空间的目录,不同的是工作空间的目录下是文件和文件夹的集合, 而目录树对象则为blob对象和目录树对象的集合。
  • commit对象:一次提交即为当前版本的一个快照,该快照就是通过提交对象保存, 其存储的内容为:一个顶级树对象、上一次提交的对像啥希、提交者用户名及邮箱、提交时间戳、提交评论。

对象库变化

Git对象库被组织及实现成一个内容寻址的存储系统。具体而言,对象库中的每个对象都有一个唯一的名称, 这个名称是向对象的内容应用SHA1得到的SHA1散列值。因为一个对象的完整内容决定了这个散列值, 并且认为这个散列值能有效并唯一地对应特定的内容,所以SHA1散列值用来做对象数据库中对象的名字和索引是完全充分的。 文件的任何微小变化都会导致SHA1散列值的改变,使得文件的新版本被单独编入索引

SHA散列计算的一个重要特性是不管内容在哪里,它对同样的内容始终产生同样的ID。 换言之,在不同目录里甚至不同机器中的相同文件内容产生的SHA1哈希ID是完全相同的。 因此,文件的SHA1散列ID是一种有效的全局唯一标识符

如果两个文件的内容完全一样,无论是否在相同的目录,Git在对象库里只保存一份blob形式的内容副本。 Git仅根据文件内容来计算每一个文件的散列码,如果文件有相同的SHA1值,它们的内容就是相同的, 然后将这个blob对象放到对象库里,并以SHA1值作为索引。项目中的这两个文件, 不管它们在用户的目录结构中处于什么位置,都使用那个相同的对象指代其内容。

如果这些文件中的一个发生了变化,Git会为它计算一个新的SHA1值,识别出它现在是一个不同的blob对象, 然后把这个新的blob加到对象库里。原来的blob在对象库里保持不变,为没有变化的文件所使用。

当文件从一个版本变到下一个版本的时候,Git的内部数据库有效地存储每个文件的每个版本,而不是它们的差异。 因为Git使用一个文件的全部内容的散列值作为文件名,所以它必须对每个文件的完整副本进行操作。 Git不能将工作或者对象库条目建立在文件内容的一部分或者文件的两个版本之间的差异上。

随着时间的推移,所有信息在对象库中会变化和增长,项目的编辑、添加和删除都会被跟踪和建模。 为了有效地利用磁盘空间和网络带宽,git 把对象压缩并存储在打包文件(pack file)里, 这些文件也在对象库里。

另外,还有两个比较重要但不在对象库中的概念:

  • 标签(tag):一个标签对象分配一个任意的且人类可读的名字给一个特定对象,通常是一个提交对象
  • 索引:索引是一个临时的、动态的二进制文件,它描述整个版本库的目录结构。 更具体地说,索引捕获项目在某个时刻的整体结构的一个版本。项目的状态可以用一个提交和一棵目录树表示, 它可以来自项目历史中的任意时刻,或者它可以是你正在开发的未来状态。

Git的关键特色之一就是它允许你用有条理的、定义好的步骤来改变索引的内容。索引使得开发的推进与提交的变更之间能够分离开来。

作为开发人员,你通过执行Git命令在索引中暂存(stage)变更。变更通常是添加、删除或者编辑某个文件或某些文件。 索引会记录和保存那些变更,保障它们的安全直到你准备好提交了。还可以删除或替换索引中的变更。 因此,索引支持一个由你主导的从复杂的版本库状态到一个可推测的更好状态的逐步过渡。引在合并(merge),允许管理、 检查和同时操作同一个文件的多个版本中起到的重要作用

版本库 版本库中各对象关系 整体原理图

底层命令

这里列出几个底层命令协助大家理解git的底层原理,具体使用方法请自行参阅其他资料。

  • git hash-object -w fileName:用来创建一个新的数据对象并将其手动存储到新的Git数据库中
  • git cat-file -p hash-object-id:读取git对象内容
  • git cat-file -t hash-object-id:读取git对象类型
  • git cat-file -p master^{tree}:查看树对象
  • git ls-files -s:查看暂存区内容
  • 构建树对象:
    • git update-index
    • git write-tree
    • git read-tree
  • git commit-tree:创建commit对象

git 存储快照是不是很低效

一个聪明的读者也许已经有了关于Git的数据模型及其单独文件存储的挥之不去的问题: 直接存储每个文件每个版本的完整内容是否太低效率了? 即使它是压缩的,把相同文件的不同版本的全部内容都存储的效率是否太低了?’ 如果你只添加一行到文件里,Git是不是要存储两个版本的全部内容?

幸运的是,答案是“不是,不完全是!”

相反,Git使用了一种叫做 打包文件(pack file) 的更有效的存储机制。 要创建一个打包文件,Git首先定位内容非常相似的全部文件,然后为它们之一存储整个内容。 之后计算相似文件之间的差异并且只存储差异。例如,如果你只是更改或者添加文件中的一行, Git可能会存储新版本的全部内容,然后记录那一行更改作为差异,并存储在包里。

存储一个文件的整个版本并存储用来构造其他版本的相似文件的差异并不是一个新伎俩。 这个机制已经被其他VCS(如RCS)用了好几十年了,它们的方法本质上是相同的。

然而,Git文件打包得非常巧妙。因为Git是由内容驱动的, 所以它并不真正关心它计算出来的两个文件之间的差异是否属于同一个文件的两个版本。 这就是说,Git可以在版本库里的任何地方取出两个文件并计算差异, 只要它认为它们足够相似来产生良好的数据压缩。因此, Git有一套相当复杂的算法来定位和匹配版本库中潜在的全局候选差异。 此外,Git可以构造一系列差异文件,从一个文件的一个版本到第二个,第三个,等等。

Git还维护打包文件表示中每个完整文件(包括完整内容的文件和通过差异重建出来的文件)的原始blob的SHA1值。 这给定位包内对象的索引机制提供了基础。

打包文件跟对象库中其他对象存储在一起。它们也用于网络中版本库的高效数据传输。

图解 git 的概念关系设计

git 中各个概念之间的联系或状态变迁都是通过命令(高层或底层命令)产生的,这样可以灵活组合而不乏清晰。

一次提交过程

一次提交过程有以下子过程:

  • 初始提交(即本次提交的上一次提交)
  • 编辑文件
  • 提交(相对于初始提交而言可称为二次提交)

文件f1没有修改,在此过程后,它的blob哈希没有改变;文件f2修改内容,在此过程后文件为f22,它的blob哈希发生改变,如图: 两次提交间的变化 提交过程发生了什么

git 命令及其使用场景

git 命令很多,一定要善用搜索和 git 帮助文档,可以通过 git help 命令动词 来获得帮助,比如 git help pull。 如果连命令动词都不记得的话,可以使用git help -a列出所有关键字。当执行一个命令或者打错了命令,git 都会 有智能提示,根据提示操作一般都会得到解决。提示的内容一般会有以下类型:

  • 下一步操作(即继续执行)
  • 相反操作(即反悔)

需要指出的是,本章不会穷尽git命令,而是根据常用的场景来组织命令集。详尽的命令可以自行参考git的帮助文档。

各区穿梭

git 可以区化为:工作区、暂存区、堆栈、本地版本库、远程副本和远程仓库等。下图收集的命令可以帮助你在各大区中 任意穿梭。

git 命令概览

改变文件状态

这里的文件包括单个文件或多个文件、目录。因此下面的命令都省略了所要操作的文件名或目录名列表。

  • 未跟踪已跟踪的命令有: git add fileName
  • 已跟踪未跟踪的命令有: git rm(要反悔可以按提示操作,比如 git restore)
  • 暂存修改:git add fileName
  • 撤销暂存(保留暂存区和工作区中相同文件的新修改):
    • 少量文件推荐:git restore –staged fileName
    • git reset – fileName
    • 撤销大量暂存文件推荐:git reset –
    • git rm –cached fileName
  • 撤销工作区的修改(但会保留暂存区中的修改):git checkout [–] fileName

【注意】从以上各命令的相似性和危险性来看,容易混淆容易出错,为安全起见,应遵守以下安全规则:

  • 在 git 仓库不要使用 rm,而是尽量使用 git rm
  • 在操作之前尽量使用 git stash save "note 一下" 保留已经跟踪的文件(包括工作区和暂存区中的文件)

分支管理

一般一个git仓库都会根据不同的需要或者不同的开发人员都会创建多个分支,这就少不了分支间的互操作。 当处于某个特定分支时,我们经常有整理提交链条的需求。这一节我们一起来探讨一下。

分支互操作

所谓分支互操作,就是两个或两个以上分支之间的切换或复制及合并等操作。

git 分支互操作

merge 参数(分支合并)

merge 之后发生冲突时推荐使用开源工具lazygit来解决冲突。在merge发生冲突时在对应仓库 下终端输入 lazygit 即可,详细用法请参考相应文档。

  • git merge --squash branchName -m "更清爽的提交": 把branchName中特有的所有提交合并为一次待提交状态(其中所有的commit描述信息都会被丢弃,但代码都不会少)。
  • git merge --no-ff branchName -m "merge branchName with --no-ff": 保留branchName中的独有的提交链条,可以清楚地看到合并的来源;一般配合rebase使用会使合并的提交 链条更笔直清爽。比如要把dev分支合并到mater分支,可以:
    • git rebase master
    • git merge –no-ff dev -m “merge dev with –no-ff”
  • git merge [--ff] branchName: git在合并两个没有分叉的分支时的默认行为(特别是在 git rebase branchName 之后使用该合并策略会没有分叉,无法判断这些合并过来的commit来自哪个分支,特别是这个分支被删除后)。 保留分支提交记录,但没有像–no-ff那样的总结性的一次提交记录。

merge 参数效果对比

rebase 分支图解

使用rebase命令的黄金法则就是:不要在公共分支中使用。否则会给其他开发的同学带来很多并且很难解决的冲突。

rebase 分支

merge 和 rebase 的区别图解

初始状态 merge后 rebase后

分支切换或更新交互

  • git branch –set-upstream-to=origin/远程分支名 本地分支名: 将本地分支和远程分支关联
  • git checkout -b 本地分支名 origin/远程分支名: 从远程仓库里拉取一条本地不存在的分支(如果无法识别,则需要先执行 git fetch origin/远程分支名)
  • git checkout -b 分支名 [commitID]:基于当前分支或某个commitID创建一个新分支,并切换到新分支
  • git checkout 分支名:切换分支
  • git pull 远程仓库地址 本地指定分支:远程指定分支:拉取远程指定仓库指定分支到本地指定分支
  • git pull origin 远程分支或者git pull:从远程分支更新本地分支
  • git push 远程仓库地址 本地指定分支:远程指定分支:将本地指定分支推送到远程指定仓库指定分支
  • git push origin 远程分支 或者 git push:从本地分支更新远程分支
  • git branch -d 分支 或强制 git branch -D 本地分支:删除本地分支
  • git push origin :远程分支 或者 git push origin --delete 远程分支名:删除远程分支
  • git branch -r -d origin/branchName:删除远程分支
  • git fetch origin 远程分支名 && git reset --hard origin/远程分支名 && git pull origin 远程分支名:远程强行覆盖本地代码

打补丁

只是想应用某个分支的某些文件的修改,而不是全部,则可以使用打补丁的方式在提示中合并应用部分代码。

合并部分文件

这里假设要把分支 test1_branch 中的 test_file.txt 合并到分支 master 中的相应文件,则可以:

  • git checkout master
  • git checkout --patch test1_branch test_file.txt
  • 然后根据提示一步步操作即可

分支打补丁

正常情况下,对于本地分支,我们可以直接 git merge 就行,然后提交就行。但是,在有些情况下需要代码审核通过之后才能提交,或者 对于某个分支你没有提交权限,此时就需要把补丁文件发送给有权限操作的人去使用这些补丁。

假设你在 fix_branch 分支中 commit 了多次,和 master 分支存在了多个不同的地方,那么,可以通过以下流程对 master 打补丁,以应用 fix_branch 中的修改。

  • git checkout fix_branch
  • git format-patch -M master
  • 然后在终端 ls 一下就可以看到 patch 文件(每次提交会生成一个补丁文件)
  • 把这些补丁文件复制到其他地方或者打包发邮件给打补丁的人

生成好了补丁之后,就可以打补丁了。

  • git checkout master
  • git apply --reject 所有的补丁文件(可以使用通配符)
  • 如果有冲突,就根据生成的 rej 文件手动解决冲突,然后删掉相应的 rej 文件
  • 解决好所有冲突之后,使用 git add 应用修改
  • 最后使用 git am --resolved 告诉 git 打补丁结束

commit 整理

在特定分支中,可以通过相关命令重组或修剪commit链条,包括合并多个commit为一个、删除某次提交的内容等操作。 需要注意的是:

  • 不能在公共分支中进行
  • 在操作之前先创建一个分支作为备份,然后在原来的分支中进行操作
  • 操作完之后要和用于备份的分支进行diff对比,确保结果符合预期

修改 commit 信息

  • git commit --amend -m 'new commit message': 修改最近一次提交的备注信息
  • git rebase -i commitID: 此时会打开编辑器,在想修改的commit信息所在行的行首,把pick改成r,保存退出。 之后会跳出编辑框,然后输入新的commit信息保存,按提示继续操作即可。

整理 commit 节点

  • git rebase -i commitID: 根据编辑框的提示和自身需求对相应的commit所在行的行首词进行修改。
    • p, pick = use commit # 保留这个 commit
    • r, reword = use commit, but edit the commit message # 修改 message message
    • e, edit = use commit, but stop for amending # 停下来,修改内容
    • s, squash = use commit, but meld into previous commit # 合并
    • f, fixup = like “squash”, but discard this commit’s log message # 合并且抛弃 message
    • d, drop = remove commit # 抛弃这个 commit
    • 调整commit的时间先后顺序:只要调换行顺序即可,不建议这样做,因为会导致时间错乱。
  • git reset --soft commitID: 把其中的commit信息都去掉,但文件内容会保留在暂存区。此时你可以:
    • git commit -m “提交信息”: 作为一个提交,并且赋予新的提交信息
    • git restore –stage fileOrFold: 把部分文件放回工作区,分几次提交
  • git reset --hard commitID: 直接重置到该commitID,丢弃最近到commitID的所有提交
  • git revert commitID:撤销某次指定提交,原来的提交记录还在,只是当前代码已经不包含已被车撤销的提交。一般用于公共分支

里程碑或打 tag,更友好的描述信息

对于已经完成一个大的版本或者里程碑,需要定格时,在不想或不便于新建分支的情况下,可以打 tag。

  • git tag -a tagName -m “描述一下”:建立本地标签
  • git push origin tagName:提送到远程仓库(也可以通过 git push origin --tags 把所有的标签都推送)

其他常用的 tag 操作:

  • git tag:列出本地当前分支的所有 tag
  • git tag -l v1.0.\*:通过通配符显示特定的 tag
  • git show tagName:显示标签的详细信息
  • git tag -d tagName:本地删除标签 tagName
  • git push origin :refs/tags/tagName:删除远程 tag(应在删除本地 tag 之后)
  • git push origin --delete tag tagName:删除远程 tag

查看分支列表

  • git branch: 查看本地分支列表
  • git branch --merged:已经合并到当前分支的分支
  • git branch --no-merged:还没有合并到当前分支的分支
  • git branch -a: 查看所有分支列表,包括远程和本地

差异比较

在提交前或合并前最好先知道你到底改了些什么,是否符合预期,这样可以达到二次检查的目的。

差异比较

commit 前比较

提交前确认一下这次到底改了些什么之后再提交会安心很多!

  • git status:显示修改过的文件列表
  • git diff:未 add 时的文件变化情况(命令后可接具体的文件等)
  • git diff --cached/--stage:在 add 之后 commit 之前文件变化情况(命令后可接具体的文件等)
  • git diff HEAD:在 commit 前文件变化情况(前两条命令和合并)

以上命令都可以指定版本、分支或具体文件,以聚焦自己感兴趣的内容

commit 后比较

要想知道最近一次或某次历史提交修改的具体内容,可以使用 git log 或 git diff 等命令。

  • git log:提交的历史记录
  • git log --oneline:简化 log 显示
  • git log --stat:输出 log 的统计信息
  • git log -p:输出修改的具体内容
  • git log --author=作者:指定显示某个作者的提交记录
  • git diff 版本1 版本2:列出相同分支两个版本之间的差异,这个在 rebase 多个提交时可以提供参考
  • git blame 文件:追踪是谁改了哪些代码

以上命令都可以指定版本或具体文件,以聚焦自己感兴趣的内容

不同分支间比较

我们在基于某个分支新建了一个分支作为开发或bug修复,在合并或提交前往往想知道 改了些什么,是否符合预期。

  • git log --left-right dev...master:查看两个分支提交记录的不同处,主要用于合并分支前
  • git diff 分支1 分支2:列出两个分支最近不同的详细信息
  • git diff 分支1...分支2:列出两个分支所有不同的详细信息
  • git diff 分支1 分支2 --stat:列出两个分支的不同点的统计信息

不同仓库间比较

只要两个仓库都是git仓库,两个不同的仓库也是可以比对差异的,常用的是git diff 命令,详细参考前面的章节。 这种场景主要用于某个仓库是由另一个仓库衍生发展而来的情况。

工具

如果你只想使用 git 自带的工具,可以 git diff --word-diff 或者 git diff --color-words。 至于 diff 工具本人推荐使用开源工具 dandavison/delta,其中 .gitconfig 配置如下。事实上,该工具对 git log 也有更友好的显示方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[core]
  pager = delta

[interactive]
    diffFilter = delta --color-only

[delta]
    navigate = true  # use n and N to move between diff sections
    side-by-side = true
    commit-decoration-style = none
    dark = true
    file-added-label = [+]
    file-copied-label = [C]
    file-decoration-style = none
    file-modified-label = [M]
    file-removed-label = [-]
    file-renamed-label = [R]
    file-style = 232 bold 184
    hunk-header-decoration-style = none
    hunk-header-file-style = "#999999"
    hunk-header-line-number-style = bold "#03a4ff"
    hunk-header-style = file line-number syntax
    line-numbers = true
    line-numbers-left-style = black
    line-numbers-minus-style = "#B10036"
    line-numbers-plus-style = "#03a4ff"
    line-numbers-right-style = black
    line-numbers-zero-style = "#999999"
    minus-emph-style = syntax bold "#780000"
    minus-style = syntax "#400000"
    plus-emph-style = syntax bold "#007800"
    plus-style = syntax "#004000"
    whitespace-error-style = "#280050" reverse
    zero-style = syntax
    syntax-theme = Nord

[merge]
    conflictstyle = diff3

[diff]
    colorMoved = default

diff 效果图

查看或搜索提交历史

好的 commit 描述和拆分合并策略有助于 git log 的展示,同时帮助了解代码的时间线。 一般遵循以下原则:

  • 外部库引入或者依赖包更新等单独作为一次提交
  • 自身业务代码,按相对完整的功能进行提交,便于功能增删或回滚
  • bug 修复和性能优化等不同目的和性质的更新要分开提交

log 的各种参数

log 查看工具推荐开源工具vim-flog,常用命令Flog -all

  • 详细的提交信息:git log [-n] [branchName] [fileOrFold1] [fileOrFold2] 其中 n 指的是你感兴趣的 log 条数
  • 查看提交的统计信息:git log --stat ,这包括了更新的文件列表
  • 单行显示:git log --oneline ,这会缩写 commitID
  • ASCII 图形显示:git log --graph ,本人更喜欢用 vim 插件,相对而言更友好。
  • 只显示某个人的提交:git log –anthor=oneName
  • 使用 grep:git log –grep=”要匹配的串”
  • 查看包含特定字符串的提交:git log [-i] [-p] -S"function login()"
  • 根据是否merge来过滤提交历史:git log --merges 或者 git log --no-merges
  • 显示某段时间内的提交:git log --since=2.weeks

后悔药

只要是已经被git跟踪的文件,一般都会有后悔药(即撤销、回滚操作)。

  • add 之前丢弃修改:git checkout -- fileOrFold ,如果想再次找回这些修改,则无能为力了。 因此,最好操作前做好备份。
  • add 之后,commit 之前:git reset HEAD fileOrFold 或者 git restore --staged fileOrFold
  • commit 之后,push 之前:
    • git reset --soft HEAD^ 或者
    • git reset HEAD^ 或者
    • git reset --hard HEAD^ 或者
    • git revert HEAD
    • 它们的区别或者其他方法请参考“整理 commit 节点”一节
  • rebase 过程中撤销本次 rebase:git rebase --abort
  • merge 过程中撤销: git merge --abort
  • rebase 或者 merge 成功之后撤销:
    • git reflog 找到操作前的 commitID
    • git reset --hard commitID
  • 修改了代码,git add 之后发现分支搞错了,必须切换分支,可以:
    • git stash save "note 一下" ,暂时把修改保留到堆栈区
    • git checkout branchName,切换到目的分支
    • git stash apply stash@{0},如果不知道是哪一个,可以用 git stash list 查看列表
  • 代码提交了,才发现搞错了分支(原分支 branch1,目的分支 branch2),可以:
    • git checkout branch2
    • git log branch1 查出 commtiID
    • git cherry-pick commitID 此时提交已经落到该去的分支了
    • git checkout branch1
    • git reset --hard HEAD^ 此时在错误的分支中删掉了不需要的最近提交
  • 不小心把还未push的分支删除了:
    • git log -g 或者 git reflog 找出被误删分支的最近一次提交的 commitID
    • git checkout -b 误删分支名 commitID

git 操作原则

有了这篇文章的帮助,我想你已经对git有了非常全面和深刻的理解,内心开始飘了,有了 随心所欲地冲动。经验告诉我,此时是你冒险犯大错的开始!最疯狂之时,也就是毁灭之时。 为此,我为你准备了以下心法,帮助你克服自大的心理,尽量养成低风险的操作习惯:

  • 在做任何操作之前请做好备份
    • commit 尽量直观,言简意赅
    • 常用 git stash
    • 常建备份分支
    • 常打tag
  • 任意操作之后要做 diff,明确改动
  • 尽量少在公共分支操作,尽量保留合并记录
  • 不要过分依赖“后悔药”
  • 不要手动或用脚本操作 .git 隐藏目录

你不知道的东西远远多于你知道的,谦虚不是客套话,而是客观需要

我要评论

本文由作者按照 CC BY 4.0 进行授权