Git教程合集
目录
文章目录
- Git教程合集
- 目录
- 分支 (branch)
- 什么是分支?
- 分支合并
- master分支
- 分支的运用
- Merge分支
- Topic分支
- 分支的切换
- HEAD
- stash
- 1. git stash 把现有的修改藏起来
- 2. 使用 ‘git stash save “desc”’ 把现有的修改藏起来,并且添加一个注释
- 3. git stash list 查看所有藏起来的
- 4. git stash pop --index {index} 删除 最近/指定 一个藏起来的,并还原回来代码
- 5. git stash apply --index {index} 从暂存区还原一个stash,但不从stash 列表中删除这个暂存
- 6. git stash drop [-q|--quiet] {index} 删除藏起来的不还原代码
- 7. git stash clear 清除所有修改
- 8. git stash show {index} 查看修改了什么文件
- 9. git stash show -p {index} 查看修改了什么文件,里面修改了什么内容
- 10.git stash 的总结
- 分支的合并
- topic分支和merge分支的运用实例
- 主分支
- 特性分支
- release分支
- hotFix分支
- shell操作分支
- 操作远程数据库
- 标签
- 修改最近的提交
- 取消过去的提交
- 遗弃提交
- 提取提交
- 改写提交的历史记录
- 汇合分支上的提交,然后一同合并到分支
- **revert 和 reset**
- **revert**
- reset
- cherry-pick 选择A分支一部分提交到B分支
- 1. git cherry-pick 合并其他分支的 commit - id 提交到当前分支,默认自动合并
- 2. git cherry-pick -n 或者 git cherry-pick --no-commit 合并其他分支的 commit - id 提交到当前分支,禁止自动提交
- 3. git cherry-pick -e 或 git cherry-pick -edit 合并其他分支的 commit - id 提交到当前分支,指定`cherr-pick`后重新编辑提交信息
- 4. git cherry-pick -x 在提交信息的末尾追加一行
- 5. git cherry-pick -s 或 ,git cherry-pick --signoff ** 在提交信息的末尾追加一行操作者的签名
- 6. git cherry-pick -m parent-number 或 git cherry-pick --mainline parent-number **
- 7. git cherry-pick --quit 合并过程中冲突后使用,当前分支中未冲突的内容状态将为modified
- 8. git cherry-pick --abort 合并过程中冲突后使用,当前分支恢复到cherry-pick前的状态,没有改变
- 9. git cherry-pick < branchname >
- 10.常见问题
- rebase
- rebase的使用场景
- rebase是一个危险的指令
- 几点总结
- **1. 永远不要rebase一个已经分享的分支(**到非remote分支,比如rebase到master,develop,release分支上)**,也就是说永远不要rebase一个已经在中央库中存在的分支.只能rebase你自己使用的私有分支**
- 2. 结论:只要你的分支上需要rebase的所有commits历史还没有被push过(比如上例中rebase时从分叉处开始有两个commit历史会被重写),就可以安全地使用git rebase来操作。
- 3. 上述结论可能还需要修正:对于不再有子分支的branch,并且因为rebase而会被重写的commits都还没有push分享过,可以比较安全地做rebase
- 代码演示
- 用rebase -i 汇合提交,合并多次提交记录
- 用rebase -i 修改提交
- merge --squash
- 提供一个好用的git模拟网站
分支 (branch)
什么是分支?
分支是为了将修改记录的整体流程分叉保存。分叉后的分支不受其他分支的影响,所以在同一个数据库里可以同时进行多个修改。
分支合并
-
分叉的分支可以合并。
为了不受其他开发人员的影响,您可以在主分支上建立自己专用的分支。完成工作后,将自己分支上的修改合并到主分支。因为每一次提交的历史记录都会被保存,所以当发生问题时,定位和修改造成问题的提交就容易多了。
master分支
创建好的空仓库在第一次执行提交后,Git会创建一个名为master的分支。因此之后的提交,在切换分支之前都会添加到master分支里。
分支的运用
在Git您可以自由地建立分支。但是,要先确定运用规则才可以有效地利用分支。
这里我们会介绍两种分支 (“Merge分支”和 “Topic分支” ) 的运用规则。
-
Merge分支
Merge分支是为了可以随时发布release而创建的分支,它还能作为Topic分支的源分支使用。保持分支稳定的状态是很重要的。如果要进行更改,通常先创建Topic分支,而针对该分支,可以使用Jenkins之类的CI工具进行自动化编译以及测试。
通常,大家会将master分支当作Merge分支使用。
-
Topic分支
Topic分支是为了开发新功能或修复Bug等任务而建立的分支。若要同时进行多个的任务,请创建多个的Topic分支。
Topic分支是从稳定的Merge分支创建的。完成作业后,要把Topic分支合并回Merge分支。
分支的切换
若要切换作业的分支,就要进行checkout操作。进行checkout时,git会从工作树还原向目标分支提交的修改内容。checkout之后的提交记录将被追加到目标分支。
-
HEAD
HEAD指向的是现在使用中的分支的最后一次更新。通常默认指向master分支的最后一次更新。通过移动HEAD,就可以变更使用的分支。
-
-
stash
还未提交的修改内容以及新添加的文件,留在索引区域或工作树的情况下切换到其他的分支时,修改内容会从原来的分支移动到目标分支。
但是如果在checkout的目标分支中相同的文件也有修改,checkout会失败的。这时要么先提交修改内容,要么用stash暂时保存修改内容后再checkout。
stash是临时保存文件修改内容的区域。stash可以暂时保存工作树和索引里还没提交的修改
1. git stash 把现有的修改藏起来
// 增加文件改动
git status //查看改动
git stash //暂存文件,暂存之后可以自由切换分支,进行开发等,暂存后打开文件查看文件已经被还原。内容已经被恢复到改动前的状态了。
git status //发现当前的工作区已经和线上一样了
git checkout release
//修改内容,模拟开发
git add .
git commit -m "开发了中秋节特效"
git checkout dev //假设现在发生了bug,需要切换暂存分支继续开发和修改
git stash pop //出栈,还原到最后一次提交的内容和状态,发现之前保存的内容又全部还原回来了
git status
2. 使用 ‘git stash save “desc”’ 把现有的修改藏起来,并且添加一个注释
git checkout dev //切换当前分支为dev分支
// 增加文件改动
git status //查看改动
git add . //添加改动的记录
git stash save "<msg>" //暂存文件,暂存之后可以自由切换分支,进行开发等,暂存后打开文件查看文件已经被还原。内容已经被恢复到改动前的状态了。可以输入自己需要的备注信息,方便多个暂存的时候,通过指令git stash list查看
// >> 到这一步骤可以发现之前add的改动消失了。但是被保存的改动仅限于我们通过add记录的变动
git status //发现当前的工作区已经和线上一样了,可见我们的修改就这样被保存了起来
git checkout release
//修改内容,模拟开发
git add .
git commit -m "开发了中秋节特效"
git checkout dev //假设现在发生了bug,需要切换暂存分支继续开发和修改
git stash pop //出栈,还原到最后一次提交的内容和状态,发现之前保存的内容又全部还原回来了
git status
程序执行到add 的时候是这样的
stash暂存需要在add状态之后执行才会生效,在stash暂存执行完成后,分支的状态变成和改动前一样了,也就是暂存的改动部分已经被保存在了文件的暂存区了,这个暂存内容的清理可以同git reset --soft 来完成
我们可以在这个时候切换分支,去修改线上比较着急的bug。
当然bug 修改完成了,dev分支的需求可以接着开发了
当你发现自己在当前dev分支开发的功能可能需要在release分支提交,那怎么办~
很神奇吧,dev分支开发的改动,通过stash被迁移到release分支了
3. git stash list 查看所有藏起来的
git stash list //查看当前分支所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
//添加第一次修改 Modify game rules~ V1.0.0
git add . //增加改动到版本记录
git stash save "Modify game rules~ V1.0.0" //添加暂存
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
//添加第二次修改,新增文件stashfile_2
git add . //增加改动到版本记录
git stash save "stashfile_2" //添加暂存
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
//添加第三次修改,新增文件stashfile_3
git add . //增加改动到版本记录
git stash save "stashfile_3" //添加暂存
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
输出:
stash@{0}: On dev: stashfile_3
stash@{1}: On dev: stashfile_2
stash@{2}: On release: Modify game rules~ V1.0.0
操作如下
4. git stash pop --index {index} 删除 最近/指定 一个藏起来的,并还原回来代码
git stash pop --index <stash_index> //将本地仓库中指定指定索引的暂存提取出来
示例如下
//接着上面的例子
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
git stash pop --index <stash_index> //将本地仓库中指定指定索引的暂存提取出来,并且将对应的暂存出栈,我们这里以提取索引为1的暂存为例子了
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
5. git stash apply --index {index} 从暂存区还原一个stash,但不从stash 列表中删除这个暂存
git stash list //查看当前分支所有暂存的列表
示例
git stash save "stashfile_2" //存储 stashfile_2 暂存
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
git stash apply --index <stash_index> //将本地仓库中指定指定索引的暂存提取出来,本地暂存不从缓存区出栈,暂存还有继续保留,直到我们手动删除,我们这里以提取索引为1的暂存为例子了
git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
6. git stash drop [-q|–quiet] {index} 删除藏起来的不还原代码
示例
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash apply -q <stash_index> //将本地仓库中指定指定索引的暂存删除并且不还原这个暂存git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
7. git stash clear 清除所有修改
git stash clear //清空当前本地仓库的暂存区中的所有暂存记录
示例
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash clear //清空当前本地仓库的暂存区中的所有暂存记录git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
8. git stash show {index} 查看修改了什么文件
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash show <stash_index> //查看指定索引为<stash_index>暂存所修改的文件git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
示例
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash show 1 //查看指定索引为 1 暂存所修改的文件git stash show 0 //查看指定索引为 0 暂存所修改的文件git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
9. git stash show -p {index} 查看修改了什么文件,里面修改了什么内容
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash show -p <stash_index> //查看指定索引为<stash_index>暂存所修改的内容git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
示例
git stash list //查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面git stash show -p 1 //查看指定索引为 1 暂存所修改的内容git stash show -p 0 //查看指定索引为 0 暂存所修改的内容git stash list//查看当前本地仓库所有暂存的列表,注意一点,stash是保存在一个栈结构当中的。所有后保存的会在前面
运行结果
10.git stash 的总结
常用git stash命令:
(1)git stash save “save message” : 执行存储时,添加备注,方便查找,只有git stash 也要可以的,但查找时不方便识别。
(2)git stash list :查看stash了哪些存储
(3)git stash show :显示做了哪些改动,默认show第一个存储,如果要显示其他存贮,后面加stash@{$num},比如第二个 git stash show stash@{1}
(4)git stash show -p : 显示第一个存储的改动,如果想显示其他存存储,命令:git stash show stash@{$num} -p ,比如第二个:git stash show stash@{1} -p
(5)git stash apply :应用某个存储,但不会把存储从存储列表中删除,默认使用第一个存储,即stash@{0},如果要使 其他个,git stash apply stash@{KaTeX parse error: Expected 'EOF', got '}' at position 4: num}̲ , 比如第二个:git st…num} ,比如应用并删除第二个:git stash pop stash@{1}
(7)git stash drop stash@{KaTeX parse error: Expected 'EOF', got '}' at position 4: num}̲ :丢弃stash@{num}存储,从列表中删除这个存储
(8)git stash clear :删除所有缓存的stash
分支的合并
完成作业后的topic分支,最后要合并回merge分支。合并分支有2种方法:使用merge或rebase。使用这2种方法,合并后分支的历史记录会有很大的差别。
merge
如下图所示,bugfix分支是从master分支分叉出来的。
合并 bugfix分支到master分支时,如果master分支的状态没有被更改过,那么这个合并是非常简单的。 bugfix分支的历史记录包含master分支所有的历史记录,所以通过把master分支的位置移动到bugfix的最新分支上,Git 就会合并。这样的合并被称为fast-forward(快进)合并。
但是,master分支的历史记录有可能在bugfix分支分叉出去后有新的更新。这种情况下,要把master分支的修改内容和bugfix分支的修改内容汇合起来。
因此,合并两个修改会生成一个提交。这时,master分支的HEAD会移动到该提交上。
rebase
跟merge的例子一样,如下图所示,bugfix分支是从master分支分叉出来的。
如果使用rebase方法进行分支合并,会出现下图所显示的历史记录。现在我们来简单地讲解一下合并的流程吧。
首先,rebase bugfix分支到master分支, bugfix分支的历史记录会添加在master分支的后面。如图所示,历史记录成一条线,相当整洁。
这时移动提交X和Y有可能会发生冲突,所以需要修改各自的提交时发生冲突的部分。
rebase之后,master的HEAD位置不变。因此,要合并master分支和bugfix分支,即是将master的HEAD移动到bugfix的HEAD这里。
topic分支和merge分支的运用实例
我们用简单的实例来讲解topic分支和merge分支的操作方法。
例如,在开发功能的topic分支操作途中,需要修改bug
这时,merge分支还是处于开发功能之前的状态。在这里新建修改错误用的主题分支,就可以从开发功能的作业独立出来,以便开始新的工作。
完成bug修正的工作后,把分支导入到原本的merge分支后就可以公开了。
回到原本的分支继续进行开发功能的操作。
但是,如果要继续进行操作,你会发现需要之前修正bug时提交X的内容。有2种导入提交X的内容的方法:一种是直接merge,另一种是和rebase导入提交X的合并分支。
这里我们使用rebase合并分支的方法。
在导入提交X的内容的状态下继续进行开发功能。
-
作为Git的分支的用例 ,这里介绍 A successful Git branching model
原文:
http://nvie.com/posts/a-successful-git-branching-model/这个用例主要分为
- 主分支
- 特性分支
- release分支
- hotFix分支
分别使用4个种类的分支来进行开发的。
主分支
主分支有两种:master分支和develop分支
- master
master分支只负责管理发布的状态。在提交时使用标签记录发布版本号。 - develop
develop分支是针对发布的日常开发分支。刚才我们已经讲解过有合并分支的功用。
特性分支
特性分支就是我们在前面讲解过的topic分支的功用。
这个分支是针对新功能的开发,在bug修正的时候从develop分支分叉出来的。基本上不需要共享特性分支的操作,所以不需要远端控制。完成开发后,把分支合并回develop分支后发布。
release分支
release分支是为release做准备的。通常会在分支名称的最前面加上release-。release前需要在这个分支进行最后的调整,而且为了下一版release开发用develop分支的上游分支。
一般的开发是在develop分支上进行的,到了可以发布的状态时再创建release分支,为release做最后的bug修正。
到了可以release的状态时,把release分支合并到master分支,并且在合并提交里添加release版本号的标签。
要导入在release分支所作的修改,也要合并回develop分支。
hotFix分支
hotFix分支是在发布的产品需要紧急修正时,从master分支创建的分支。通常会在分支名称的最前面加上 hotfix-。
例如,在develop分支上的开发还不完整时,需要紧急修改。这个时候在develop分支创建可以发布的版本要花许多的时间,所以最好选择从master分支直接创建分支进行修改,然后合并分支。
修改时创建的hotFix分支要合并回develop分支。
shell操作分支
- 如何创建本地仓库
mkdir gittest //创建repositorycd gittest/ //进入这个文件夹 git init已初始化空的 Git 仓库于 /home/zbc/gittest/.git/
- 添加文件到git
git add xxxx // 添加单个文件git add . // 添加当前目录下的所有未在版本记录或者有改动的文件
- 提交代码到本地仓库
//提交记录的附加信息简单可以使用第一种方式git commit -m "这里添加需要提交的备注信息"//如果提交的备注信息有比较复杂的格式信息,需要使用第二种方式git commit -s //首次提交,此时会进入vim编辑器,编辑自己需要提交的备注信息git commit --amend //此时会进入vim编辑器,编辑自己需要提交的备注信息,这种提交方式会将我们上一次的commit信息抹除,在我们push远端的时候只会显示最后一次的commit。
- 创建本地分支
git branch <分支名称> //创建制定分支名称的分支,注意如果仓库是本地创建出来的,需要通过commit创建master分支后才可以创建其他名称的分支
- 查看本地和远程分支
git branch //列举所有本地分支git branch remote //列举所有远程分支git branch -a //列举本地分支和所有远程分支
- 切换分支
git checkout <期望切换到的分支名称,目标分支必须存在> //分支切换后,如果当前分支有未提交的代码将会被保留到新的分支上,如果同时修改了一个地方可能会导致冲突,如果希望当前分支的改动被带到行的分支上可以收stash暂存当前分支未提交的改动git checkout -b <branch> //在checkout命令指定 -b选项执行,可以创建分支并进行切换。
新创建的仓库史记录是这样的。
通过执行如下代码
$ git add myfile.txt$ git commit -m "添加add的说明"
目前的历史记录是这样的。
git stash
分支暂存
很多时候,我们在开发一个feature功能的时候,会遇到线上的各种bug,这个时候我们需要停下手中正在开发的功能优先去解bug;当然还有很多情况,我们在当前hotfix分支解决一个bug,但是这个时候其他业务线的同学可能会找你联调一个功能,这时候的你可以选择新开一个branch,当然你喜欢的话也可以重新拉一份代码,但是我觉的,还有一个更简单的,就是通过git stash的功能,完成这个任务
向master分支合并issue1分支的修改。
执行merge命令以合并分支。
$ git merge <commit>
该命令将指定分支导入到HEAD指定的分支。先切换master分支,然后把issue1分支导入到master分支。
$ git checkout master //切换到master分支Switched to branch 'master'
打开readme.txt档案以确认内容,然后提交。
已经在issue1分支进行了编辑上一页的档案,所以master分支的readme.txt的内容没有更改。
$ git merge issue1Updating 1257027..b2b23c4Fast-forward readme.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
master分支指向的提交移动到和issue1同样的位置。这个是fast-forward(快进)合并。
- 删除分支
$ git branch -d <branchname> //删除指定分支
- 并行操作
接下来,创建2个分支来尝试并行操作吧。
首先创建issue2分支和issue3分支,并切换到issue2分支。
$ git branch issue2$ git branch issue3$ git checkout issue2Switched to branch 'issue2'$ git branch* issue2 issue3 master
在issue2分支的readme.txt添加commit命令的说明后提交。
$ git add readme.txt //add 把变更录入到索引中$ git commit -m "添加commit的说明" //commit 记录索引的状态
接着,切换到issue3分支。
$ git checkout issue3 //切换分支
打开readme.txt档案。由于在issue2分支添加了commit命令的说明,所以issue3分支的readme.txt里只有add命令的说明。
添加pull命令的说明后提交。
$ git add readme.txt //add 把变更录入到索引中$ git commit -m "添加pull的说明" // pull 取得远端数据库的内容
这样,添加commit的说明的操作,和添加pull的说明的操作就并行进行了。
把issue2分支和issue3分支的修改合并到master。
切换master分支后,与issue2分支合并。
$ git checkout master //切换指定分支$ git merge issue2 //将指定分支的代码合并到当前的分支上
执行fast-forward(快进)合并。
接着合并issue3分支。
$ git merge issue3
自动合并失败。由于在同一行进行了修改,所以产生了冲突。这时readme.txt的内容如下:
add 把变更录入到索引中<<<<<<< HEADcommit 记录索引的状态=======pull 取得远端数据库的内容>>>>>>> issue3
- 代码冲突的修改
修改冲突的部分,重新提交。
$ git add readme.txt$ git commit -m "合并issue3分支"
历史记录如下图所示。因为在这次合并中修改了冲突部分,所以会重新创建合并修改的提交记录。这样,master的HEAD就移动到这里了。这种合并不是fast-forward合并,而是non fast-forward合并。
合并issue3分支的时候,使用rebase可以使提交的历史记录显得更简洁。
现在暂时取消刚才的合并。
$ git reset --hard HEAD~ //将当前分支回退到上一个版本
切换到issue3分支后,对master执行rebase。
$ git checkout issue3$ git rebase master
会报错,代码合并过程中发生冲突,没有关系,解决冲突然后合并
和merge时的操作相同,修改发生冲突的文件。
add 把变更录入到索引中<<<<<<< HEADcommit 记录索引的状态=======pull 取得远端数据库的内容>>>>>>> issue3
rebase的时候,修改冲突后的提交不是使用commit命令,而是执行rebase命令指定 --continue选项。若要取消rebase,指定 --abort选项。
$ git add .$ git rebase --continue
这样,在master分支的issue3分支就可以fast-forward合并了。切换到master分支后执行合并。
$ git checkout master //切换master分支$ git merge issue3 //合并issue3分支代码到当前分支
gradle.property的最终内容和merge是一样的,但是历史记录如下。
操作远程数据库
pull 拉取
基本指令
git fetch //此命令可以将所有的本地分支从远程数据库中拉取最新的历史记录,同步并更新到一致的状态。更新后的代码会保存的本地数据库的remotes/分支中,这个命令是用在想将远程分支数据库状态全部同步到本地数据库但是又不想合并到本地分支的情况git merge remotes/origin/dev //使用merge可以同本地远程分支的记录合并到本地分支中
我们执行pull可以取得远程数据库的历史记录。接下来,我们用图来讲解数据库提交的细节。
首先确认更新的本地数据库分支没有任何的更改。
这时只执行fast-forward合并。图中的master是本地数据库的master分支,origin/master是远程数据库的origin的master分支。
如果本地数据库的master分支有新的历史记录,就需要合并双方的修改。
执行pull就可以进行合并。这时,如果没有冲突的修改,就会自动创建合并提交。如果发生冲突的话,要先解决冲突,再手动提交。
- 记住这句话就可以了
push
从本地数据库push到远程数据库时,要fast-forward合并push的分支。如果发生冲突,push会被拒绝的。
若要共享在本地数据库创建的分支,需要明确的push。因此,没有执行push就不会给远程数据库带来影响,因而可以自由的创建自己的本地分支。
本地分支推送远程
git push // 将当前的本地分支推送远程分支数据库git push --set-upstream origin dev //推送一个远程不存在的本地分支,git将会同步在远端创建对应的分支来保存代码
标签
标签是为了更方便地参考提交而给它标上易懂的名称。
Git可以使用2种标签:轻标签和注解标签。打上的标签是固定的,不能像分支那样可以移动位置。
- 轻标签
- 添加名称
- 注解标签
- 添加名称
- 添加注解
- 添加签名
一般情况下,发布标签是采用注解标签来添加注解或签名的。轻标签是为了在本地暂时使用或一次性使用。
基本命令
操作标签
-
操作之前,我们使用git命令创建一个本地git仓库代码如下
-
git init //初始化本地git仓库git add . //文件添加到git管理,这一步骤前考入需要管理的文件git commit -m "首次提交" //
1. 添加轻标签
-
轻量级的标签无法为标签增加注释信息,这种标签在后期维护的时候随着标签的增加,可能变得不好维护,推荐使用注解标签
-
使用tag命令来添加标签,在执行标签的名称。
$ git tag <tagname>
在HEAD指向的提交里添加名为apple的标签,请执行以下的命令。
$ git tag apple
如果没有使用参数而执行tag,可以显示标签列表。
$ git tag //仅仅列举所有的标签,但是不会列举出标签的注释信息
-
如果在log命令添加 --decorate选项执行,可以显示包含标签资料的历史记录。
$ git log --decorate //详细的列举各个标签的信息 和 代码的提交信息
运行结果
当前的分支情况如下
2. 添加注解标签
若要添加注解标签,可以在tag命令指定 -a选项执行。执行后会启动编辑区,请输入注解,也可以指定-m选项来添加注解。
$ git tag -a <tagname> //推荐使用者种方式
在HEAD指向的提交里添加名为banana的标签,请执行以下的命令。
$ git tag -am "增加注解标签" banana // 如果增加的标签注释内容不是很长,格式也不是很复杂,可以考虑使用这种
如果在tag命令指定-n选项执行,可以显示标签的列表和注解。
$ git tag -n //列举所有的标签,同时展示标签的注释信息
执行结果如下
当前的分支情况如下
3. 删除标签
若要删除标签,在tag命令指定 -d选项执行。
$ git tag -d <tagname>
修改最近的提交
难度 :
指定amend选项执行提交的话,可以修改同一个分支最近的提交内容和注解。
代码
git --amend //修改最近的一笔提交,如果是多笔请使用git reset --mixed 合并成一笔
主要使用的场合:
- 添加最近提交时漏掉的档案
- 修改最近提交的注解
取消过去的提交
难度 :
在revert可以取消指定的提交内容。使用后面要提到的rebase -i或reset也可以删除提交。但是,不能随便删除已经发布的提交,这时需要通过revert创建要否定的提交。
主要使用的场合:
- 安全地取消过去发布的提交
遗弃提交
难度 :
在reset可以遗弃不再使用的提交。执行遗弃时,需要根据影响的范围而指定不同的模式,可以指定是否复原索引或工作树的内容。
除了默认的mixed模式,还有soft和hard模式。欲了解受各模式影响的部分,请参照下面的表格。
模式名称 | HEAD的位置 | 索引 | 工作树 |
---|---|---|---|
soft | 修改 | 不修改 | 不修改 |
mixed | 修改 | 修改 | 不修改 |
hard | 修改 | 修改 | 修改 |
主要使用的场合:
- 复原修改过的索引的状态(mixed)
- 彻底取消最近的提交(hard)
- 只取消提交(soft)
提取提交
难度 :
在cherry-pick,您可以从其他分支复制指定的提交,然后导入到现在的分支。
主要使用的场合:
- 把弄错分支的提交移动到正确的地方
- 把其他分支的提交添加到现在的分支
改写提交的历史记录
难度 :
在rebase指定i选项,您可以改写、替换、删除或合并提交。
主要使用的场合:
- 在push之前,重新输入正确的提交注解
- 清楚地汇合内容含义相同的提交。
- 添加最近提交时漏掉的档案
改写提交
汇合分支上的提交,然后一同合并到分支
难度 :
我们介绍一下merge的特殊选项:squash (使)挤进;塞入;
用这个选项指定分支的合并,就可以把所有汇合的提交添加到分支上。
主要使用的场合:
- 汇合主题分支的提交,然后合并提交到目标分支。
1. 使用 commit --amend 改写提交
-
还是服用上一步骤的本地仓库
-
使用如下命令查看本地仓库的日志
git log //查看本地提交日志
-
修改仓库中的文件,使文件有变化
-
执行如下命令
git add . //添加变化到仓库记录git commit --amend //使用命令修改提交,随后修改或确认提交的信息,这个信息在push到远端数据库之前以最后一次的修改为准
-
执行结果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPL1FkoU-1630855221114)(/home/zbc/.config/Typora/typora-user-images/image-20210816113435222.png)]
可以看到我们上一次的本地git仓库的提交记录已经被成功改写了,如果你的提交,已经被push到远端分支了,这时候如果要同步你的代码和记录到远端分支,就需要重新push记录到远端
具体步骤如下
//修改你使产生差异git add . //添加文件到版本记录git commit --amend //修改上一笔提交git push //推送代码到远端分支,当然,这一步极有可能报错//如果推动远端提示 ! [rejected] dev -> dev (non-fast-forward)error: 推送一些引用到 'http://hass.898311.xyz:30000/zbc/gitdemo.git' 失败提示:更新被拒绝,因为您当前分支的最新提交落后于其对应的远程分支。提示:再次推送前,先与远程变更合并(如 'git pull ...')。详见提示:'git push --help' 中的 'Note about fast-forwards' 小节。//可以使用git push --force-with-lease origin <远端分支名称>//你还可能遇到这个命令也解决不了的情况,这种一般需要解决远端仓库分支保护//可以参考: https://github.com/LeachZhou/blog/issues/11
通过远程仓库可验证,上一个提交记录被抹除
revert 和 reset
- 关于revert和reset网上有一片不错的文章,值得参考一下 https://blog.youkuaiyun.com/yxlshk/article/details/79944535
- 其实说了很多最核心的理解就是下面这两张图
- 这个是revert前后的分支记录比对
- 下面这个是reset前后的分支比对
revert
//基于对应分支的HEAD进行修改,重做指定chang的提交git add . //将修改添加到git库git revert HEAD //撤销当前分支的最后一笔commitgit revert HEAD^ //撤销当前分支的倒数第二笔提交git revert <commit-id> //撤销当前分支指定commit的提交//撤销连续的若干笔提交 ;l 使用该命令可以将提交撤回到commit_id_start的位置git revert -n <commit-id_start>..<commit-id_end>//撤销非连续的若干笔提交 ; 使用该命令可以撤回到commit_id_1和commit_id_10的提交git revert -n <commit-id_1>git revert -n <commit-id_10>//revert回退过程中如果有冲突,需要解决冲突然后依次执行 git add . ; git commitgit add . //标记修正后的文件git commit //保存冲突状态已经解决完成//如果需要同时更改远端数据库,需要使用git push //提交,如果提交报错,不是最新远端有更高的版本 需要使用如下命令解决git push -f //强制推动的第二种方式git push --force-with-lease origin master
注意: git revert是提交一个新的版本,将需要revert的版本的内容再反向修改回去,版本会递增,不影响之前提交的内容.
Tip : 通常情况下,上面这条revert命令会让程序员修改注释,这时候程序员应该标注revert的原因,假设程序员就想使用默认的注释,可以在命令中加上-n或者–no-commit,应用这个参数会让revert 改动只限于程序员的本地仓库,而不自动进行commit,如果程序员想在revert之前进行更多的改动,或者想要revert多个commit。
-
代码演示
执行如下命令后
git revert HEADgit push -fgit revert -n ad3f96952b9568901b7c18ec55f18cc51e379201 //撤回指定一笔提交git push -fgit log
查看记录
代码已经还原到指定位置了
不难理解revert就是将指定commit的代码去除掉,并且这次还原的会有提交记录,你可以理解为异常普通的commit动作,只不过这个动作会被标注为revert,这样做的优点是会保留完成的操作记录,方便日后的查找。
有种特殊情况,revert的过程中产生了冲突怎么解决,其实和merge一样,本地解决,然后通过
git add . //改之改动范围 git commit //提交改动到数据库 git push //推动远端分支 // 如果推动失败可以通过 git push -f //执行强制推送
reset
Git reset 命令有三个主要选项:Git reset 命令有三个主要选项:git reset --soft; git reset --mixed; git reset --hard;
git reset --soft
将HEAD引用指向给定提交。索引(暂存区)和工作目录的内容是不变的,在三个命令中对现有版本库状态改动最小。
git reset --mixed(git reset默认的模式)
HEAD引用指向给定提交,并且索引(暂存区)内容也跟着改变,工作目录内容不变。这个命令会将索引(暂存区)变成你刚刚暂存该提交全部变化时的状态,会显示工作目录中有什么修改。
git reset --hard
HEAD引用指向给定提交,索引(暂存区)内容和工作目录内容都会变给定提交时的状态。也就是在给定提交后所修改的内容都会丢失(新文件会被删除,不在工作目录中的文件恢复,未清除回收站的前提)。
用表格看起来会更清楚些:
Git reset产生的影响 | |||
---|---|---|---|
选项 | HEAD | 索引 | 工作树 |
–soft | 是 | 否 | 否 |
–mixed | 是 | 是 | 否 |
–hard | 是 | 是 | 是 |
在说这个问题之前我们先了解下git的Working Tree 、Index/Stage 、Repository,下面这张图描述了Working Tree 、Index/Stage 、Repository ** 与 –soft、–mixed**、–hard
这三个模式理解了,对于使用这个命令很有帮助。在理解这三个模式之前,需要略微知道一点Git的基本流程。正如上图,Git会有三个区域:
- Working Tree 当前的工作区域
- Index/Stage 暂存区域,和git stash命令暂存的地方不一样。使用git add xx,就可以将xx添加近Stage里面
- Repository 提交的历史,即使用git commit提交后的结果
以下简单敘述一下把文件存入Repository流程:
-
刚开始 working tree 、 index 与 repository(HEAD)里面的內容都是一致的
-
当git管理的文件夹里面的内容出现改变后,此時 working tree 的內容就会跟 index 及 repository(HEAD)的不一致,而Git知道是哪些文件(Tracked File)被改动过,直接将文件状态设置为 modified (Unstaged files)。
-
当我們执行 git add 后,会将这些改变的文件內容加入 index 中 (Staged files),所以此时working tree跟index的內容是一致的,但他们与repository(HEAD)內容不一致。
-
接着执行 git commit 後,將Git索引中所有改变的文件內容提交至 Repository 中,建立出新的 commit 节点(HEAD)后, working tree 、 index 與与repository(HEAD)区域的内容 又会保持一致。
有了上面的铺垫,紧接着就上手实操下:
1. reset --soft:保留工作目录,并把重置 HEAD 所带来的新的差异放进暂存区
reset --soft 会在重置 HEAD 和 branch 时,保留工作目录和暂存区中的内容,并把重置 HEAD 所带来的新的差异放进暂存区。
什么是「重置 HEAD 所带来的新的差异」?就是这里:
由于 HEAD 从 4 移动到了 3,而且在 reset 的过程中工作目录和暂存区的内容没有被清理掉,所以 4 中的改动在 reset 后就也成了工作目录新增的「工作目录和 HEAD 的差异」。这就是上面一段中所说的「重置 HEAD 所带来的差异」。
此模式下会保留 working tree工作目录的內容,不会改变到目前所有的git管理的文件夹的內容;也会
保留 index暂存区的內容,让 index 暂存区与 working tree 工作目录的內容是一致的。就只有 repository 中的內容的更变需要与 reset 目标节点一致,因此原始节点与reset节点之间的差异变更集合会存在与index暂存区中(Staged files),所以我们可以直接执行 git commit 將 index暂存区中的內容提交至 repository 中。当我们想合并「当前节点」与「reset目标节点」之间不具太大意义的 commit 记录(可能是阶段性地频繁提交)時,可以考虑使用 Soft Reset 来让 commit 演进线图较为清晰点。
a. 准备一个目录仓库查看目前的提交记录 git log
git log
b. 通过git status查看本地状态
git status
c.修改本地的settings.gradle文件,是发生变化
d.通过 git add . 添加文件到本地暂存区
git add .
e.通过git reset --soft 撤销缓存区暂存
git reset --soft git status //重新查询本地状态
最终发现刚刚add到暂存区中的文件被还原了~,到这里你是不是明白了,加入我们add了Android项目中应该被忽略的文件,如 .idea文件等,可以通过这个来还原。
注意:已经通过commit提交到repository的记录将不受影响
相关指令
git reset --softgit show --stat //显示最新的提交日志详情git show //显示全部的提交日志
做另一个尝试执如下命令
git add . //添加改动的文件到暂存区 git commit -m "测试git reset --soft" //提交改动到本地repository git show --stat //查看最近一次提交详情 git reset --soft //回退本地暂存区的记录 git show --stat //查看最近一次提交详情
执行结果如下
2. reset 不加参数(mixed):保留工作目录,并清空暂存区
reset 如果不加参数,那么默认使用 –mixed 参数。它的行为是:保留工作目录,并且清空暂存区。也就是说,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作目录。简而言之,就是「把所有差异都混合(mixed)放在工作目录中」。
接着上面的步骤测试(本地有一笔改动,已通过commit推送本地repository但是还没有推送到远端):
执行如下命令:
//模拟改动已提交到本地的情况git add .git commit -m "9月分提交"//验证已经推动本地repositorygit statusgit show --stat //到这是为了验证本地已经将变更提交本地repository//使用指令撤回提交git reset --mixed HEADgit show --stat //到这里得出结论, git reset --mixed <commit-id> 指令是将HEAD回退到指定commit-id的位置,并且会回退本地暂存区的改动git reset --mixed HEAD^ //将HEAD回退到倒数第二笔提交的位置,回退后本地工作树中的内容不变,改变将会被变更成改动保留在本地gitgit show --stat
有这个得出结论:
git reset --mixed 指令是将commit的记录的HEAD变更到指定的commit的位置。并且将add到暂存区中的记录还原,但是本地工作树没有改变。所有的改动将生成差异到本地git。说人话,就是回退了所有本地add的操作和并且将线上commit的记录回退到 位置,本地文件不会被回退,还是保留了所有,所有和线上最新代码的差异将会已差异的形式保存本地数据库。
当我们回退到指定版本后,我们可以通过认为的舍弃或者修改后重新进行add . -> commit -> push,获得最终的目标HEAD代码
3. reset --hard:重置stage区和工作目录:
reset --hard 会在重置 HEAD 和branch的同时,重置stage区和工作目录里的内容。当你在 reset 后面加了 –hard 参数时,你的stage区和工作目录里的内容会被完全重置为和HEAD的新位置相同的内容。换句话说,就是你的没有commit的修改会被全部擦掉。
下面我们来做一个演示,还是来修改 setting.gradle 文件
-
准备阶段
-
git add .git commit -m "//10月提交"git push //推动到远程仓库//实验第二步,继续修改 **setting.gradle** 增加 "//11月提交 " 这一句,然后仅仅执行add,commit,这一步主要验证是否会擦除我们提交到本地仓库的内容和记录git add .git commit -m "//11月提交"//实验第三步,继续修改 **setting.gradle** 增加 "//12月提交 " 这一句,然后仅仅执行add,这一步主要验证是否会擦除我们添加到暂存区的记录git add .//执行git status 查看状态git status//将本地的改动推动到远端git push -f
查看本地分支的状态
同时验证远程仓库记录push的最新状态
-
-
验证阶段
-
//执行reset命令git reset --hard <commit-id>//查看分支状态git status//查看分支操作日志git log//将状态同步远程分支git push -f
命令查看本地日志
查看本地文件修改,发现本地的修改记录不论是add到暂存区,commit到本地repository,还是推送到远程分支的记录都被还原了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t71NlDfL-1630855221122)(/home/zbc/.config/Typora/typora-user-images/image-20210823140023357.png)]
-
执行过git push -f后查看远端仓库的变动,发现远端分支的操作也被还原到指定版本了
总结
-
你的 HEAD 和当前 branch 切到上一条commit 的同时,你工作目录里的新改动和已 经add到stage区的新改动也一起全都消失了:
-
可以看到,在 reset --hard 后,所有的改动都被擦掉了。
-
cherry-pick 选择A分支一部分提交到B分支
git cherry-pick可以理解为"挑拣"提交,它会获取某一个分支的单笔提交,并作为一个新的提交引入到你当前分支上。 当我们需要在本地合入其他分支的提交时,如果我们不想对整个分支进行合并,而是只想将某一次提交合入到本地当前分支上,那么就要使用git cherry-pick了。
对于多分支的代码库,将代码从一个分支转移到另一个分支是常见需求。
这时分两种情况。
- 你需要另一个分支的所有代码变动,那么就采用合并(git merge)。
- 你只需要部分代码变动(某几个提交),这时可以采用 Cherry pick。
Cherry pick直译为捡樱桃🍒,它的功能是把已经存在的commit进行挑选,然后重新提交。比较合适的一个场景是把A分支的某次或者多次的提交也提交到B分支上,使用方法
git cherry-pick [<options>] <commit-ish>...常用options: --quit 退出当前的chery-pick序列 --continue 继续当前的chery-pick序列 --abort 取消当前的chery-pick序列,恢复当前分支 -n, --no-commit 不自动提交 -e, --edit 编辑提交信息
1. git cherry-pick <commit - id> 合并其他分支的 commit - id 提交到当前分支,默认自动合并
在本地仓库中,有两个分支:dev和release,我们先来查看各个分支的提交:
// 切换到dev分支git checkout dev# 查看最近四次提交git log --oneline -4# 切换到release分支git checkout release# 查看最近四次提交git log --oneline -3
操作如下:
现在,我想要将dev分支上的第一次提交内容合入到release分支上,则可以使用git cherry-pick
命令:
git checkout release //首先需要切换到合并的目标分支git cherry-pick <commit-id> //合并指定提交到目标分支,如果合并成功将会自动提交,如果合并失败了需要先解决冲,解决冲突后需要git commit手动进行提交//如果没有出现冲突,cherry-pick 命令将自动提交。//如果发生冲突,需要先解决冲突,然后执行如下指令//方案一git add . //标记冲突解决git commit //这个不需要添加备注信息git push //推送到远端//方案二git add . //标记冲突解决git cherry-pick --continue //继续剪出合并git push //推送远端分支
这个是dev分支的一笔提交
这个演示的是将dev分支的这笔提交合并到release分支当中,这个是发生了合并冲突的解决流程
如果没有发生冲突会是这样的
可以发现合并完成后没有冲突的话是自动提交的如果我们不期望自动提交怎么版呢?
2. git cherry-pick -n <commit - id> 或者 git cherry-pick --no-commit <commit - id> 合并其他分支的 commit - id 提交到当前分支,禁止自动提交
//将指定分支dev中的单笔提交合并到当前分支relesegit checkout release //切换工作分支为releasegit pull //同步远程代码git cherry-pick -n <commit - id> //和并远程dev分支指定提交到当前工作分支releasegit status //验证工作树中的代码状态git commit //提交本次改动到本地repositorygit push //推送代码到远程分支
3. git cherry-pick -e <commit - id> 或 git cherry-pick -edit <commit - id>合并其他分支的 commit - id 提交到当前分支,指定cherr-pick
后重新编辑提交信息
//将指定分支dev中的单笔提交合并到当前分支relesegit checkout release //切换工作分支为releasegit pull //同步远程代码git cherry-pick -e <commit - id> //和并远程dev分支指定提交到当前工作分支release,并编辑提交信息git status //验证工作树中的代码状态git push //推送代码到远程分支
4. git cherry-pick -x <commit - id> 在提交信息的末尾追加一行
在提交信息的末尾追加一行(cherry picked from commit ...)
,保留原提交者信息,方便以后查到这个提交是如何产生的。
5. git cherry-pick -s <commit - id> 或 ,git cherry-pick --signoff <commit - id>** 在提交信息的末尾追加一行操作者的签名
在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。
6. git cherry-pick -m parent-number <commit - id> 或 git cherry-pick --mainline parent-number <commit - id>**
如果原始提交是一个合并节点,来自于两个分支的合并,那么 Cherry pick 默认将失败,因为它不知道应该采用哪个分支的代码变动。
-m配置项告诉 Git,应该采用哪个分支的变动。它的参数parent-number是一个从1开始的整数,代表原始提交的父分支编号。
git cherry-pick -m 1 <commitHash>
上面命令表示,Cherry pick 采用提交commitHash来自编号1的父分支的变动。
一般来说,1号父分支是接受变动的分支(the branch being merged into),2号父分支是作为变动来源的分支(the branch being merged from)。
7. git cherry-pick --quit 合并过程中冲突后使用,当前分支中未冲突的内容状态将为modified
这时如果要继续cherry-pick,则首先需要解决冲突,通过git add .将文件标记为已解决,然后可以使用git cherry-pick --continue命令,继续进行cherry-pick操作。
如果要中断这次cherry-pick,则使用git cherry-pick --quit,这种情况下当前分支中未冲突的内容状态将为modified
可以发现没有冲突的部分已经自动commit,但是冲突的部分需要没有add到工作目录的暂存区
8. git cherry-pick --abort 合并过程中冲突后使用,当前分支恢复到cherry-pick前的状态,没有改变
如果要取消这次cherry-pick,则使用git cherry-pick --abort,这种情况下当前分支恢复到cherry-pick前的状态,没有改变。
代码被还原到未合并的当前分支节点了。
9. git cherry-pick < branchname >
如果在git cherry-pick后加一个分支名,则表示将该分支顶端提交进cherry-pick,如:
git cherry-pick dev //合并dev的HEAD提交到当前分支git cherry-pick …< branchname > //合并< branchname > 的指定提交到当前分支git cherry-pick ^HEAD < branchname > //合并< branchname > 的倒数第二笔提交到当前分支
以上两个命令作用相同,表示应用所有提交引入的更改,这些提交是branchname的祖先但不是HEAD的祖先,比如,现在我的仓库中有三个分支,其提交历史如下图:
C<---D<---E branch2 /master A<---B \ F<---G<---H branch3 | HEAD
如果我使用git cherry-pick …branch2或者git cherry-pick ^HEAD branch2,那么会将属于branch2的祖先但不属于branch3的祖先的所有提交引入到当前分支branch3上,并生成新的提交,执行命令如下:
git cherry-pick ..branch2
执行后的提交历史如下:
C<---D<---E branch2 / master A<---B \ F<---G<---H<---C'<---D'<---E' branch3 | HEAD
10.常见问题
1.The previous cherry-pick is now empty, possibly due to conflict resolution.
原因:
在cherry-pick时出现冲突,解决冲突后本地分支中内容和cherry-pick之前相比没有改变,因此当在以后的步骤中继续git cherry-pick或执行其他命令时,由于此时还处于上次cherry-pick,都会提示该信息,表示可能是由于解决冲突造成上一次cherry-pick内容是空的。
- 解决方案:
- 1.执行git cherry-pick --abort取消上次操作。
- 2.执行git commit --allow-empty,表示允许空提交。
2.fatal: You are in the middle of a cherry-pick – cannot amend.
原因:
在cherry-pick时出现冲突,没有解决冲突就执行git commit --amend命令,从而会提示该信息。
- 解决方案:
- 首先在git commit --amend之前解决冲突,并完成这次cherry-pick:
git add .git cherry-pick --continue
rebase
rebase的使用场景
假设你在你的项目master分支上(我们只显示最后4个提交):
现在你想在不同的分支上开发一些很酷的新功能,所以你创建一个git checkout -b feature分支并进行两次提交。现在你master和feature分支看起来是这样的:
但是现在有人告诉你你的应用程序存在一个bug,并且你很快就会将你的分支(checkout)切换到你master分支来查找并修复这个bug。一段时间后,您修复它并推送(并部署)更改。你可以回到你的feature分支。
但是现在你意识到你有一个小问题。您的feature分支仍然存在您在master分支上修复的相同错误,并且您意识到该错误也会影响feature分支上的新代码,因此您需要将这些错误修复(提交)合并到feature分支中。但是该如何?在做feature分支之前进行错误修复。这样,具有修复代码的提交在feature分支中。你用rebase命令来做。所以,如果你在你的feature分支,你会这样做:
git rebase master
这就是你的分支会如何看起来像之后的样子:
如你所见,feature分支的历史已经改变。如果有人现在看它,认为你feature在错误修复提交后制作了你的分支。
所以这很好?但有一个问题。git rebase会改变git的提交记录,但不用担心,你的代码将保持不变,只有feature分支上的提交的提交记录会改变。这些提交记录是什么?git log,你可能会注意到,当你这样做时,你看到每个提交都有40个字符的长字符串:
commit f5cad47722bc98419fe5037753f5bbe29d3917c4Author: John Doe <john@doe.com>Date: Mon Apr 23 11:49:07 2018 +0200
如果提交之前提交记录是这样的:
然后在rebase之后,feature分支上的提交记录会改变:
rebase是一个危险的指令
这就是为什么你一直听到git rebase是一个危险的命令。因为它改变了历史,你应该谨慎使用它。
但为什么这是危险的?那么,让我们假设你不是唯一一个在这个feature分支上工作的人,但是有更多的人在对它进行改变。现在你可以写你的代码将更改推送到feature远程。现在,当你的同事试图从远端拉动时feature,这个时候会发生代码冲突,因为他的本地feature分支版本的历史与远端版本不同。
所以同事代码冲突,但是他没有好的办法来解决这个问题。
总结:
- 什么场景使用rebase呢
- a. 当我们在本地分支上工作,并且尚未将其推送到远程服务器
- b. 我们的代码已经提交远程分支,但是当前的远程分支可以保证只有我们字节在维护这个分支,那可以使用rebase。
几点总结
1. 永远不要rebase一个已经分享的分支(到非remote分支,比如rebase到master,develop,release分支上),也就是说永远不要rebase一个已经在中央库中存在的分支.只能rebase你自己使用的私有分支
上面这个例子中展示了已经在中央库存在的feature分支,两个开发人员做了对feature分支针对master做rebase操作后,再次push并且同步工作带来的灾难:历史混乱,并且merge后存在多个完全相同的changeset。
在执行git rebase之前,总是多问问你自己:“有没有其他人也需要这个分支来工作?”,如果答案是yes,那么你就需要思考必须使用一种非破坏性的方式来完成rebase一样的工作(就是需要合入别人的工作成果),比如使用git revert命令。否则,如果这个branch没有别人来使用,那么很好,你可以非常安全地随心所欲地re-write history(注意rebase往往会重写历史,所有已经存在的commits虽然内容没有改变,但是commit本身的hash都会改变!!!)
极端情况
但是我们要注意,即使对于上面的这个已经分享的feature分支,Bob和Anna也可以互相rebase对方的feature分支,这并不违反上面强调的rebase黄金定律,下面用图例再说明一下:
假如你和你的同事John都工作在一个feature开发上,你和他分别做了一些commit,随后你fetch了John的feature分支(或者已经被John分享到中央库的feature分支),那么你的repo的版本历史可能已经是下面的样子了:
John分享到中央库的feature分支),那么你的repo的版本历史可能已经是下面的样子了:
这时你希望集成John的feature开发工作,你也有两个选择,要么merge,要么rebase,
记住在这个场景中,你rebase到John/feature分支的操作并不违反rebase的黄金定律,因为:
只有你的local本地私有(还未push的) feature commits被移动和重写历史了,而你的本地commit之前的所有commit都未做改变。这就像是说“把我的改动放到John的工作之上”。在大多数情况下,这种rebase比用merge要好很多
2. 结论:只要你的分支上需要rebase的所有commits历史还没有被push过(比如上例中rebase时从分叉处开始有两个commit历史会被重写),就可以安全地使用git rebase来操作。
3. 上述结论可能还需要修正:对于不再有子分支的branch,并且因为rebase而会被重写的commits都还没有push分享过,可以比较安全地做rebase
我们在rebase自己的私有分支后希望push到中央库中,但是却会由于rebase改写了历史,因此push时肯定会存在冲突,从而git拒绝你的push,这时,你可以安全地使用-f参数来覆盖中央库的历史(同时其他对这个feature也使用的人员可以git pull):
git push --force
代码演示
这个案例完成了我们来通过代码演示下
小明拉取feature1分支,进行了两笔提交代码如下
git branch -a //查看所有分支git checkout feature1 //下载feature1分支到本地//开发新功能,增加游戏动效git add .git commit -m "增加游戏动效"git push//开发新功能,增加签到功能git add .git commit -m "增加签到功能"git push
这时候的分支如下
这个时候我们在本地feature分支同步的开发功能
git branch -a //查看所有分支git checkout feature1 //下载feature1分支到本地//开发新功能,增加娱乐竞技模式git add .git commit -m "增加娱乐竞技模式"没有push远端//开发新功能,开发国庆礼物功能git add .git commit -m "开发国庆礼物功能"没有push远端
分支变成这样,只是紫色部分是在我的本地仓库没有推送远程仓库
假设我们想将这两个分支合(一个远端,另一个在本地)并带一个分支,我们应该怎么做呢?
git fetch //拉取远程仓库数据库同步到本地git rebase origin/feature1 //通过rebase的方式合并远程代码到本地git push //推送本地数据库到远端//如果推送失败,使用前置推送git push -f
于是分支信息变成这样了:
是不是感觉很神奇,我们是不是可以用来模拟这是一个人在开发的
用rebase -i 汇合提交,合并多次提交记录
1.我们来合并最近的 4 次提交纪录,执行:
git pull //拉取最新的代码git rebase -i HEAD~4 //合并最近的4次提交
然后会出现如下的vim状态
不难发现每一次提交都会显示一条提交信息,在提交信息的前面会有一个指令,注意这个指令的含义
# 命令:# p, pick <提交> = 使用提交# r, reword <提交> = 使用提交,但修改提交说明# e, edit <提交> = 使用提交,进入 shell 以便进行提交修补# s, squash <提交> = 使用提交,但融合到前一个提交# f, fixup <提交> = 类似于 "squash",但丢弃提交说明日志# x, exec <命令> = 使用 shell 运行命令(此行剩余部分)# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)# d, drop <提交> = 删除提交# l, label <label> = 为当前 HEAD 打上标记# t, reset <label> = 重置 HEAD 到该标记# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]# . 创建一个合并提交,并使用原始的合并提交说明(如果没有指定# . 原始提交,使用注释部分的 oneline 作为提交说明)。使用# . -c <提交> 可以编辑提交说明。
2.使用不同的,前缀可以完成不同的畸变
3.如果保存的时候,你碰到了这个错误:
error: cannot 'squash' without a previous commit
注意不要合并先前提交的东西,也就是已经提交远程分支的纪录。
4.如果你异常退出了 vim
窗口,不要紧张:
git rebase --edit-todo
这时候会一直处在这个编辑的模式里,我们可以回去继续编辑,修改完保存一下:
git rebase --continue
5.查看结果
git log
用rebase -i 修改提交
使用 git pull rebase 合并远程更新到本地
当我们日常开发中,当自己从release拉分支后进行开发,开发完成高高兴兴去提交代码,却发现如下提示
这个基本就是我们当前分支追踪的远程分支已经被其他change占用,你需要做的就是通过拉取远程分支的代码,合并到自己的本地分支上。你可通过pull这看起来没有问题。但是会出现大量的冗余的合并信息。另一种就是通过rebase的形式,合并到当前分支,这种可以保留原有分支的提交记录,看起来和我们直接在release分支上开发的一样。
git pull --rebase
merge --squash
提供一个好用的git模拟网站
https://learngitbranching.js.org/?locale=zh_CN