Git 学习与实战总结

342 阅读18分钟

Git常用,但是自己没有深入研究下,每次提交时,总担心自己会操作失误,趁着五一假期,找了基本Git 的书,也粗略看了几个Git课程,都无感,直到找到progit [git-scm.com/book/zh/v2/ 或者 www.progit.cn/#_pro_git]这本书,挺棒,满足我的诉求,从中选择几章自己常用且感兴趣的,做了梳理与总结。

1 基础知识

1.1 三种文件状态:

已提交:表示数据已经安全地保存在本地数据库中。

已修改:修改了文件,还没有保存到本地数据库中。

已暂存:对一个已修改文件的当前版本做了标记,是指保存在下次提交的快照中。

1.2 三个工作区:

  • Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。
  • 工作目录是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
  • 暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作`‘索引’',不过一般说法还是叫暂存区域。

1.3 基本的git工作流程

(1)在工作目录中修改文件。

(2)暂存文件,将文件的快照放入暂存区域。

(3)提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

2 Git 基础操作

2.1 新建文件和更新文件的流程。

在工作目录下的每一个文件,只有两种状态:已跟踪、未跟踪。

status变化:

(1)没有任何改动

(2)修改了README.md 文件,新增了test2 文件,再看下status。

从图中可以看到,对于修改过的文件,可以"git add "", 也可以"git checkout -- " 来撤退。 新增的文件是untracked ,是未被git跟踪的意思。

被修改的文件是 Changes not staged for commit 状态,说明已跟踪文件的内容发生了变化,但还没有放到暂存区。 要暂存这次更新,需要运行 git add 命令。

只要在 Changes to be committed 这行下面的,就说明是已暂存状态。

(3)add 一下 修改的文件 git add README.md, 再看下status 。

修改已经commit, 此时可以通过git reset HEAD <file> 撤回。

(4) 再add 一下 添加的文件: git add test2, 再看下status。

文件已经添加了,当然,同样可以使用 git reset HEAD 回滚。 此时已经是 Changes to be committed 状态了,说明已经被暂存了。

(5) 执行下 git commit -m "second commit":

(6) push 一下, 再status:

已经恢复到最初的状态了。

下面是一个文件状态的变更流程:

2.2 更新之后add, commit 之前再更新。

操作流程:

我们发现,commit 之后,第二次更新,此时会既有暂存区,又有未被暂存的。如果此时commit ,则只能保存第一次add的内容。那我们看下如果push之后,到底上传了什么。

第二次更新的内容:

那我们看下远程的内容:

发现,我们第二次添加的内容并没有被提交到远程。

如果想生效,必须把第二次更新的内容,也add到暂存区,然后commit .

此时远程就有我们的内容了。

Git 只不过暂存了你运行 git add 命令时的版本, 如果你现在提交,CONTRIBUTING.md 的版本是你最后一次运行 git add 命令时的那个版本,而不是你运行 git commit 时,在工作目录中的当前版本。 所以,运行了 git add 之后又作了修订的文件,需要重新运行 git add 把最新版本重新暂存起来:

2.3 git diff

git diff : 修改之后还没有暂存起来的变化内容。

git diff --cached : 查看已暂存的将要添加到下次提交里的内容。

2.4 commit

注意:提交时记录的是放在暂存区域的快照。 任何还未暂存的仍然保持已修改状态!!!

2.5 git rm

2.5.1 git rm

rm 只是删除本地文件,而从暂存区域移除 需要使用git rm。 来个例子:

只是本地文件删除了,但是暂存区还有。下面我们试下git rm (会同时删除本地跟暂存区的内容)

已经提交到暂存区了。先commit 然后 push 一下:

看下远程的变化,message 是我们刚刚提交的信息,并且test2 文件已经消失了:

2.5.2 git rm --cached

只删除暂存区的内容,但是本地工作区的内容保留。

来个例子:

2.6 文件移动

git mv 文件在本地工作区跟暂存区进行了重命名,其实底层相当于 执行了如下三条指令:

mv test3 test4
git rm test3
git add test4

看下远程变化:

2.7 提交历史

git log -2 --since="2020-04-30 15:00:00" --before="2020-04-30 16:00:00" <file>

其中: -2: 是指显式2条; --since= : 表示从什么时间开始,可以具体到秒; --before= : 是指截止到某个时间,具体到秒; : 是指具体的文件,如果没写,则表示当前项目的所有文件。

2.8 撤消操作

2.8.1 撤销commit

git commit --amend

第一次commit之后,未push, 如果想继续修改一些文件,则可以git add 要修改的文件,然后执行git commit --amend ,修改第一次的commit, 这样最后push的时候,使用的是第二次commit的内容。

举个例子:

看下远程提交信息:

2.8.2 撤销暂存区的内容

git reset ,下面来个例子。 先存入两个文件到暂存区:

注意看,git 已经提示我们git reset 可以将文件从暂存区撤销。那我们试一下:

2.8.3 撤销对某个文件的修改

git checkout -- 注意,会全部撤销所有的修改点,很危险.

git 中任何已提交的东西几乎总是可以恢复的。甚至那些被删除的分支中的提交都可以恢复。然后,未提交的东西丢失后很可能再也找不回来了。

2.9 打标签

给历史中某一个提交打上标签,以示重要。

2.9.1 列出标签

git tag 以字母顺序列出标签,出现的顺序不重要。 git tag -l 'v.1*' 使用特定模式查找标签。

2.9.2 创建标签

分为 附注标签 跟轻量标签。

(1)附注标签: 存储在Git数据库中的一个完整对象。

git tag -a v1.4 -m 'my version 1.4'

-m:指定了一条将会存储在标签中的信息。

可以使用git show 来展示标签信息。

输出显示了打标签者的信息、时间、附注信息,然后显示具体的内容。

(2)轻量标签: 像一个不会改变的分支,只是一个特定提交的引用。

git tag v1.1-lw

然后git show :

2.9.3 后期打标签

这个功能牛逼~~~ 一通操作。

2.9.4 共享标签

推送某个tag到远端: git push origin v1.0

推送所有tag到远端: git push origin --tags

2.9.5 检出标签

不好使,不建议用了。

2.9.10 git 别名

$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

3 Git分支

3.1 分支简介

文件变更之后,会生成一个内容快照。

在进行提交操作时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,

上述过程,类似下图:

其中根目录hash 就是tree 对象。 文件hash 就是blob对象。 在 这篇文章Git之旅(6)从概念到实战中 讲的很棒。

git 的分支,本质上仅仅是指向提交对象的可变指针。

3.2 分支创建与合并

3.2.1 分支创建

使用git branch xxx ,会在当前的提交对象上创建一个分支。此时并没有切换到新分支。

git 里面有个head 指针, 它指向当前所在的分支。

使用git log --decorate 可以查看。

3.2.2 分支合并

如下是分支合并可能会用到的三种策略:

  • Fast-forward

A分支是从B分支上拆分出来的,则改完A,向B合并时,就直接快进A的指针。 看个例子:

  • Merge made by the 'recursive' strategy.

A跟B没有冲突的文件,并且A跟B都有新的改动,那么合并时,git会 利用A/B两个分支的指针,以及末尾指针指向的两个节点的共同祖先,这三个节点进行合并。就不需要用户自己关心了。

  • 遇到冲突时的分支合并

A并不是从B分支拆分出来的,并且A跟B对同一个文件的同一个部分进行了不同的修改。那么合并时,就会产生冲突,需要收到改冲突点。改完之后,git add 所有文件,就相当于告诉git ,冲突已经解决了。然后正常commit跟push即可。

例子:

下面我们把b4-1的内容合并到b4:

冲突文件内容:

3.3 分支开发工作流

3.3.1 长期分支

一般是一个稳定的分支,比如master分支。

3.3.2 特性分支

一种短期分支,被用来实现单一特性或其相关的工作。

3.3.3 远程分支

网络上的分支。

3.3.4 推送 push

当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。

3.3.5 跟踪分支 pull

当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支。 然而,如果你愿意的话可以设置其他的跟踪分支 - 其他远程仓库上的跟踪分支,或者不跟踪 master 分支。 最简单的就是之前看到的例子,运行 git checkout -b [branch] [remotename]/[branch]。

git pull 相当于 git fetch + git merge 。

下面例子是展示账户U_A在分支b4上添加了一个文件b4-2, 并提交到远端。而U_B账户 也在分支b4上本地增加了文件b4-2 ,而且跟U_A 修改的内容不同,那么当U_B git pull b4分支时,会报错。

而git fetch 只会拉取本地没有的数据,并不会修改工作目录中的内容。

3.3.6 删除远程分支

git push origin --delete b4

注意: 这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

3.4 变基 rebase.

为了确保在想远程分支推送时,能保持提交历史的整洁。另外,也会用于给开源项目提交patch,方便维护人合并代码。

如下例子:

3.4.1 merge

使用merge,应该是这样的:

3.4.2 rebase

也可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做 变基。 你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。

然后回到master 分支,进行一次快速合并:

$ git checkout master
$ git merge experiment

例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

3.4.3 变基的一篇不错文章,及操作举例。

这儿还有一篇不错的rebase 的文章: jartto.wang/2018/12/11/… 。 我们来实战一下文章中提到的两个应用场景:

(1)修改多次提交的操作举例:

(2) rebase 其他分支的举例: 我们从"rebase" 这个分支上,新拉一个分支rebase2,并提交两次。

然后,切回到"rebase" 分支,提交两次,并push到远端:

最后,再次切到"rebase2" 分支,然后执行git rebase rebase (第二个rebase 是分支名,论一个好名字的重要性。。。)

我们看到,rebase2 分支的提交历史已经变了。

3.4.4 变基的风险

不要对在你的仓库外有副本的分支执行变基。 简单点就是 只对尚未推送或分享给别人的本地修改执行变基操作清理历史,千万不要对已经推送到别处的提交执行变基操作。

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。

4 分布式git

4.1 集中式工作流

只需要搭建好一个中心仓库,并给开发团队中的每个人推送数据的权限,就可以开展工作了。Git 不会让用户覆盖彼此的修改。 例如 John 和 Jessica 同时开始工作。 John 完成了他的修改并推送到服务器。 接着 Jessica 尝试提交她自己的修改,却遭到服务器拒绝。 她被告知她的修改正通过非快进式(non-fast-forward)的方式推送,只有将数据抓取下来并且合并后方能推送。

4.2 集成管理者工作流

  • 项目维护者推送到主仓库。
  • 贡献者克隆此仓库,做出修改。
  • 贡献者将数据推送到自己的公开仓库。
  • 贡献者给维护者发送邮件,请求拉取自己的更新。
  • 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。
  • 维护者将合并后的修改推送到主仓库。

4.3 司令官与副官工作流

不常用,不整理了。

5 git 工具

5.1 暂存 stash

git stash 是将当前的变动,存放在一个栈上,可以通过git stash list 查看有哪些stash. 这个栈上的内容并不归属于某个特定的分支,并不是说在A分支上git stash 就只能在A分支上git stash pop, 也可以在B分支上pop 出来。 看个例子:

在master上stash 的内容切换到b3 分支上,直接pop出来,并提交。

然后再回到master 分支,再执行git stash list, 发现之前stash 的内容已经不存在了。

当然,可以有多个stash, 在使用时,直接pop特定的stash内容即可。这种不常用,我们就不整理了。

5.2 搜索

5.2.1 git grep 搜索源码
5.2.2 git log 查看历史
git log -L '/开始字符串/',/结束字符串/:文件名

比如 :
git log -L '/unsigned long git_deflate_bound/',/^}/:zlib.c

来个例子: 会展示内容字符串相关的commit id, commit message, 以及具体的内容。

5.3 重写历史

5.3.1 修改最后一次提交

git commit --amend

不只是修改上一次提交信息,也可以继续修改文件,新增文件。执行git commit --amend 之后,会替换掉上一次的commit id ,产生新的commit id. 使用这个技巧的时候需要小心,因为修正会改变提交的 SHA-1 校验和。 它类似于一个小的变基 - 如果已经推送了最后一次提交就不要修正它。

backlog.com/git-tutoria….

看个例子:

amend 之后的弹窗信息:要求我们去修改第一次commit的内容。

5.3.2 修改多次提交

如果想要改多次提交,可以使用git rebase。 注意: 前提是只能是未提交的内容。如果已经提交了,一旦被别人用了提交内容,就产生混乱了。

如果尝试修改最近三次的历史,则使用 git rebase -i HEAD~3。

5.4 重置揭密

5.4.1 三棵树(文件的集合)

(1) HEAD(提交历史): 上一次提交的快照,下一次提交的父结点

(2) Index(暂存区): 预期的下一次提交的快照

(3) Working Directory(工作目录): 沙盒

5.4.2 工作流程

5.4.3 重置的作用

必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。

整个reset 的操作过程: reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:

  • 移动 HEAD 分支的指向 (若指定了 --soft,则到此停止)
  • 使索引看起来像 HEAD (若未指定 --hard,则到此停止)
  • 使工作目录看起来像索引

其中,原来从工作区--> 暂存区 --> head。 而reset 则是一个逆向的过程。如果只指定soft,则head 回退一个指针,而index跟工作区不用更改; 如果不指定(此时默认是mixed), 则head 回退一个指针,而且会将这个变动同步至 暂存区; 如果指定了hard, 则head 回退一个指针,而且同步至 暂存区跟工作目录。

上图中,本来的提交commitid 记录是 1<-2<-3<-4。 最新的commitId是4.

reset 参数 head区 暂存区 工作目录
--soft 3 4 4
--mixed(不填) 3 3 4
--hard 3 3 3

git reset --soft origin/master : 将本地的head区回退到跟远程分支origin/master 一样的状态,但是工作目录跟index 还是保留更改信息。

git reset --hard origin/master: 将本地的head 区、index区、工作目录 都回退到跟远程分支origin/master 一样的状态。本地的更新也丢了,慎重!!!

如果执行了基于远程分支的reset, 则意味着head 回退到 远程分支的commitID 上。可以参考这个连接:What is the meaning of git reset --hard origin/master?

5.4.4 通过路径来重置

可以通过 git reset commitID fileName 来直接回退到某个commitID.

5.4.5 reset 与 revert

revert 不常用。不同于reset的回退head, revert 是指针向前移动一个,但是内容是合并之前的内容。如下图。由于不常用,我们就不举例子了。

6 Git 内部原理

6.1 底层命令和高层命令

底层命令:对用户不太友好的命令 高层命令:对用户有好的命令,这个定义。。。只能说是历史原因吧。

项目下面 .git 文件夹中的目录结构:

  • objects 目录存储所有数据内容;
  • refs 目录存储指向数据(分支)的提交对象的指针;
  • HEAD 文件指示目前被检出的分支;
  • index 文件保存暂存区信息。

6.2 Git 对象

6.2.1 数据对象: (只有内容)

文件的一次commit 产生的对象。

Git 的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。

6.2.2 树对象 (解决文件名保存的问题,也允许我们将多个文件组织到一起)

它能解决文件名保存的问题,也允许我们将多个文件组织到一起。 树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

注意:tree对象是在执行git commit 指令时才生成的。就像blob 是在git add 时生成的一样。

6.2.3 提交对象(就是commit之后的对象)

每次我们运行 git add 和 git commit 命令时, Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 \

数据对象、树对象、提交对象,分别位于 工作目录、暂存区、head区 ???

是的,基本能对上。

6.2.4 对象存储

一个改动的文件,先被算出一个commit ID,然后压缩,最后将压缩内容放在一个目录中。

6.3 Git 引用

就是指针。指向某个commit 对象的指针。 有head 引用,每个分支都有一个head 指针, 并且tag 也是一个指向某个commit 对象的永久指针。

6.4 维护与数据恢复

6.4.1 reflog 恢复

可以查看所有分支的所有操作记录(包括已经被删除的 commit 记录和 reset 的操作)。 wjp2013.github.io/tool/git-re… www.findme.wang/share/detai…

reflog 的应用场景: 一般是因为发现reset错误了,此时使用git log 是找不到之前的commitid 的,所以可以使用reflog。

7 参考

1、progit 有选择的看

2、底层原理深入

3、从原理视角看Git

4、rebase的讲解