美文网首页
Git原理浅析及实用技巧

Git原理浅析及实用技巧

作者: 弘法大师 | 来源:发表于2024-01-02 20:14 被阅读0次

    前言

    Git是一个开源的分布式版本控制系统,可以快速高效地进行项目版本管理,作为日常使用中的基础工具,大家都不陌生。更好地使用需要更多的了解,我们结合着Git的实现原理去学习回顾Git命令,文末会附上常用的git命令。

    了解Git

    Git存储结构

    当我们执行命令git init时,会在当前目录下生成一个隐藏文件夹 .git,Git会将整个数据库储存在

    .git/目录下。

    执行如下命令

    ➜ git init
    ➜ echo "1" > a.txt
    ➜ echo "2" > b.txt
    ➜ git add a.txt
    ➜ git add b.txt
    

    每次执行git add .git/objects目录下都会新增一个文件。

    ➜ tree .git/objects
    .git/objects
    ├── 0c
    │   └── fbf08886fca9a91cb753ec8734c84fcbe52c9f
    ├── d0
    │   └── 0491fd7e5bb6fa28c517a0bb32b8b506539d4d
    ├── info
    └── pack
    

    新增的这两个文件是git将数据压缩成的一个二进制文件, git cat-file [-t] [-p], -t可以查看object的类型,-p可以查看object储存的具体内容。

    ➜ git cat-file -t 0cfb
    blob
    ➜ git cat-file -p 0cfb
    2
    

    这里我们看到object的类型是blob,blob类型,它只储存的是一个文件的内容,不包括文件名等其他信息,git将这些信息经过SHA1哈希算法得到对应的哈希值。

    执行git commit会新增两个文件

    ➜ git commit -m "init git"
    ➜ tree .git/objects       
    .git/objects
    ├── 99
    │   └── 59835d90e1f2a652da437586f6e867581f8a1d
    ├── 9c
    │   └── e769527cef798b85b8c0b841f2cfdb757b9794
    

    通过git cat-file -t会发现是tree和commit两种类型。

    tree,将当前的目录结构打了一个快照。从储存的内容来看可以发现其储存了一个目录结构(类似于文件夹),以及每一个文件(或者子文件夹)的权限、类型、对应的身份证(SHA1值)、以及文件名。

    ➜ git cat-file -p 9ce7
    100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d    a.txt
    100644 blob 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f    b.txt
    

    commit,储存的是一个提交的信息,包括对应目录结构的快照tree的哈希值,上一个提交的哈希值(这里由于是第一个提交,所以没有父节点。在一个merge提交中还会出现多个父节点),提交的作者以及提交的具体时间,最后是此次提交的信息。

    ➜ git cat-file -p 9959
    tree 9ce769527cef798b85b8c0b841f2cfdb757b9794
    author sunhongfa <sunhongfa@kuaishou.com> 1633960408 +0800
    committer sunhongfa <sunhongfa@kuaishou.com> 1633960408 +0800
    

    Git的主要的存储结构就是这三个object,图示如下:

    image.png

    除了blob 、tree 、commit外还有一个tag的object,git tag -a时会创建

    以上默认是在master分支,分支的信息存储在.git/HEAD下

    ➜ cat .git/HEAD
    ref: refs/heads/master
    ➜ cat .git/refs/heads/master
    9959835d90e1f2a652da437586f6e867581f8a1d
    

    在Git仓库里面,HEAD、分支、普通的Tag可以简单的理解成是一个指针,指向对应commit的SHA1值。

    图示如下:


    image.png

    至此我们知道了Git是怎样储存一个文件的内容、目录结构、commit信息和分支的。其本质上是一个key-value的数据库加上默克尔树形成的有向无环图(DAG)

    默克尔树
    最下面的叶节点包含存储数据或其哈希值。
    非叶子节点(包括中间节点和根节点)都是它的两个孩子节点内容的哈希值。

    简单总结

    Git 在原有文件版本的基础上重新生成一份新的文件快照,类似于备份。为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。

    • 缺点:占用磁盘空间较大
    • 优点:版本切换时非常快,因为每个版本都是完整的文件快照,切换版本时直接恢复目标版本的快照即可
    • 特点:空间换时间

    Git三个分区

    image.png

    Git有三个分区:工作目录、暂存目录(也叫做索引)和仓库

    • 工作目录 ( working directory ):操作系统上的文件,所有代码开发编辑都在这上面完成。
    • 索引( index or staging area ):可以理解为一个暂存区域,这里面的代码会在下一次commit被提交到Git仓库。
    • Git仓库( git repository ):由Git Object记录着每一次提交的快照,以及链式结构记录的提交变更历史。
    image.png

    上面的四条命令在工作目录、暂存目录(也叫做索引)和仓库之间复制文件。

    • git add files 在仓库里面新建了一个blob object,储存了新的文件内容,并且更新了索引指向了新建的blob object
    • git commit 给暂存区域生成快照(tree&commit)并提交,parent指向上一个commit,组成一条链记录变更历史,并将对应分支的指针移动到新的节点。
    • git reset -- files 用来撤销最后一次git add files,你也可以用git reset 撤销所有暂存区域文件。
    • git checkout -- files 把文件从暂存区域复制到工作目录,用来丢弃本地修改。
    image.png

    Git常用命令分析

    前文介绍了Git的基本工作原理,接下来会分析一些常用的Git命令。

    commit

    # 更改一次提交
    git commit --amend -m "<new commit message>" 
    

    git会使用与当前提交相同的父节点进行一次新提交,旧的提交会被取消。

    image.png

    注意:如果已经push到远程的commit,无法更改

    checkout

    # 创建分支
    git checkout -b branch
    # 切换分支
    git checkout stable
    

    HEAD标识会移动到那个分支(也就是说,我们“切换”到那个分支了),然后暂存区域和工作目录中的内容会和HEAD对应的提交节点一致。新提交节点中的所有文件都会被复制(到暂存区域和工作目录中);只存在于老的提交节点中的文件会被删除;不属于上述两者的文件会被忽略,不受影响。

    reset

    reset命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引,也可用来在从历史仓库中复制文件到索引,而不动工作目录。

    # 分支指向不变,但是索引会回滚到最后一次提交,并更新工作目录
    git reset --hard
    
    # 当前分支指向前三次递交
    git reset HEAD~3
    
    # 当前分支指向前三次递交,并更新工作目录
    git reset HEAD~3 --hard
    
    image.png

    git reset --hard 因为会更新工作目录,会直接将文件删除掉,所以需慎用,没有后悔药可以吃
    git clean同样慎用,git clean -df 会删除当前目录下没有被track(没有git add)的文件或文件夹

    merge

    merge 命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。 另一种情况是如果当前提交是另一个分支的祖父节点,就导致fast-forward合并。指向只是简单的移动,并生成一个新的提交。


    image.png

    否则就是一次真正的合并。默认把当前提交(ed489 如下所示)和另一个提交(33104)以及他们的共同祖父节点(b325c)进行一次三方合并。结果是先保存当前目录和索引,然后和父节点33104一起做一次新提交。

    image.png

    git merge --ff

    Git 合并两个分支时,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,叫做“快进”(fast-forward)不过这种情况如果删除分支,则会丢失merge分支信息。

    git merge –squash

    把一些不必要commit进行压缩,比如说,你的feature在开发的时候写的commit很乱,那么我们合并的时候不希望把这些历史commit带过来,于是使用–squash进行合并,此时文件已经同合并后一样了,但不移动HEAD,不提交。需要进行一次额外的commit来“总结”一下,然后完成最终的合并。

    git merge –no-ff

    关闭fast-forward模式,在提交的时候,会创建一个merge的commit信息,然后合并的和master分支

    merge其实最终都会将代码合并到master分支,而区别仅仅只是分支上的简洁清晰的问题,使用git merge --no-ff时,再使用reset 的时候,就会发现,会直接回到merge前的commit,防止搅乱commit历史

    如果merge过程不顺利,可以dry run 放弃这次merge,执行git merge --abort

    cherry pick

    git cherry-pick 1ab62
    

    常用又好用的一个命令,cherry-pick命令"复制"一个提交节点并在当前分支做一次完全一样的新提交。

    rebase

    衍合是合并命令的另一种选择。合并把两个父分支合并进行一次提交,提交历史不是线性的。衍合在当前分支上重演另一个分支的历史,提交历史是线性的。 本质上,这是线性化的自动的 cherry-pick

    image.png

    上面的命令都在topic分支中进行,而不是main分支,在main分支上重演,并且把分支指向新的节点。注意旧提交没有被引用,将被回收。

    git rebase期间可能会有多次解决冲突的操作

    # resove conflicts
    git add .
    git rebase --continue
    # dry-run 放弃这次rebase
    git rebase --abort
    

    git rebase合并commit

    # 合并提交,从HEAD版本开始往过去number个版本
    # 提交的范围
    git rebase -i/--interactive HEAD~<number of commits>
    # 该hash之后的所有提交
    git rebase -i/--interactive <commit hash>
    
    # 执行完之后会弹出vim
    # 将要合并的commit的pick改为squash或者s,之后保存并关闭文本编辑窗口
    git add .
    git rebase --continue
    

    在配置的编辑器中倒序列出所有的 commit,像这样:

    # <command> <commit hash> <commit message>
    pick 5df8fbc commit message
    pick ca5154e commit message
    pick a104aff commit message
    
    # p, pick   = 使用提交而不更改
    # r, reword = 修改提交信息
    # e, edit   = 编辑提交
    # s, squash = 汇合提交
    # f, fixup  = 类似"squash",但是会丢弃提交信息
    # x, exec   = 运行命令 (其余行)
    # d, drop   = 移除提交
    

    保存编辑器后,git 将运行该方案以重写历史记录。

    e, edit会暂停 rebase,就可以编辑代码仓库的当前状态。完成编辑后,运行

    git add .
    git rebase --continue。
    
    # 如果过程中出现问题(例如合并冲突),我们需要重新开始,可以使用
    git rebase --abort
    

    git pull --rebase也是一种rebase
    rebase的好处是commit log更清晰直观,缺点是本地的分叉提交已经被修改过了,merge会有分叉

    rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

    git实用技巧

    配置

    # 全局设置
    git config --global <keypath> <value>
    # 本地设置
    git config <keypath> <value>
    
    # 显示当前设置及其来源
    git config --list --show-origin
    
    # 设定身份
    git config --global user.name "<your name>"
    git config --global user.email <your email>
    # 首选编辑器
    git config --global core.editor vim
    

    TBD

    基于主干的开发(TBD)通常是最好的分支模型,最适合连续交付工作流。

    git checkout origin/release # 切换到release分支
    git checkout -b feat_branch  # 创建你自己的分支
    git branch -u origin/release # 追溯origin/release
    

    注意使用kdev,如果有多个CR要核准,需要另外创建一个分支去同步开发,比如一个本地分支下的有两个CR,一个核准一个未核准,无法 kdev land

    切换远程分支

    # To update the remote url in your local repository run (for ssh):
    git remote set-url origin git@git.corp.kuaishou.com:exploration-client/overseaads.git
    
    # or for http(s):
    git remote set-url origin https://git.corp.kuaishou.com/exploration-client/overseaads.git
    

    查找Commits或更改

    # 通过commit信息查找(所有分支)
    git log --all --grep='<search term>'
    # 通过commit信息查找(包含reflog)
    git log -g --grep='<search term>'
    # 通过更新的内容查找
    git log -S '<search term>'
    # 通过日期范围查找
    git log --after='DEC 15 2019' --until='JAN 10 2020'
    

    stash

    stash 将当前的更改临时搁置起来。可以返回当前状态的索引,并能在稍后应用已储藏的更改。

    # 创建新的STASH
    git stash
    # 创建新的STASH (包含未追踪的更改)
    git stash -u/--include-untracked
    # 创建新的STASH并命名
    git stash save "<stash name>"
    
    # 列出所有的STASH
    git stash list
    
    # 浏览STASH内容
    git stash show
    # 浏览STASH差异
    git stash show -p
    
    # 应用上一个STASH (删除stash)
    git stash pop
    # 应用上一个STASH (保留stash)
    git stash apply
    # 应用特定的STASH (n = stash列表序号)
    git stash pop/apply stash@{n}
    # 从STASH创建新的分支 (n = stash列表序号)
    git stash branch <new branch name> stash@{n}
    
    # 删除特定的STASH (n = stash列表序号)
    git stash drop stash@{n}
    # 删除所有的STASH
    git stash clear
    

    diff

    diff常用来查看两次提交之间的变动

    image.png
    # 不加参数即默认比较工作区与暂存区
    git diff 
    # 比较暂存区与最新本地版本库(本地库中最近一次commit的内容)
    git diff --cached  [<path>...] 
    # 比较工作区与最新本地版本库,如果HEAD指向的是master分支,那么HEAD还可以换成master
    git diff HEAD [<path>...]  
    # 比较工作区与指定commit-id的差异
    git diff commit-id  [<path>...] 
    # 比较暂存区与指定commit-id的差异
    git diff --cached [<commit-id>] [<path>...] 
    # 比较两个commit-id之间的差异
    git diff [<commit-id>] [<commit-id>]
    

    使用git diff打补丁

    # 将暂存区与版本库的差异做成补丁,在当前目录下生成diff.patch文件
    git diff --cached > diff.patch 
    # 将工作区与版本库的差异做成补丁
    git diff --HEAD > diff.patch 
    # 将单个文件做成一个单独的补丁
    git diff file > diff.patch 
    # 应用补丁
    git apply diff.patch 
    

    .gitignore

    在项目中一般都会添加 .gitignore 文件,表示git要忽略的文件,比如build文件夹的下文件;但有时会发现,规则不生效。 原因是 .gitignore 只能忽略那些原来没有被track的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。

    解决方式:把本地缓存删除(改变成未track状态),然后再提交

    git rm -r --cached .
    git add .
    git commit -m 'update .gitignore'
    

    常用Git命令总结

    • git config --global user.name "你的名字" 让你全部的Git仓库绑定你的名字
    • git config --global user.email "你的邮箱" 让你全部的Git仓库绑定你的邮箱
    • git init 初始化你的仓库
    • git add . 把工作区的文件全部提交到暂存区
    • git add ./<file>/ 把工作区的<file>文件提交到暂存区
    • git commit -m "xxx" 把暂存区的所有文件提交到仓库区,暂存区空空荡荡
    • git remote add origin https://git.corp.kuaishou.com/exploration-client/overseaads.git 把本地仓库与远程仓库连接起来
    • git push -u origin master 把仓库区的主分支master提交到远程仓库里
    • git push -u origin <其他分支> 把其他分支提交到远程仓库
    • git status查看当前仓库的状态
    • git diff 查看文件修改的具体内容
    • git log 显示从最近到最远的提交历史
    • git clone + 仓库地址下载克隆文件
    • git reset --hard + 版本号 回溯版本,版本号在commit的时候与master跟随在一起
    • git reflog 显示命令历史
    • git checkout -- <file> 撤销命令,用版本库里的文件替换掉工作区的文件。我觉得就像是Git世界的ctrl + z
    • git rm 删除版本库的文件
    • git branch 查看当前所有分支
    • git branch <分支名字> 创建分支
    • git checkout <分支名字> 切换到分支
    • git merge <分支名字> 合并分支
    • git branch -d <分支名字> 删除分支,有可能会删除失败,因为Git会保护没有被合并的分支
    • git branch -D + <分支名字> 强行删除,丢弃没被合并的分支
    • git log --graph 查看分支合并图
    • git merge --no-ff <分支名字> 合并分支的时候禁用Fast forward模式,因为这个模式会丢失分支历史信息
    • git stash 当有其他任务插进来时,把当前工作现场“存储”起来,以后恢复后继续工作
    • git stash list 查看你刚刚“存放”起来的工作去哪里了
    • git stash apply 恢复却不删除stash内容
    • git stash drop 删除stash内容
    • git stash pop 恢复的同时把stash内容也删了
    • git remote 查看远程库的信息,会显示origin,远程仓库默认名称为origin
    • git remote -v 显示更详细的信息
    • git pull 把最新的提交从远程仓库中抓取下来,在本地合并,和git push相反
    • git rebase 把分叉的提交历史“整理”成一条直线,看上去更直观
    • git tag 查看所有标签,可以知道历史版本的tag
    • git tag <name> 打标签,默认为HEAD。比如git tag v1.0
    • git tag <tagName> <版本号> 把版本号打上标签,版本号就是commit时,跟在旁边的一串字母数字
    • git show <tagName> 查看标签信息
    • git tag -a <tagName> -m "<说明>" 创建带说明的标签。-a指定标签名,-m指定说明文字
    • git tag -d <tagName> 删除标签
    • git push origin <tagname> 推送某个标签到远程
    • git push origin --tags 一次性推送全部尚未推送到远程的本地标签
    • git push origin :refs/tags/<tagname> 删除远程标签<tagname>
    • git config --global color.ui true 让Git显示颜色,会让命令输出看起来更醒目
    • git add -f <file> 强制提交已忽略的的文件
    • git check-ignore -v <file> 检查为什么Git会忽略该文件

    相关文章

      网友评论

          本文标题:Git原理浅析及实用技巧

          本文链接:https://www.haomeiwen.com/subject/ltpafdtx.html