乐趣区

Git进阶-温故知新系列

​日常开发中用的最多的是 git add、git commit、git pull、git fetch、git push 等,不过当出现一些稍复杂一点的场景,如果具备相应的 git 知识储备,就很有可能脱颖而出。本文首先阐述 git 的一些基本原理,然后对开发中比较常遇到的合并和撤销等场景做讲解,避免死记硬背。

前言

版本控制工具采用的存储方式:

  1. 存储差异:存储 base 文件,之后的版本存储 base 文件的更改,SVN
  2. 存储快照:每次更改都存储一个新文件,Git(类似于 immutable 的原理?)

内部原理

先来认识下 .git 目录下几个比较重要的文件:

config:仓库的配置文件
HEAD:当前所在的位置,指向具体分支
refs/:存储的是引用文件,如本地分支,远端分支,标签等(其内容为 40 位 hash,指向 commit 节点)objects/:数据文件的存储目录
hooks/: 钩子目录,在特定的重要动作发生时触发自定义脚本 

这里重点介绍下 objects,仓库中每个文件都有对应的 hash 值,包括 3 种数据类型:文件(blob)、文件夹(tree)、提交(commit),其内容分别如下:

1. blob 文件:具体文本
2. tree 文件:100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
3. commit 文件:tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
    author Scott Chacon <schacon@gmail.com> 1243040974 -0700
    committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

    first commit

可以看到,git 对文件的管理是通过记录的文件 hash,然后通过 hash 查找对应文件内容的方式。
最后看一张 git 仓库的原理图:

[注] HEAD:指向当前所在的分支;ref/head/:指向当前的 commit 版本;ci:commit 节点,包含当前版本的文件引用

理解 commit 节点间的引用关系,对理解分支的新建、切换、合并、撤销等至关重要。

由于每个分支 ref 指向的是 commit 节点,而 commit 通过 parent 指针指向前一次提交节点,实现链式提交记录的回溯

总结:git 操作时,关键还是指针的移动,这也是为什么 git 又名 – 内容寻址系统

理解了上面的基本知识,下面对合并和撤销操作做讲解就会简单很多。

合并

部分提交

// 合并某个提交
git cherry-pick <commitHash>
// 合并多个提交
git cherry-pick <HashA> <HashB>
// 合并连续提交 (不含 A)
git cherry-pick A..B
// 合并连续提交 (含 A)
git cherry-pick A^..B

代码冲突时:

1. 解决冲突
// 解决冲突后 git add .
git cherry-pick --continue
2. 退出合并
git cherry-pick --abort

全部提交

merge
git merge [待合并的分支名]
# 3 种模式
-fast-forward:默认方式,如果待合并的分支在当前分支的下游,即也就是说没有分叉时,会发生快速合并
–no-ff:在当前分支上新建一个提交节点,从而完成合并
–squash:和 no-ff 非常类似,区别只有一点不会保留对合入分支的引用 

代码冲突时:

1. 解决冲突
// 解决冲突后 git add .
git commit -m 'xx'
2. 退出合并
git merge --abort
rebase
git rebase [作为合并基准的分支名]
// 一般使用:比如 feature 要合并到 master
1)先在 feature 分支 rebase master,即将 feature 的提交应用到 master 节点后
2)再 checkout 到 master 使用 merge 移动 master 指针到最新节点 (fast-forward)

代码冲突时:

1. 解决冲突
// 解决冲突后 git add .
git rebase --continue
2. 退出合并
git rebase --abort
两者的区别

假定当前分支情况如下

   {master}
      |
A--B--C
   |
   D--E
      |
  {feature}

merge
1)合并待合并分支的提交信息后,生成一个新的提交节点
2)保留分支的提交合并记录,便于掌握历史操作记录

// 在 master 分支使用 merge
feature> git merge master

      {master}
         |
A--B--C--F
   |    /
   D--E
      |
  {feature}

rebase
1) 指定目标分支作为基准点,将当前分支的节点依次 patch 到目标分支最新提交后
注意这里仅改变了待合并分支的指针位置,需要 checkout 到目标分支使用 merge,则会直接将目标分支指针移动到最新提交节点
2) 不会删除或复用待合并的提交,而是依次创建新的提交应用到目标分支
3) 不会保留合并记录,使得提交记录线性化

// 在 feature 分支使用 rebase
feature> git rebase master

  {master} {feature}
      |      |
A--B--C--D'--E'
   |
   D--E

至于在开发中是何时使用 merge,何时使用 rebase,这个见仁见智。(个人觉得主分支保持清爽很重要,但是特殊情况下撤销 merge 的分支合并更方便)

撤销

工作区内容撤销

// 使用暂存区内容覆盖工作区
git checkout -- fileName

暂存区内容撤销

reset

主要用途: 回退版本内容
一般格式

