Git
全面介绍了Git的原理和使用
Source
廖雪峰的教程
git使用技巧和笔记
Learn Git Branching
Tools:
- Git Command Explorer
Pro Git
Git Community Book
Intro
- Git是Linus用C写的分布式版本管理系统
集中式:
- CVS[^1]:最早的开源且免费的集中式版本控制系统<由于自身设计问题,会造成提交文件不完整,版本库莫名损坏的情况
- SVN[^2]:同样开源且免费,修正了CVS的一些稳定性问题,是目前用得最多的集中式版本控制系统
- ClearCase:IBM的,收费,又大又笨
分布式:
- Git
git add
tells git that it should track changes made to a particular file. Useful if you don't necessarily want to track all changes to all files in the index at every commit.
git commit
is like a save button. Up until that point all changes since the last commit are still just "staged" and not yet permanently written into the local git repo. This command tells git to permanently store changes made to the files you selected usinggit add
as a node in the git tree.
git commit -a
is a shortcut for "save all changes to all known files in the index". It's the same as if yougit add
'ed all the files you're already tracking and then rangit commit
. No new files will be tracked, so if something / someone writes junk like temporary files they won't get swept up into the repo.
创建版本库
版本库:repository
1 | $ git init |
自动生成.git
目录, 用于跟踪管理版本库; 以及在 project 和 每个 module 中生成一个 .gitgnore
文件
基本操作
git add
我们修改Git.md文件,运行git.status
看看结果:
1 |
|
git.status
命令可以展示仓库当前的状态,上面的输出告诉我们,Git.md
被修改过了,但还没有准备提交的修改.
用git diff Git.md
能看到具体修改了什么内容:
1 | $ git diff |
git diff
顾名思义就是查看difference,显示的格式正是Unix通用的diff格式
知道了对Git.md
作了什么修改后,再把它提交到仓库就放心多了. 提交修改和提交新文件一样是两步,
git add
1 | $ git add Git.md |
没有任何反应,在执行git commit
之前,我们再运行git status
看看当前仓库的状态:
1 | $ git status |
git status
告诉我们,将要被提交的修改包括Git.md
,下一步就能放心提交了:
1 | $ git commit -m"4" |
提交后,再用git status
看看仓库的当前状态:
1 | $ git status |
Git告诉我们当前没有需要提交的修改,且工作目录是干净(working tree clean)的.
- windows下
commit
的message千万不能有中文,否则会乱码
版本回退
先提交文件,message为"origin";再输入"初次修改"并提交,message为"chucixiugai";再输入"再次修改" 并提交,message为"再次修改".
现在,Git.md
一共有三个版本(origin之前的不算)被提交到repository里了,
版本1:origin
1 |
(啥都没写)
版本2:chucixiugai
1 | 初次修改 |
版本3:zaicixiugai
1 | 再次修改 |
我们不可能记住一个文件每次都改了什么内容,版本控制系统肯定有某个命令可以告诉我们历史记录,Git中用git log
命令查看:
1 | $ git log |
git log
命令显示最近到最远的全部提交日志,我们看最近三次,最近一次是zaicixiugai
,上一次是chuxixiugai
,再上一次是origin
. 如果嫌输出信息太多不太好看,可以加上--pretty=oneline
参数:
1 | $ git log --pretty=oneline |
我们看到许多类似028637e
的commit id
(版本号),这是一个SHA1计算出的非常大的数字,用十六进制表示. SVN的版本号是1,2,3,4递增,因为SVN是集中式. Git是分布式,号码容易不够,所以用这种方式
每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线
现在我们把Git.md
回退到上个版本chucitijiao
. 首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD
(小写也可以)表示当前版本,即最近的提交02863
,上个版本就是HEAD^
,上上个版本就是HEAD^^
,以此类推. 往前100个版本数不过来,可以写成HEAD~100
.
用 git reset
命令:
1 | $ git reset --hard HEAD^ |
,--hard
参数的意义之后再讲.
OK,已经被还原了
现在用git log
看看当前版本库的提交日志:
1 | $ git log |
我们看不到最近的那个版本zaicixiugai
了! 好比时间从21世纪穿越回了10世纪,当然看不到后面的历史了! 如果想回去,就要用commit id
.
- 如果还没关掉GIt窗口,可以把窗口往上翻,找到那个
zaicixiugai
的commit id
是028637e...
,于是可以指定回到未来的某个版本:
1 | $ git reset --hard 0286 |
版本号没必要写全,写前几位. Git会自动去找.
OK,已经回到未来了.
- 如果已经关掉窗口了,Git中有
git reflog
记录你的每一次命令:
1 | $ git reflog --pretty=oneline |
小结
HEAD
指向的版本就是当前版本,这是个指针. 版本穿梭使用git reset --hard commit_id
- 穿梭前,用
git log
可以查看提交历史,以便确定要回退到哪个版本. - 要回到未来,可以用
git reflog
来确定要回到未来的哪个版本. - 如果commit(提交)比较多,git log 的内容就会比较多;当满屏放不下,就会显示冒号,回车(往下滚一行)、空格(往下滚一页)可以继续查看剩余内容;退出:英文状态下 按 q 可以退出git log 状态。
撤销修改
在readme .txt
中添加了一行,但还没有git add
:
1 | $ cat readme.txt |
现在想撤销最后一行,可以用 git restore readme.txt
, 有两种情况:
read.me.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态 .readme.txt
已经添加到暂存区后,又做了修改. 现在,撤销修改就回到和添加到暂存区后的状态.
总之,就是让该文件回到最近一次add
时的状态(即 暂存区里的状态)
现在,看看readme.txt
的内容:
1 | $ cat readme.txt |
果然复原了.
如果我把那句话git add
到暂存区了呢? ( 但还没有commit
) ,先用git status
查看一下,
1 | $ git status |
git告诉我们,修改只是被添加到了暂存区,还没有被提交,可以用 git restore --staged readme.txt
把暂存区的修改撤销掉( unstage ),重新放回工作区:
1 | $ cat readme.txt |
再用git status
查看一下,现在暂存区是干净的,工作区有修改:
1 | $ git status |
我们要丢弃工作区的修改,就回到了上上步: git restore readme.txt
1 | $ git restore readme.txt |
Over!
如果我不仅把那句话git add
了,我还git commit
到了版本库, 我们可以用版本回退. 但如果你还把它git push
到了 远程版本库,那就完蛋了.
删除文件
情况一: 工作区文件删除,无其它操作
rm readme.txt
可用命令git restore readme.txt
恢复文件
情况二: 工作区文件删除,版本库文件删除
rm readme.txt
git rm readme.txt
git commit -m" remove readme.txt "
可用命令git reset --hard HAED^
恢复文件( 回到哪个头要看情况,如果当前分支内有这个文件,那就可以回到当前版本,不用去上个版本)
rm
是DOS命令,在各个shell都可以用. 用在git仓库中,是删除工作区的文件,用 git restore readme.txt
可以还原. 而git rm readme.txt
是删除工作区和暂存区的文件, 由于git restore
的原理是将工作复原为暂存区中的版本,而暂存区中该文件也被删除了,所以恢复不了, 分支里还有这个文件,所以用版本回退git reset --hard HEAD^
git pull
1、将远程指定分支 拉取到 本地指定分支上:
1 | git pull origin <远程分支名>:<本地分支名> |
2、将远程指定分支 拉取到 本地当前分支上:
1 | git pull origin <远程分支名> |
3、将与本地当前分支同名的远程分支 拉取到 本地当前分支上(需先关联远程分支,方法见文章末尾)
1 | git pull |
在克隆远程项目的时候,本地分支会自动与远程仓库建立追踪关系,可以使用默认的origin来替代远程仓库名
pull强制覆盖本地文件
ref: https://www.jianshu.com/p/1ac2e1f99166
重要提示:如果您有任何本地更改,将会丢失。无论是否有--hard选项,任何未被推送的本地提交都将丢失。 如果您有任何未被Git跟踪的文件(例如上传的用户内容),这些文件将不会受到影响。 下面是正确的方法:
1 | git fetch --all |
然后,你有两个选择:
1 | git reset --hard origin/master |
或者如果你在其他分支上:
1 | git reset --hard origin/<branch_name> |
说明:
git fetch
从远程下载最新的,而不尝试合并或rebase任何东西。
然后git reset
将主分支重置为您刚刚获取的内容。 --hard
选项更改工作树中的所有文件以匹配origin/master
中的文件
在重置之前可以通过从master创建一个分支来维护当前的本地提交:
1 | git checkout master |
在此之后,所有旧的提交都将保存在new-branch-to-save-current-commits中。然而,没有提交的更改(即使staged)将会丢失。确保存储和提交任何你需要的东西。
git push
1、将本地当前分支 推送到 远程指定分支上:
1 | git push origin <本地分支名>:<远程分支名> |
2、将本地当前分支 推送到 与本地当前分支同名的远程分支:
1 | git push origin <本地分支名> |
3、将本地当前分支 推送到 与本地当前分支同名的远程分支上(需先关联远程分支])
1 | git push |
修改commit信息
https://blog.csdn.net/Muscleape/article/details/105637401
远程仓库
添加远程库
把已有的本地仓库与一个git仓库相关联( 建立绑定关系 ):
$ git remote add origin https://github.com/LYK-love/Learning
, 添加后,远程库的名字就是origin
,这是Git默认的叫法,也可以改名,但没必要.
下一步,就可以把本地库的内容push到远程库上:
1 | $ git push -u origin master |
把本地库内容推送到远程,用git push
命令,实际上是把当前分支master
(本地的那个)推送到远程库(名叫origin
).
由于远程库是空的,我们第一次推送master
分支时,加上了-u
参数. Git不但会把本地的master
分支内容推送到远程新的master
分支( 现在远程也有一个叫master
的分支了),还会把本地的master
分支和远程的master
分支关联起来,这样在以后的oush或oull时就可以简化命令.
从现在起,只要在本地作了git commit
,就可以通过命令git push origin
把本地的master
分支的最新修改推送到Github,大功告成!
删除远程库
如果添加远程库的时候地址写错了,或者就是想删除远程库,可以用git remote rm <name>
命令. 使用前建议先用git remote -v
查看远程库信息:
1 | $ git remote -v |
然后,根据名字删除.
注意:
- 此处的"删除"其实是解除了本地和远程库的绑定关系,并没有物理上删除远程库,远程库本身没有任何改动( 也就是说远程库里的内容都还在 )要恢复绑定关系,可以再次用
$ git remote add origin https://github.com/LYK-love/Learning
, 然后用$ git push -u origin master
来推送(-u
参数能关联分支 ) origin
就是你指向的远程库的名字,可以等价于https://github.com/LYK-love/Learning
,它指向的是repository, 而master只是这个repository中默认创建的第一个branch. 当我们push
的时候,因为origin
和master
是默认创建的,所以这二者可以省略,但这是个bad practice,因为如果我换一个branch再push的时候,这样就很纠结了.- 当然
origin
这个名字来源于$ git remote add origin https://github.com/LYK-love/Learning
,我们也可以把origin
改成别的名字,比如阿猫阿狗,如$ git remote add aMao https://github.com/LYK-love/Learning
,那么推送的时候就可以用git push -u aMao master
了, 如果你的本地版本库是从远程仓库git clone而来,git会默认把这个远程仓库的地址叫做origin. 这时候依旧可以通过 git remote add 把远程仓库的名称改成'aGou'
Branch
创建分支
创建dev
分支, 然后切换到dev
分支:
1 | $ git switch -c dev |
切换分支
1 | git switch dev |
查看分支
查看本地所有分支:
1
git branch
查看远程所有分支
1
git branch -r
查看本地及远程的所有分支:
1
git branch -a
删除分支
删除远程分支:
1
git push origin :br (origin 后面有空格)
删除本地分支:
1
git branch -D br
关联分支
将本地分支与远程同名分支相关联
1 | git branch --set-upstream-to=origin/develop develop |
也可以在push时设置:
1 | git push --set-upstream origin <本地分支名> |
pull和push同.
创建与合并分支
HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
首先,我们创建
dev
分支,然后切换到dev
分支:然后,用
git branch
命令查看当前分支:然后提交:
1
2
3
4$ git add readme.txt
$ git commit -m "branch test"
[dev b17d20e] branch test
1 file changed, 1 insertion(+)现在,
dev
分支的工作完成,我们就可以切换回master
分支:1
2$ git switch master
Switched to branch 'master'
切换回master
分支后,再查看一个readme.txt
文件,刚才添加的内容不见了!( 工作区的不见了!!! )因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:
现在,我们把
dev
分支的工作成果合并到master
分支上:1
2
3
4
5$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt
的内容(工作区),就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
1 | $ git branch -d dev |
删除后,查看branch
,就只剩下master
分支了:
1 | $ git branch |
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全。
查看分支:
git branch
创建分支:
git branch <name>
切换分支:
git checkout <name>
或者git switch <name>
创建+切换分支:
git checkout -b <name>
或者git switch -c <name>
合并某分支到当前分支:
git merge <name>
删除分支:
git branch -d <name>
查看远程分支
查看远程与本地当前分支对应的分支:
1 | git branch -vv |
查看本地和远程所有分支
1 | git branch -a |
解决冲突
如果master
分支和feature1
分支各自都分别有新的提交,变成了这样:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
1 | $ git merge feature1 |
果然冲突了!Git告诉我们,readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
1 | $ git status |
打开工作区的readme.txt
,我们可以直接查看readme.txt的内容:
1 | Git is a distributed version control system. |
Git用<<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
1 | Creating a new branch is quick and simple. |
再提交:
1 | $ git add readme.txt |
现在,master
分支和feature1
分支变成了下图所示:
用带参数的git log
也可以看到分支的合并情况:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
最后,删除feature1
分支:
1 | $ git branch -d feature1 |
工作完成。
当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
用
git log --graph
命令可以看到分支合并图。
分支管理策略
通常,合并分支时,如果可能,Git会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward
模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff
方式的git merge
:
首先,仍然创建并切换dev
分支:
1 | $ git switch -c dev |
修改readme.txt文件,并提交一个新的commit:
1 | $ git add readme.txt |
现在,我们切换回master
:
1 | $ git switch master |
准备合并dev
分支,请注意--no-ff
参数,表示禁用Fast forward
:
1 | $ git merge --no-ff -m "merge with no-ff" dev |
因为本次合并要创建一个新的commit,所以加上-m
参数,把commit描述写进去。
合并后,我们用git log
看看分支历史:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
可以看到,不使用Fast forward
模式,merge后就像这样:
- 不用
Fast forwa
模式,提交图就像是dev
分支上做提交,master
分支上做提交, 然后在master
分支上手动解决冲突再提交 的图一样!. - 合并分支时,加上
--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward
合并就看不出来曾经做过合并。 - 如果之前
master
和dev
只想相同提交. 现在你在master
分支, 对readme.txt
做了修改, 然后switch
到了dev
分支,add
,commit
, 现在dev
指向新的提交了! 我们switch
回到master
分支, 打开readme.txt
,发现里面的内容是没有修改过的, 这是因为分支是指向提交的, 尽管你在master
分支做了修改,但没有提交, 提交是在dev
分支上完成的. 因此master
分支指向的是上一次提交, 也就是没有修改过的版本.
Bug分支
软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交:
1 | $ git status |
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,Git还提供了一个stash
功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
1 | $ git stash |
现在,用git status
查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master
分支上修复,就从master
创建临时分支:
1 | $ git checkout master |
现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交:
1 | $ git add readme.txt |
修复完成后,切换到master
分支,并完成合并,最后删除issue-101
分支:
1 | $ git switch master |
太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev
分支干活了!
1 | $ git switch dev |
工作区是干净的,刚才的工作现场存到哪去了?用git stash list
命令看看:
1 | $ git stash list |
工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply
恢复,但是恢复后,stash内容并不删除,你需要用git stash drop
来删除;
另一种方式是用git stash pop
,恢复的同时把stash内容也删了:
1 | $ git stash pop |
再用git stash list
查看,就看不到任何stash内容了:
1 | $ git stash list |
你可以多次stash,恢复的时候,先用git stash list
查看,然后恢复指定的stash,用命令:
1 | $ git stash apply stash@{0} |
在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?
有木有更简单的方法?
有!
同样的bug,要在dev上修复,我们只需要把4c805e2 fix bug 101
这个提交所做的修改“复制”到dev分支。注意:我们只想复制4c805e2 fix bug 101
这个提交所做的修改,并不是把整个master分支merge过来。
为了方便操作,Git专门提供了一个cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
1 | $ git branch |
Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803
,它并不同于master的4c805e2
,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick
,我们就不需要在dev分支上手动再把修bug的过程重复一遍。
修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;
当手头工作没有完成时,先把工作现场
git stash
一下,然后去修复bug,修复后,再git stash pop
,回到工作现场;在master分支上修复的bug,想要合并到当前dev分支,可以用
git cherry-pick <commit>
命令,把bug提交的修改“复制”到当前分支,避免重复劳动。
Feature分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
现在,你终于接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。
于是准备开发:
1 | $ git switch -c feature-vulcan |
5分钟后,开发完毕:
1 | $ git add vulcan.py |
切回dev
,准备合并:
1 | $ git switch dev |
一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。
但是!
就在此时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:
1 | $ git branch -d feature-vulcan |
销毁失败。Git友情提醒,feature-vulcan
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D
参数。。
现在我们强行删除:
1 | $ git branch -D feature-vulcan |
终于删除成功!
- 开发一个新feature,最好新建一个分支;
- 如果要丢弃一个没有被合并过的分支,可以通过
git branch -D <name>
强行删除。
多人协作
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;- bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
- feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
- 首先,可以试图用
git push origin <branch-name>
推送自己的修改; - 如果推送失败,则因为远程分支比你的本地更新,需要先用
git pull
试图合并; - 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用
git push origin <branch-name>
推送就能成功
- 查看远程库信息,使用
git remote -v
;- 比
git remote
更详细 - 上面显示了可以抓取和推送的
origin
的地址。如果没有推送权限,就看不到push的地址。
- 比
- 本地新建的分支如果不推送到远程,对其他人就是不可见的;
- 从本地推送分支,使用
git push origin branch-name
,如果推送失败,先用git pull
抓取远程的新提交; - 在本地创建和远程分支对应的分支并关联起来( 就是说不用
set-upstream
),使用git switch -c branch-name origin/branch-name
,本地和远程分支的名称最好一致. - 建立本地分支和远程分支的关联,使用
git branch --set-upstream branch-name origin/branch-name
; - 从远程抓取分支,使用
git pull
,如果有冲突,要先处理冲突。
Rebase
在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。
每次合并再push后,分支变成了这样:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?
其实是可以做到的!
Git有一种称为rebase的操作.
同步后,我们对hello.py
这个文件做了两次提交。用git log
命令看看:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
注意到Git用(HEAD -> master)
和(origin/master)
标识出当前分支的HEAD和远程origin的位置分别是582d922 add author
和d1be385 init hello
,本地分支比远程分支快两个提交。
现在我们尝试推送本地分支:
1 | $ git push origin master |
很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下:
1 | $ git pull |
再用git status
看看状态:
1 | $ git status |
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用git log
看看:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,有没有问题?
有!
什么问题?
不好看!
有没有解决方法?
有!
这个时候,rebase就派上了用场。我们输入命令git rebase
试试:
1 | $ git rebase |
输出了一大堆操作,到底是啥效果?再用git log
看看:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1
之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello
,而是基于f005ed4 (origin/master) set exit=1
,但最后的提交7e61ed4
内容是一致的。
这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。
最后,通过push操作把本地分支推送到远程:
1 | Mac:~/learngit michael$ git push origin master |
再用git log
看看效果:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
远程分支的提交历史也是一条直线。
- rebase操作可以把本地未push的分叉提交历史整理成直线;
- rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。
合并远程分支
假设你本地在使用的分支为a, 需要合并的远程分支为b
新建和远程分支对应的本地分支:
1
git switch -c b origin/b
将远程代码pull到本地:
1
git pull origin b
返回到你的分支a
1
git switch a
合并分支a与分支b
1
git merge b
Submodule
A git submodule is a record within a host git repository that points to a specific commit in another external repository.
Clone a submodule
When you clone a project that contains submodules, you need to initialize and update the submodules after cloning the repository.
You can do this in one step or in two separate steps.
One-step cloning your project with submodules inside it
1
git clone --recurse-submodules <repository_url>
Two-step cloning
1
2git clone <repository_url>
cd <project_directory>and
1
git submodule update --init --recursive
Add a submodule
To add a submodule to your project, follow these steps:
Navigate to your project directory:
1
cd /path/to/your/project
Add the submodule:
1
git submodule add <repository_url> <submodule_path>
Initialize and update the submodule:
1
git submodule update --init --recursive
Commit the changes:
1
2git add .gitmodules <submodule_path>
git commit -m "Added submodule <submodule_path>
标签管理
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
Git有commit,为什么还要引入tag?
“请把上周一的那个版本打包发布,commit号是6a5819e...”
“一串乱七八糟的数字不好找!”
如果换一个办法:
“请把上周一的那个版本打包发布,版本号是v1.2”
“好的,按照tag v1.2查找commit就行!”
所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。
创建标签
在Git中打标签非常简单,首先,切换到需要打标签的分支上:
1 | $ git branch |
然后,敲命令git tag <name>
就可以打一个新标签:
1 | $ git tag v1.0 |
可以用命令git tag
查看所有标签:
1 | $ git tag |
默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的commit id,然后打上就可以了:
1 | $ git log --pretty=oneline --abbrev-commit |
比方说要对add merge
这次提交打标签,它对应的commit id是f52c633
,敲入命令:
1 | $ git tag v0.9 f52c633 |
再用命令git tag
查看标签:
1 | $ git tag |
注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>
查看标签信息:
1 | $ git show v0.9 |
可以看到,v0.9
确实打在add merge
这次提交上。
还可以创建带有说明的标签,用-a
指定标签名,-m
指定说明文字:
1 | $ git tag -a v0.1 -m "version 0.1 released" 1094adb |
用命令git show <tagname>
可以看到说明文字:
1 | $ git show v0.1 |
操作标签
如果标签打错了,也可以删除:
1 | $ git tag -d v0.1 |
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin <tagname>
:
1 | $ git push origin v1.0 |
或者,一次性推送全部尚未推送到远程的本地标签:
1 | $ git push origin --tags |
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:
1 | $ git tag -d v0.9 |
然后,从远程删除。删除命令也是push,但是格式如下:
1 | $ git push origin :refs/tags/v0.9 |
要看看是否真的从远程库删除了标签,可以登陆GitHub查看。
- 命令
git push origin <tagname>
可以推送一个本地标签; - 命令
git push origin --tags
可以推送全部未推送过的本地标签; - 命令
git tag -d <tagname>
可以删除一个本地标签; - 命令
git push origin :refs/tags/<tagname>
可以删除一个远程标签