git pod 正则 等工具

[Git] git原理及使用

2020-05-19  本文已影响0人  木小易Ying

目录:

  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的指向。

  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哈~

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
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,那么这一步做了些什么呢?

  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

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
  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命令操作,我蒸的是感觉自己太不适合写代码了… 心如刀割啊。。。

上一篇下一篇

猜你喜欢

热点阅读