Author:onceday date:2023年3月4日
满满长路有人对你微笑过嘛…
windows安装可参考文章:git简易配置_onceday_CSDN博客
參考文档:
在提交时,git会保存一个提交对象(commit object)。该提交对象包含以下内容:
根据提交对象的不同时刻,其父对象信息也不一样:
如果存在一个工作目录,里面有三个暂存和要被提交的文件。暂存操作会为每一个文件计算校验和,然后会把当前版本的文件快照保存到Git仓库(使用blob对象)中,最终将检验和加入到暂存区域中等待提交。
当使用git commit
提交时,Git会先计算每一个子目录的校验和,然后在Git仓库中这些校验额保存为树对象。随后,Git便会创建一个提交对象,除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。
现在,Git仓库中有三个层次的对象:
如下图所示:
对于git而言,其保存的是一系列不同的快照,如Snapshot1
,而不是具体的内容(tree+blob
)。
这里需要注意,Git并不是不保存数据,而是将数据保存和版本控制分开,保存的数据可以用于多个版本,从而降低版本分支操作的开销。
每个提交对象commit
除了指向提交内容的指针之外,还包含作者信息,提交信息以及它的父对象指针。但首次提交是没有父对象的,而多个分支合并产生的提交对象,则有多个父对象。
如图所示,如果再在首次提交的基础上做出修改,然后再提交,就会得到second commit
,第二次提交就会包含指向父对象的指针。
对于Git的分支,如release/master/test
等,其本质是指向提交对象的可变指针。Git的默认分支名字是master,对于当前的显示分支(HEAD指向),每次提交都会自动向前移动。master分支只是默认创建的分支,没有任何特殊之处,现在的git安装时可以选择默认分支名了,可以替换master。
所以对于Git,创建新分支,就是创建一个可以移动的新的指针。如使用git branch test
创建一个新的指针。在使用git branch
命令之后,并不会将HEAD移动到新分支处,HEAD也是一个指针,表示当前所在的分支对象。
使用git log --decorate
命令可以查看各个分支当前所指的对象。
ubuntu->py-code:$ git log --decorate --oneline
0acf60b (HEAD -> test, tag: v1.1, master) third commit.
5c08dbf (tag: v1.0, release) second commit
220cb01 first commit
切换分支使用git checkout
命令,然后再提交一次:
ubuntu->py-code:$ git checkout test
ubuntu->py-code:$ git commit -a -m "made a change"
[test e01c849] made a change
1 file changed, 1 insertion(+), 1 deletion(-)
ubuntu->py-code:$ git log --oneline
e01c849 (HEAD -> test) made a change
0acf60b (tag: v1.1, master) third commit.
5c08dbf (tag: v1.0, release) second commit
220cb01 first commit
这个时候HEAD指针指向test分支,然后test分支又自动移到最新commit上。如下:
这个时候,master分支还指向原来的第三个提交,release依然指向第二个分支。可以使用checkout再切回master分支,这时候会发生两件事情:
HEAD
指针指回master分支。因此,分支切换会改变工作目录中的文件,如果git无法直接完成文件切换,比如工作区有未提交的文件,那么就会报错。此时可以暂存(stash)、提交(commit)、忽略(ignore)以及删除(delete)等操作。
如果接下来在master分支做出修改,再提交,那么项目的提交历史将会产生分叉。
使用命令查看如下:
ubuntu->py-code:$ git log --oneline --all --graph
* 0339c00 (HEAD -> master) made anther change
| * e01c849 (test) made a change
|/
* 0acf60b (tag: v1.1) third commit.
* 5c08dbf (tag: v1.0, release) second commit
* 220cb01 first commit
Git分支创建只是创建了一个指针(长度为40的SHA-1值字符串)的文件,所以创建和删除都很高效。而其他的版本控制系统往往还需要复制文件。
如上图所示,当前正开发到commit 46时,因为问题(issue 1)开出了一个新分支,然后又因为问题(issue 2)开出第二个分支。在解决这些问题之后,我们是希望master分支移动到commit 50的。如下:
首先,切换分支到master,git checkout master
。
合并issue 2
和master
,git merge issue2
,这样就把issue2
所在的分支合入到master分支中。
因为master分支可以直达issue2分支,因此属于fast-forward
合并,直接移动master指针到达commit 48
即可。
然后再把issue 1
合入到master
,这时就存在冲突,因为从commit 46
达到commit 50
存在两条路径,commit 50
存在两个父提交。
这个时候,Git会使用commit 49
和commit 48
这个两个分支末端,以及这两个分支的公共祖先commit 46
,来做一个简单的三方合并。
三方合并存在冲突时,可以使用git status
去查看未合并文件(unmerged
状态),Git会在冲突区域加入下面的额外字符:
<<<<<<< HEAD:index.html
xxxx(当前HEAD指针指向的内容,即master分支)xxxx
=======
xxxx(要合并分支指向的内容,即issue1分支)xxxxx
>>>>>>> issue1:index.html
=======
是分界线,但这个并非完全正确,实际合并时要慎重考虑。
再解决merge
冲突之后,对冲突文件使用git add
,将它们暂存起来,这样可以将其标记为冲突已解决状态。
最后再使用git commit
完成最终的提交,即commit 50
,master
指针也会指向这个提交。
可以使用git branch --merged
看看那些分支还未合并到当前分支。使用git branch --no-merged
查看哪些分支未合并到当前分支。
ubuntu->py-code:$ git log --oneline --all --graph
* 0339c00 (HEAD -> master) made anther change
| * e01c849 (test) made a change
|/
* 0acf60b (tag: v1.1) third commit.
* 5c08dbf (tag: v1.0, release) second commit
* 220cb01 first commit
ubuntu->py-code:$ git branch --merged
* master
release
ubuntu->py-code:$ git branch --no-merged
test
git branch --merged/--no-merged name
,可以用来查看指定名字的分支合并状态,这样无需切换分支。
远程分支是对远程仓库的引用,包括分支、标签等等,可以通过git ls-remote
来显示地获取远程引用的完整列表。
ubuntu->requests:$ git ls-remote
From [email protected]:onceday/requests.git
1e62a3ec18e19f85ddae03b4cbbdf0b4c62834c0 HEAD
cd4762d5a3b56d8933d1d9c1dff365fc5db4c768 refs/heads/3.0
6ab16db7bd55bc63dca2b6ef8ad04d37117927af refs/heads/bug/5671
1e62a3ec18e19f85ddae03b4cbbdf0b4c62834c0 refs/heads/main
9c6bd54b44c0b05c6907522e8d9998a87b69c1cd refs/heads/proposed/3.0.0
190a68550ac2172d9651470558f90cb6b754d065 refs/heads/update-3.0
fa1b0a367abc8488542f7ce7c02a3614ad8aa09d refs/heads/v2.27.x
22701d149ad9585cc01b3f9bda4e78cd77ffb996 refs/tags/2.0
b0555b68e7da5a64c46ce24567dbfa178eca1259 refs/tags/v0.10.0
8493d8329e9b606503923323af6417844508a744 refs/tags/v0.10.1
......
可以看到,不光有远程分支,还有各类标签也都显示出来了。
使用git remote show
可获取远程分支的更多信息。
ubuntu->requests:$ git remote show origin
* remote origin
Fetch URL: [email protected]:onceday/requests.git
Push URL: [email protected]:onceday/requests.git
HEAD branch: main
Remote branches:
3.0 tracked
bug/5671 tracked
main tracked
proposed/3.0.0 tracked
update-3.0 tracked
v2.27.x tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)
这里有一个关键的要素是远程tracked
分支,这些分支无法进行移动,它们只会反应远程仓库的这些分支的真实情况,并且在fetch之后,更新到远程仓库最新的状态。
这些远程追踪分支以
的形式命名,一般clone
时,默认远程分支名字是origin
。
本地仓库的远程分支,只会在同远程服务器更新数据之后,才会主动更新数据并且移动分支,否则就会保持本地的状态不会改变。
同步远程分支的命令为:git fetch
,该命令只会同步数据,而不会对本地分支做出修改。
推送远程分支的命令为git push
,如果和远程分支之间存在冲突,那么就会推送失败,并且提示原因,极端情况下(如不再需要远程分支,可以使用git push -f
来覆盖远程分支,注意,这可能导致数据丢失。
一般都使用远程ssh
连接来访问数据库,这样安全,而且不需要频繁输入密码。如果使用https
方式连接远程仓库,那么每次都需要输入密码,当然,也可通过某种方式避免这个过程。
如果远程分支和本地分支存在冲突,可使用git merge
将远程分支合并到本地之后,再推送本地分支到远程仓库。
追踪分支的一个好处就是输入git push/pull
的时候可以自动识别远程分支。使用如下命令即创建一个本地追踪分支:git checkout -b
。
对于上面的main
分支,其追踪的远程分支就是remote/main
,因此其push
和pull
便默认绑定该远程分支。可以使用git branch -u
来为当前分支绑定远程分支。
下面是一些常见的操作命令:
git push --delete
删除远程分支,只是删除指针,数据会在后续等待垃圾回收。一般来说,新手最常用的合并方法是merge,但这种方法存在一个缺陷,那就是会将一段长时间的交叉开发过程混合在一起,在提交代码时,就会遇到commit杂乱分布的场景。
如上所示,merge是在两个分支之上,合并出一个新的commit(C5),C5提交有两个父对象,因此merge过程保留了所有的信息。
如果不需要保留C4 -> C5的提交信息,那么就需要用到rebase,如下:
rebase
基于master分支,将自C2提交之后的所有提交(C4以及更多的提交)先reset出来暂存,然后在C3提交的基础之上,一个个再apply上去。这里就是在C3
基础之上应用C4
,如果存在冲突,那么需要先修改冲突。然后暂存冲突文件并提交。这一次就生成了C4'
提交,如果C4后面还有更多的提交,那么一直循环处理,直到experiment
分支自C2之后的所有分支都已合并成功。
rebase和merge本质上没有太大差别,但rebase的不足之处是会抹去原先的提交记录,因此在多人开发的场景下使用,反而造成合并冲突更多。
因此,rebase
更推荐在个人本地仓库使用,用来对最终的提交进行commt整理,merge在多人合并时使用,特别是已经提交出去的分支,最好不要用rebase去抹除。
对于这种情况,希望将C8和C9合并到master分支中,但是不需要C3,此时可使用下面的命令:
git rebase --onto master server client
该命令会把C8和C9提出来,然后放回master分支进行提交,如下:
然后可以快速合并master分支,使之包含来自client分支的修改。
git checkout master
git merge client
rebase操作命令一般如下:
git rebase