目录:
- git原理
- git fork & cherry-pick & rebase
- git打patch以及应用patch(把你的代码给别人)
- 一个惨案的发生: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
目录,里面存着文件的引用修改记录之类的以及git配置啥的,虽然目前里面是空的没啥东西只有一堆目录和head ref。你可以直接修改.git里面的文件其实,也就是你可以通过改这些文件修改历史。
现在把我们的文件加入到git缓存区:
git add data/letter.txt
这个时候git会通过把文件内容hash一下生成一个字符串类似a
就是2e65efe2a145dda7ee51d1741299f848e5bf752e
,于是前两个字符会作为一个目录,放在objects文件夹下面,后面的字符作为blob文件名,里面存储着文件内容的压缩版本。所以即使你不小心把letter文件搞丢了还可以通过git还原。
除了把文件内容保存起来,git还做了什么呢?它还记录了letter文件的指向,也就这个文件内容在哪里(objects里面的哪个blob):
打开都是十六进制看不懂怎么破呢,你可以用git ls-files --stage
查看~
100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0 data/letter.txt
可以看到data/letter.txt
对应的就是a的hash值,也就是objects里面的路径。
我们再建一个文件看一下:
printf '1234' > data/number.txt
git add data
然后看下index文件:
100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0 data/letter.txt
100644 274c0052dd5408f8ae2bc8440029ff67d79bc5c3 0 data/number.txt
然后我们把number.txt
里面的内容从1234
改为1,可以看到会新建一个blob文件,并且把index里面的指向改为新的~
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 data
,56
文件里面的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命令会干三件事情:
- 创建一个树图代表跟踪历史commit
- 新建一个commit object
- 将当前的branch指向新建的commit object
graph是由blobs和trees组成的,blob记录了文件(由git add触发),tree记录了commit。
类似上面的例子,就是root指向了data目录,然后data里面指向了data/letter.txt 以及 data/number.txt的blob。
当提交commit会在.git/objects/
目录下生成一个文件:
// 这里内容打不开只是个示例哈
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 add data/number.txt
这个时候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的指向:
可以看到已经指向了最新的commit号,所以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会做四个事情:
- 获取你要checkout的commit指向的tree
- 把拿到的tree里面的内容,写入到working copy也就是工作区里面,也就是当前的目录
- 把tree里面的内容,写到index文件里面
- 把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了:
并且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(-)
※ Branch
现在我们来创建新的分支:
git branch deputy
当你此时创建新分支的话,新的分支就指向了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-forward
merge,也就是直接把master的指针指向deputy最新的commit。
- 下面看如果两个分支对不同文件分别做了修改,那么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个步骤:
- 把giver commit这里也就是master的hash写到
.git/MERGE_HEAD
里面,标记着当前其实是在merging的 - 找到giver和receiver branch的最近共同祖先(是不是很算法题。。)
- 根据giver和receiver commit生产index文件
- 生成结合了两个分支从最近共同祖先之后的修改的diff,列出所有被add, remove, modify or conflict的文件路径
- 把diff里列出来的区别应用到working copy工作区
- 应用diff里面的blob到index文件
- 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来的,所以有两个祖先哦
- 把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还是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
就会被删除~
※ Remove
删除其实比较简单,就是把index里的blob指针删掉。
※ Remote
这里模拟一下remote的情形,copy一下当前目录,重命名为新的目录名,然后add一下作为remote~
cd ..
cp -R tryGit tryGitRemote
cd tryGit
git remote add localRemote ../tryGitRemote
可以看到tryGit
目录下面的remote已经有了url啦~~
现在改一下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步:
- 获取远端 localRemote 的master指向的commit,这里也就是
12
- 获取
12
这个commit的object本身,以及它所依赖的所有object(就是objects目录下的文件),然后和本地的objects比对,把剩余的拷贝到本地.git/objects/
- 把本地的
.git/refs/remotes/localRemote/master
的内容设置为远端12
那个commit的hash值 - 把本地的
.git/FETCH_HEAD
设置为
f482a44f82e769ff748876ef301dedee9abe114f branch 'master' of ../tryGitRemote
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一下~
※ 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
作为Charlie
的origin
远端,并且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/4a8f4af4e803和https://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一下代码,会显示下面酱紫:
sourcetree变成了酱紫:
但是如果这个时候你不想提交这部分,于是在sourcetree把这部分提交重置了,并且push到了远端:
然后当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分支的修改,综上所述不要pull以后没有提交就重置source tree的stash哈(大概只有我这么傻...),以及最好还是直接git命令操作,我蒸的是感觉自己太不适合写代码了… 心如刀割啊。。。