23

Git基本技能 - 温故知新系列

 3 years ago
source link: https://segmentfault.com/a/1190000022981556
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

​日常开发中用的最多的是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 <[email protected]> 1243040974 -0700
    committer Scott Chacon <[email protected]> 1243040974 -0700

    first commit

可以看到,git对文件的管理是通过记录的文件hash,然后通过hash查找对应文件内容的方式。

最后看一张git仓库的原理图:

FrIBbui.jpg!web

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

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

iu22iaZ.jpg!web

由于每个分支 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} //将记录列表中取出的对应暂存记录删除

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

BjeqQzZ.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK