[Git] git原理及使用

作者: 木小易Ying | 来源:发表于2020-05-19 08:41 被阅读0次

    目录:

    1. git原理
    2. git fork & cherry-pick & rebase
    3. git打patch以及应用patch(把你的代码给别人)
    4. 一个惨案的发生:git pull + source tree丢弃导致其他人的代码被revert

    1. git原理

    refer to: https://codewords.recurse.com/issues/two/git-from-the-inside-out

    首先可以建一个文件夹,我是建了一个tryGit,然后里面执行:

    mkdir data
    printf 'a' > data/letter.txt
    

    现在文件夹就是酱紫的了:


    文件夹

    然后就可以用git init初始化我们的tryGit

    git init以后

    执行以后目录下面会有个隐藏的.git目录,里面存着文件的引用修改记录之类的以及git配置啥的,虽然目前里面是空的没啥东西只有一堆目录和head ref。你可以直接修改.git里面的文件其实,也就是你可以通过改这些文件修改历史。

    现在把我们的文件加入到git缓存区:

    git add data/letter.txt
    

    这个时候git会通过把文件内容hash一下生成一个字符串类似a就是2e65efe2a145dda7ee51d1741299f848e5bf752e,于是前两个字符会作为一个目录,放在objects文件夹下面,后面的字符作为blob文件名,里面存储着文件内容的压缩版本。所以即使你不小心把letter文件搞丢了还可以通过git还原。

    git objects

    除了把文件内容保存起来,git还做了什么呢?它还记录了letter文件的指向,也就这个文件内容在哪里(objects里面的哪个blob):


    index文件

    打开都是十六进制看不懂怎么破呢,你可以用git ls-files --stage查看~

    100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0   data/letter.txt
    

    可以看到data/letter.txt对应的就是a的hash值,也就是objects里面的路径。

    我们再建一个文件看一下:

    printf '1234' > data/number.txt
    git add data
    
    objects

    然后看下index文件:

    100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0   data/letter.txt
    100644 274c0052dd5408f8ae2bc8440029ff67d79bc5c3 0   data/number.txt
    

    然后我们把number.txt里面的内容从1234改为1,可以看到会新建一个blob文件,并且把index里面的指向改为新的~

    objects
    100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0   data/letter.txt
    100644 56a6051ca2b02b04ef92d5150c9ef600403cb1de 0   data/number.txt
    

    • 这里来尝试一个好玩儿的(可能没那么好玩啦)

    首先修改新生成的56文件夹下面的目录,把内容改为27文件夹下面的那样子,也就是把 压缩后的1 改为 压缩后的1234,保存以后我们看nunber.txt会发现内容还是1,并没有被修改,然后我们执行git checkout .也就是还原所有未暂存的文件,打开nunber.txt会内容还是1,是不是灰常绝望,为什么明明把compress内容改了,还是没有呢?

    现在把data目录都删除,然后再执行git checkout .,会发现data目录回来了,然后打开number.txt内容终于变成了1234了!(✿✿ヽ(°▽°)ノ✿撒花!)

    但是哦,如果这么搞完,会发现你即使再把1234改回1,然后git add data56文件里面的compress content并不会改为你1的压缩码,删掉56文件夹都不行。就会把内容搞的有点儿乱。

    如果你删掉56文件夹,其实index里面的指向仍旧是56,然后你删掉data文件夹,再git checkout .会发现旧的恢复的文件夹里面只有letter.txt没有number了。

    但现在你如果看index,里面仍旧是两个文件的指向,这个时候你可以执行git add data,这样的话由于你本地已经没有number.txt了,他就会更新index文件只留下一个啦~


    ※ Commit

    现在我们执行一下commit看看~

    git commit -m 'a1'
    [master (root-commit) a57beba] a1
     2 files changed, 2 insertions(+)
     create mode 100644 data/letter.txt
     create mode 100644 data/number.txt
    

    The commit command has three steps. It creates a tree graph to represent the content of the version of the project being committed. It creates a commit object. It points the current branch at the new commit object.

    => Commit命令会干三件事情:

    1. 创建一个树图代表跟踪历史commit
    2. 新建一个commit object
    3. 将当前的branch指向新建的commit object

    graph是由blobs和trees组成的,blob记录了文件(由git add触发),tree记录了commit。

    tree

    类似上面的例子,就是root指向了data目录,然后data里面指向了data/letter.txt 以及 data/number.txt的blob。

    当提交commit会在.git/objects/目录下生成一个文件:

    commit的blob文件
    // 这里内容打不开只是个示例哈
    tree ffe298c3ce8bb07326f888907996eaa48d266db4
    author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
    committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
    
    a1 // commit message
    

    第一行根节点,然后他会指向我们项目的根目录,然后根目录会指向他下面的子目录来构成tree。

    然后把当前branch指向最新的commit,这里可以看.git/HEAD

    ref: refs/heads/master
    

    也就是当前的head指向了master,那么master在哪里呢?打开.git/refs/heads/master可以看到酱紫的:

    a57beba0934728820d423464d2c000367cc655d2
    

    master文件里面保存了最新的commit,就是刚才我们提交的时候的a57beba,也就是master指向的commit。这里的hash你会发现和objects里面的文件是对应的哈。
    (commit号一般都是不一样的,因为它会包含作者信息的hash)

    所以现在head指向了master,master指向了最新的commit文件,commit文件里面保存了子节点的指向,也就是这样的树:


    树图

    现在我们改一下number.txt的内容:

    printf '2' > data/number.txt
    

    实际上的目录文件里面number已经改为2了,但是git的index指向还是1的blob,并且tree也没有变化,因为commit里面都没有变:


    git状态

    现在让我们执行一下:

    git add data/number.txt
    
    git状态

    这个时候index里面的会更新,因为执行了add:

    git ls-files --stage
    100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0   data/letter.txt
    100644 d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 0   data/number.txt
    

    但是由于master仍旧指向的之前的commit,所以tree里面的number文件仍旧指向1的blob而非2的。

    现在我们提交一个commit看会怎样:

    git commit -m 'a2'
    
    [master da39af2] a2
     1 file changed, 1 insertion(+), 1 deletion(-)
    

    此时看master的指向:


    master指向的commit

    可以看到已经指向了最新的commit号,所以tree变成了这样:


    新的tree

    旧的节点和旧tree不再表示最新的状态了,虽然a1的commit还存在在objects里面,但a2已经生成了一个新的tree来成为最新的master指向了。

    注意其实letter文件并没有修改,所以其实它仍旧指向同一个blob文件~

    the nodes in the objects/ directory are immutable. 注意其实objects里面的文件是不可变的哦


    ※ Checkout
    git log
    commit da39af2c85f8d2b5af98b915e0e170105415a7f7 (HEAD -> master)
    Author: xxx
    Date:   Sun Apr 12 11:40:07 2020 +0800
    
        a2
    
    commit a57beba0934728820d423464d2c000367cc655d2
    Author: xxx
    Date:   Sun Apr 12 09:54:51 2020 +0800
    
        a1
    

    这个时候可以用git checkout commit号来挪动Head的指向。

    • checkout会做四个事情:
    1. 获取你要checkout的commit指向的tree
    2. 把拿到的tree里面的内容,写入到working copy也就是工作区里面,也就是当前的目录
    3. 把tree里面的内容,写到index文件里面
    4. 把Head指向该commit

    这里我们试一下git checkout [a1的commit号]

    git checkout a57beba0934728820d423464d2c000367cc655d2
    Previous HEAD position was da39af2 a2
    HEAD is now at a57beba a1
    

    然后会发现Head文件里面变成了酱紫,master的指向并没有变,也就是head不再指向master了:


    checkout以后

    并且index文件变为了:(a1)的样子

    100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0   data/letter.txt
    100644 56a6051ca2b02b04ef92d5150c9ef600403cb1de 0   data/number.txt
    

    现在我们再checkout到a2节点,注意这个时候其实head的指向仍旧不是master,master虽然也指向a2,但是head文件里面的ref不再是master了,所以如果此时你改了点东西提交只是提交到了Head上面,master是没有被改变的哦

    printf '3' > data/number.txt
    git add data/number.txt
    git commit -m 'a3'
    [detached HEAD 3709709] a3
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    checkout以后提交 tree
    ※ Branch

    现在我们来创建新的分支:

    git branch deputy
    
    新分支的ref

    当你此时创建新分支的话,新的分支就指向了Head的最新节点也就是a3。

    创建新分支

    如果你执行git checkout master那么Head会再次指向master哈~

    • 如果你改了number.txt以后再checkout到deputy:
    printf '789' > data/number.txt
    git checkout deputy
    error: Your local changes to the following files would be overwritten by checkout:
        data/number.txt
    Please commit your changes or stash them before you switch branches.
    Aborting
    

    那么会不允许你修改的哈,因为当前master指向的number是2,然后deputy里面指向的是3,但是工作区里面又是789,两两不同于是就产生了问题。

    如果你想修改成功checkout就先把number.txt改为master指向的a2的样子再checkout就可以啦。


    ※ Merge

    如果把祖先分支merge到后代分支,也就是把时间线前面的merge到后面的,那么就什么都不会发生:

    git merge master
    Already up to date.
    

    反之如果merge后代分支到祖先分支呢?

    git checkout master
    Switched to branch 'master'
    
    git merge deputy
    Updating da39af2..3709709
    Fast-forward
     data/number.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    

    git发现master是deputy的祖先,就会进行Fast-forwardmerge,也就是直接把master的指针指向deputy最新的commit。

    Fast-forward merge
    • 下面看如果两个分支对不同文件分别做了修改,那么merge的时候会发生什么呢?
    git checkout master
    printf '4' > data/number.txt
    git add data/number.txt
    git commit -m 'a4'
    
    git checkout deputy
    printf 'b' > data/letter.txt
    git add data/letter.txt
    git commit -m 'b3'
    
    改了不同文件

    现在执行git merge master -m 'b4'可以成功merge,那么这一步做了些什么呢?

    • 不同line的merge会指向性8个步骤:
    1. 把giver commit这里也就是master的hash写到.git/MERGE_HEAD里面,标记着当前其实是在merging的
    2. 找到giver和receiver branch的最近共同祖先(是不是很算法题。。)
    3. 根据giver和receiver commit生产index文件
    4. 生成结合了两个分支从最近共同祖先之后的修改的diff,列出所有被add, remove, modify or conflict的文件路径
    5. 把diff里列出来的区别应用到working copy工作区
    6. 应用diff里面的blob到index文件
    7. commit最新的index:
    tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
    parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
    parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
    author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
    committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
    
    b4
    

    注意这个时候b4因为是merge来的,所以有两个祖先哦


    merge
    1. 把deputy指向最新的commit

    • 现在我们看下如果两个分支分别对同一个文件修改会怎样,首先把master挪到最新的deputy上面:
    git checkout master
    git merge deputy
    

    然后分别把master上改成b6,deputy改成b5:

    git checkout deputy
    printf '5' > data/number.txt
    git add data/number.txt
    git commit -m 'b5'
    
    git checkout master
    printf '6' > data/number.txt
    git add data/number.txt
    git commit -m 'b6'
    

    现在执行merge会:

    git merge deputy
    Auto-merging data/number.txt
    CONFLICT (content): Merge conflict in data/number.txt
    Automatic merge failed; fix conflicts and then commit the result.
    

    这个时候如果conflict了你可以看到MERGE_HEAD文件啦~

    merge中

    其实现在merge还是8步,但是第4步combine diff的时候它会发现改了同一个文件,第7步和第8步就不执行了。

    第5步应用diff到工作区的时候,如果发现conflict就会都应用过去:

    <<<<<<< HEAD
    6
    =======
    5
    >>>>>>> deputy
    

    第6步改index的时候,Entries in the index are uniquely identified by a combination of their file path and stage. The entry for an unconflicted file has a stage of 0. Before this merge, the index looked like this, where the 0s are stage values

    git ls-files --stage
    100644 63d8dbd40c23542e740659a7168a0ce3138ea748 0   data/letter.txt
    100644 bf0d87ab1b2b0ec1a11a3973d2845b42413d9767 1   data/number.txt
    100644 62f9457511f879886bb7728c986fe10b0ece6bcb 2   data/number.txt
    100644 7813681f5b41c028345ca62a2be376bae70b7f61 3   data/number.txt
    

    大概意思就是会结合diff的index,这个时候发现diff里面有对同一个file放两个不同的index,于是就会conflict。

    如果没有conflict会用0标记,然后1代表base的content的hash,2表示receiver的content的hash,3代表giver的content的hash。

    然后就是如何解冲突了,这个时候你可以:

    printf '11' > data/number.txt
    git add data/number.txt
    

    这个时候会告诉git我们已经解了冲突文件,并且index文件会自动更新:

    git ls-files --stage
    100644 63d8dbd40c23542e740659a7168a0ce3138ea748 0   data/letter.txt
    100644 9d607966b721abde8931ddd052181fae905db503 0   data/number.txt
    

    number.txt就指向了新的blob文件,这个时候再git commit -m 'b11'就可以commit啦~~ 在commit结束的时候.git/MERGE_HEAD就会被删除~

    merge成功以后
    ※ Remove

    删除其实比较简单,就是把index里的blob指针删掉。

    删除文件
    ※ Remote

    这里模拟一下remote的情形,copy一下当前目录,重命名为新的目录名,然后add一下作为remote~

    cd ..
    cp -R tryGit tryGitRemote
    cd tryGit
    git remote add localRemote ../tryGitRemote
    

    可以看到tryGit目录下面的remote已经有了url啦~~

    add remote

    现在改一下remote里面的number.txt:

    cd ../tryGitRemote
    printf '12' > data/number.txt
    git add data/number.txt
    git commit -m '12'
    

    然后进入tryGit拉一下remote~

    cd ../tryGit
    git fetch localRemote master
    From ../tryGitRemote
     * branch            master     -> FETCH_HEAD
     * [new branch]      master     -> localRemote/master
    
    • fetch remote到本地会有4步:
    1. 获取远端 localRemote 的master指向的commit,这里也就是12
    2. 获取12这个commit的object本身,以及它所依赖的所有object(就是objects目录下的文件),然后和本地的objects比对,把剩余的拷贝到本地.git/objects/
    3. 把本地的.git/refs/remotes/localRemote/master的内容设置为远端12那个commit的hash值
    4. 把本地的.git/FETCH_HEAD设置为
    f482a44f82e769ff748876ef301dedee9abe114f        branch 'master' of ../tryGitRemote
    
    类似酱紫的fetch

    objects can be copied. This means that history can be shared between repositories. 注意其实objects就是提交历史,所以提交历史是可以被share的哈


    Merge FETCH_HEAD
    git merge FETCH_HEAD
    

    其实这个就和把后代给祖先merge是一样的,会直接fast-forward一下~

    fast-forward merge
    ※ Pull
    git pull bravo master
    

    Pull is shorthand for “fetch and merge FETCH_HEAD”.
    Pull就是fetch远端然后merge FETCH_HEAD的缩写~


    ※ Clone
    git clone tryGit Charlie
    

    clone和cp的区别可能是,clone会把tryGit作为Charlieorigin远端,并且fetch origin + merge FETCH_HEAD一下~


    2. git fork & cherry-pick & rebase

    ※ fork

    可参考https://www.jianshu.com/p/8200d4d90d77

    比较大的产品都不能每个人都在公司真的project上面干活,然后每个人都直接推到公司的project,这样很容易出错,所以每个人都先fork一下公司的项目到自己的账号下,然后进行开发,最后提交Merge Request到真正的公司项目中,当review并修改过后才能提交测试,都过了以后可以merge到公司产品的branch里面。

    如果想拉公司仓库的代码可以:

    $ git remote add upstream  https://github.com/datura-lj/git-fork-demo.git
    $ git pull upstream master
    

    如果想拉别人仓库的代码:

    git pull https://github.com/xiaolv/datura-lj/git-fork-demo.git master
    

    这里其实可以用git pull -r也就是rebase~
    可参考https://blog.csdn.net/jiajia4336/article/details/87855235

    git pull = git fetch + git merge
    git pull --rebase = git fetch + git rebase
    

    ※ rebase

    可参考https://www.jianshu.com/p/4a8f4af4e803https://www.jianshu.com/p/dc367c8dca8e

    也就是rebase其实是把merge会产生的分叉合成了一个节点,方便提交以及cherry-pick等,如果revert也只要一个节点会方便一点,但是丢失了分次commit的信息也,万一某个小commit有问题不是很容易回滚。

    适用的场景包括合并多个commit为1个commit:git rebase -i [startpoint] [endpoint]
    以及将几个节点移动到其他的branch:git rebase [startpoint] [endpoint] --onto [branchName]

    昨天尝试的一个用法是删除某一个commit的时候也用到了rebase:https://www.jianshu.com/p/2fd2467c27bb

    注意当rebase有冲突的时候需要先解冲突+git add .+git rebase --continue+push哦~


    ※ cherry-pick

    可参考:https://blog.csdn.net/jxianxu/article/details/79240158

    这个命令其实就是用于拣选某些节点从一个分支到另外一个分支的~

    可以用:

    git cherry-pick  65ad383c977acd6c7e
    

    还可以一次拣选多个commit,用空格分隔commit id即可。但是每个commit被拣选都会生成一个新的commit,如果希望拣选多个commit合成一条,可以用-n来实现。


    3. git打patch以及应用patch(把你的代码给别人)

    其实就是如果我们改了啥但是不方便提交,可以把最近的几个commit打成一个文件发给别人,同事只要apply一下就行了比较方便~ 可以参考:https://www.cnblogs.com/ArsenalfanInECNU/p/8931377.html

    打包自己的代码:

    git format-patch HEAD^   #生成最近的1次commit的patch
    git format-patch HEAD^^  #生成最近的2次commit的patch
    git format-patch <r1>..<r2>  #(包含两个commit. <r1>和<r2>都是具体的commit号)
    git format-patch -1 <r1> #生成单个commit的patch
    git format-patch <r1> #生成某commit以来的修改patch(不包含该commit)
    git format-patch --root <r1> #生成从根到r1提交的所有patch
    

    应用别人发来的文件:

    git am 0001-limit-log-function.patch  // # 将名字为0001-limit-log-function.patch的patch打上
    

    4. 一个惨案的发生:git pull + source tree丢弃导致其他人的代码被revert

    举个例子,如果建两个分支都在同一个节点,一个叫test_3.8.9_me,另一个叫test_3.8.9_others,sourcetree显示是下面酱紫的:


    三个在一起的节点

    然后两个分支分别去加一句log,各自提交到远端就变成了酱紫:


    各自提交一下

    这个时候如果在test_3.8.9_others分支上,用命令行pull一下代码,会显示下面酱紫:


    test_3.8.9_others分支pull

    sourcetree变成了酱紫:


    test_3.8.9_me改的东东,也是pull之后拉到的东东

    但是如果这个时候你不想提交这部分,于是在sourcetree把这部分提交重置了,并且push到了远端:


    pull后重置

    然后当test_3.8.9_me分支merge test_3.8.9_others分支以后,test_3.8.9_me做的修改就丢掉了,因为其实pull的时候会自动和本地merge,相当于others分支当时是在me分支的节点上把它的代码重置了,所以me分支的修改就丢掉了,于是变成了酱紫:


    test_3.8.9_me merge test_3.8.9_others

    代码里没有test_3.8.9_me分支的修改,综上所述不要pull以后没有提交就重置source tree的stash哈(大概只有我这么傻...),以及最好还是直接git命令操作,我蒸的是感觉自己太不适合写代码了… 心如刀割啊。。。

    相关文章

      网友评论

        本文标题:[Git] git原理及使用

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