git reset <option> < 版本 >

常用 option

--hard: // 使用指定的提交版本覆盖工作区和暂存区。即删除指定版本后的所有提交内容
--soft:// 将指定提交之后的内容回退到暂存区,工作区不变动
--mixed: // 将指定提交之后的 commit 以及暂存区的内容退回到工作区未保存状态 

对于本地仓库,都是指针的变更;文件或要舍弃(hard),或要合并提交(soft),或要修改(mixed

举例
git reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区

// 把所有暂存区中的修改回退到工作区未保存状态 git reset HEAD   // 默认为 --mixedgit rest HEAD file

回退版本的指定

HEAD^: 上一个版本 HEAD^^:上上一个版本...HEAD~n: 比如回退到倒数第 3 个版本,HEAD~3

回退版本后,想要提交到远程仓库,则需要使用 -f 强推:

push -f -u origin master

如果回退版本后,想要回到撤销前的版本,则可以使用 git reflog 查看对应的 commitId。git reflog 会列出所有历史提交记录,和 git log 的差异在于包含删除的提交记录

git reset --hard HEAD@{N}
revert

主要用途:撤销某个提交操作

1) 撤销某个普通 commit

对指定的 commit 进行撤销操作,生成一个新的提交记录

git revert <commitId>

2) 撤销某个 merge commit

先来看合并两个分支后生成的 commit 节点信息

git show cs38864
commit bd868465569400a6b9408050643e5949e8f2b8f5
// 有两个 parant commit,指明当前 commit 节点由哪两个 commit 合并
Merge: ba25a9d 1c7036f

执行撤销时,需要指定主线分支,将会撤销另一分支内容

/**   
 *- m 表示是一个 merge 节点  
 *parent-number:1 | 2 表示保留哪个 parent 节点,顺序为上例中 Merge 中的 commit
 **/
 git revert -m parent-number <commitId>

对于撤销的合并分支上的提交,后续再合并,撤销的节点也不会被合并;想要将撤销的分支提交重新合并,需对 revert 生成的 commit 进行一次撤销。

两者的区别
1.revert:不会删除提交记录,保留每一步操作记录,更安全  
  reset:不包含已删除的合并提交    
2.revert 因为是对指定提交进行取反操作,创建一个新的提交,因此无需强推到远程仓库    
3. 建议:本地 reset,公共分支 revert    
4. 合并后如果做了操作,reset 到合并节点前,会删除之后的提交信息;可以使用 reset 达到 revert 的效果,如下:// 回退提交内容到工作区  
  git reset devb-3  
  // 将工作区的内容暂存 stash  
  git stash  
  // 使用 hard 改变(删除)指针 commit  
  git reset --hard devb-2  
  // 恢复 stash 的内容到工作区  
  git stash pop  
  // 提交  
  git add & commit & push

拾遗

1. 对上一次提交进行修改 (打补丁),会创建一个新的 commitId

git commit --amend
// 修改提交信息
git commit --amend -m 'xxx'
// 代码逻辑的修补
git commit --amend –-no-edit

2. 合并多个提交记录
方式 1

git reset --soft commitId  // 将制定 commit 后的提交信息回退到暂存区
git commit -m '合并多个 commit'

方式 2

# (start-commit, end-commit] 前开后闭区间,默认 end-commit 为当前 HEAD
git rebase -i [start-commit] [end-commit]

3. 查看不同阶段代码的差异

git diff: 查看工作区和暂存区的差异
git diff --cached HEAD:暂存区和当前仓库指针的差异 

4.git stash

将工作区和暂存区的内容存储起来;默认状态,git stash 命令会将工作区和暂存区内容重置为最近一次提交后的内容,并且只能将已经跟踪和非.gitignore 忽略的文件储藏,未跟踪的文件不会被存储。如果想要将未跟踪的文件一并存储,使用 -u--include-untracked

git stash push -u

The latest stash you created is stored in refs/stash; older stashes are found in the reflog of this reference and can be named using the usual reflog syntax (e.g. stash@{0} is the most recently created stash, stash@{1} is the one before it, stash@{2.hours.ago} is also possible). Stashes may also be referenced by specifying just the stash index (e.g. the integer n is equivalent to stash@{n}).

git stash pop // 取出栈顶的暂存信息
git stash list // 查看暂存区的所有暂存修改
git stash apply stash@{X} // 取出相应的暂存;不会向 pop 一样从栈中移除 stash
git stash drop stash@{X} // 将记录列表中取出的对应暂存记录删除 

获取更多干货分享,欢迎【扫码关注】~

参考:
https://dotblogs.com.tw/wasic…
https://www.liaoxuefeng.com/w…
https://zhuanlan.zhihu.com/p/…
https://yanhaijing.com/git/20…

退出移动版