Git07-分支
- 分支是在软件项目中启动一条单独的开发线的基本方法。分支是从一种统一的、原始的状态分离出来的,使开发能在多个方向上同时进行,并可能产生项目的不同版本。
- 通常情况下,分支会被调解并与其他分支合并,来重聚不同的力量。
- Git允许很有多分支,因此在同一个版本库中可以有许多不同的开发线。Git的分支是轻量级的、简单的。此外,Git对合并的支持是一流的。所以,大多数Git用户把分支作为日常使用。
示例1-1:
//(1)添加用户配置 ]# git config --global user.email "hengha@123.com" ]# git config --global user.name "heng ha" //(2)初始化一个新的版本库 ]# mkdir hello ]# cd hello/ ]# git init //(3)创建一个提交 ]# echo "hello world" > hello.txt ]# git add hello.txt ]# git commit -m "Commit a file that says hello"
1、使用分支的原因
- 有无数个技术、哲学、管理,甚至是社会方面的理由来创建分支。基本原因是:
- 一个分支通常代表一个单独的发布版。如果你想开始项目的1.1版本,但是有一些客户想要保持1.0版,那就把旧版本留作一个单独的分支。
- 一个分支可以封装一个开发阶段,比如原型、测试、稳定或临近发布。你也可以认为1.1版本发布是一个单独的阶段,也就是维护版本。
- 一个分支可以隔离一个特性的开发或者研究特别复杂的bug。比如,可以引入一个分支来完成一个明确定义的、概念上孤立的任务,或在发布之前帮助几个分支合并。只是为了解决一个bug就创建一个新分支,这看起来可能是杀鸡用牛刀了,但Git的分支系统恰恰鼓励这种小规模的使用。
- 每一个分支可以代表一个开发者的工作。另一个分支——“集成”分支——可以专门用于凝聚力量。
- Git把在版本库中分出的每个具有特定的目分支视为特性分支(topicbranch)或开发分支(developmentbranch)。
- Git也有追踪分支(trackingbranch)的概念,或者保持一个版本库的副本同步的分支。
- 分支名和标签很相似,甚至是可互换的。那什么时候应使用标签名,什么时候使用分支名呢?标签和分支名用于不同的目的。
- 标签是静态的,它不随着时间的推移而改变。一旦应用,不应该对它做任何改动。它相当于地上的一个参考点。
- 分支名是动态的,并且随着每次提交而移动。分支名用来标明你的持续开发。
- 可以用同一个名字来命名分支和标签。如果这样做,就必须要使用其索引名全称来区分它们。例如,可以使用refs/tags/v1.0和refs/heads/v1.0。你可能想使用相同的名称,在开发的时候作为分支名,然后在开发结束的时候将它转换为一个标签名。除非你有一个令人信服的理由,否则就应该避免使用相同的名称命名分支和标签。
2、分支名
- 分支命名的规则:
- 可以使用斜杠(/)创建一个分层的命名方案。但是,不能以斜线结尾。
- 可以使用减号(-),但不能以减号开头。
- 可以使用实心点(.)
- 不能使用两个连续的实心点(..)。
- 使用斜杠分割的分支名,斜杠后面就不能以实心点(.)开头。如feature/.new这样的分支名是无效的。
- 不能使用空格或其他空白字符
- 不能使用在Git中具有特殊含义的字符,包括波浪线(~)、插入符(^)、冒号(:)、问号(?)、星号(*)、左方括号([)。
- 不能使用ASCI码控制字符,即值小于八进制040的字符,或DEL字符(八进制177)。
- 版本库中默认分支的名称是master,大多数开发者使用这个分支保持版本库中最强大和最可靠的开发线。可以重命名甚至删除master分支,建议保留它。
- 为了支持可扩展性和分类组织,可以创建一个带层次的分支名,类似于UNIX的路径名。例如,假设你所在的一个开发团队正在修正大量的bug。把每个修复的bug放在层次结构中,在bug分支下建立不同的分支,如bug/pr-1和bug/pr-2。
- 使用层次分支命名的一个原因是Git可以像UNIX shell一样支持通配符。例如,给定命名方案bug/pr-1和bug/pr-2,可以一次选择所有bug分支(git show-branch 'bug/*')。
- 分支的命名规则是由底层命令git check-ref-format强制检测的,为了确保每个分支的名字不仅容易输入,而且在.git目录和脚本中作为一个文件名是可用的。
3、使用分支
- 在任意时刻,版本库中可能有许多不同的分支,但最多只有一个当前的或活动的分支。活动分支决定在工作目录中检出哪些文件。
- 当前分支往往是Git命令中的隐含操作数,如合并操作的目标。
- 默认情况下,master分支是当前分支,但可以把任意分支设置成当前分支。
- 每次的提交都会应用到当前分支。
- 每个分支的名字在一个特定的版本库中必须唯一,这个名字始终指向该分支上最近的提交。一个分支的最近提交称为该分支的头部(tip或head)。Git不会保持分支的起源信息。相反,分支名随着分支上新的提交而增量地向前移动。因此旧的提交必须通过散列值(提交ID)或一个相对名称(如dev~5)来选择。
- 如果你想追踪一个特定的提交——它是项目中的一个稳定点,或者是测试的一个版本——那么你可以显式地分配给它一个轻量级的标签名。
- 分支的起点并没有永久记录,但Git可以通过git merge-base命令来在算法上确定它们。
- 合并是一个分支的补充。合并时,把一个或多个分支的内容加入到一个隐式的目标分支中。但合并不会消除任何源分支或分支名。
- 可以把分支名当成一个指向特定的(虽然是变化的)提交的指针。一个分支包括足以重建整个项目历史记录的提交,沿着分支来的路径可以回到项目最开始的地方。
- 在图7-1中,dev分支指向提交的头Z。如果你想重建版本库在提交Z时的状态,那么从Z回到原始提交A的所有可达提交都是必需的。图7-1中的可达部分突出显示为粗线,涵盖除(S、G、H、J、K、L)之外的每一次提交。
- 你的每一个分支名和分支上提交的内容一样,都放在你的本地版本库中。然而,当把版本库提供给他人用时,也可以发布或选择任意数量的分支和相关的可用提交。发布一个分支必须显式地完成。同样,如果复制版本库,分支名和分支上的代码都将是新复制版本库的副本的一部分。
4、创建分支
- 新分支基于版本库中现有的提交。完全由你来决定并由哪个提交作为新分支的开始。Git支持任意复杂的分支结构,包括分支的分支和从同一个提交分叉出的多个分支。
- 分支的生命周期同样由你来决定。一个分支可能稍纵即逝,也可能长久留存。一个分支名可以在其版本库的整个生命周期中多次添加或删除。
- 使用git branch命令创建分支:
git branch Branch_Name [starting-commit] starting-commit:指定一个提交(可以是提交ID、标签名或分支名)。如果没有指定的starting-commit,默认使用当前分支的最近提交。换言之,默认在你现在工作的地方启动一个新的分支。
- 注意,git branch命令只是把分支名引进版本库,并没有改变工作目录去使用新的分支。没有工作目录文件发生变化,没有隐式分支环境发生变化,也没有做出新的提交。这条命令只是在给定的提交上创建一个分支。
示例1-2:
//(1)从当前分支的HEAD创建一个新的分支 ]# git branch bug/pr-1 //(2)从当前分支的HEAD创建一个新的分支 ]# git branch bug/pr-2
5、查看分支
示例1-3:
//(1)切换分支 ]# git checkout bug/pr-1 //(2)创建一个提交 ]# echo "hello world 2" > hello2.txt ]# git add hello2.txt ]# git commit -m "Commit a file that says hello2" //(3)创建一个提交 ]# echo "hello world 3" > hello3.txt ]# git add hello3.txt ]# git commit -m "Commit a file that says hello3"
5.1、查看分支名
- git branch命令列出版本库中的分支名。
- -r:列出远程追踪分支。
- -a:把本地分支和远程分支都列出来。
示例1-4:
//默认仅列出版本库中的本地分支。 ]# git branch * bug/pr-1 bug/pr-2 master
5.2、查看分支的详情
- git show-branch命令提供比git branch更详细的输出,按时间由近到远的顺序列出对一个或多个分支有贡献的提交。
- 与git branch一样,没有选项则列出本地分支,-r显示远程追踪分支,-a显示所有分支。
示例1-5:
]# git show-branch ! [bug/pr-2] Commit a file that says hello * [bug/pr-1] Commit a file that says hello3 ! [master] Commit a file that says hello --- * [bug/pr-1] Commit a file that says hello3 #该提交仅在一个分支中,因为提交名前面只有一个标识符 * [bug/pr-1^] Commit a file that says hello2 #该提交仅在一个分支中,因为提交名前面只有一个标识符 +*+ [bug/pr-2] Commit a file that says hello #该提交在三个分支中,因为提交名前面只有三个标识符
- git show-branch的输出破折号分为两部分:
- 上半部分:列出分支名,并用方括号括起来,每行一个。每个分支名前面用感叹号或星号(如果它是当前分支)标记,后面是该分支最近提交的日志消息的第一行。
- 下半部分:是一个表示每个分支中提交的矩阵。每个提交前面使用加号(+)表示提交在一个分支中、星号(*)表示提交在活动分支中,减号表示一个合并提交。每个提交后面跟着该提交中日志消息的第一行。
- 如果同一个提交存在于多个分支中,那么这个提交的前面会有多个加号或星号标识符,每个标识符对应一个分支。
- 一个分支中的提交是有序的,但是分支本身是以任意顺序排列的。这是因为所有分支的地位都是平等的,所以没有规则说明一个分支比另一个更重要。
- git show-branch遍历列出分支上的提交,并在它们最近的共同提交处停止。
- 在第一个共同提交处停止是默认策略,这个行为是合理的。据推测,达到这样一个共同的点会产生足够的上下文来了解分支之间的相互关系。如果由于某种原因,你想要更多提交历史记录,使用--more=mum选项,指定你想在共同提交后看到多少个额外的提交。
- git show-branch命令可以使用一组分支名作为参数,并列出这些分支的历史记录。
示例1-6:
//查看一个分支 ]# git show-branch bug/pr-2 [bug/pr-2] Commit a file that says hello //查看二个分支 ]# git show-branch bug/pr-2 bug/pr-1 ! [bug/pr-2] Commit a file that says hello * [bug/pr-1] Commit a file that says hello3 -- * [bug/pr-1] Commit a file that says hello3 * [bug/pr-1^] Commit a file that says hello2 +* [bug/pr-2] Commit a file that says hello //使用通配符 ]# git show-branch bug/* ! [bug/pr-2] Commit a file that says hello * [bug/pr-1] Commit a file that says hello3 -- * [bug/pr-1] Commit a file that says hello3 * [bug/pr-1^] Commit a file that says hello2 +* [bug/pr-2] Commit a file that says hello
6、检出(切换)分支
- 使用git checkout命令会切换到其他分支
git checkout [-f] [-m] [<branch>]
- git checkout [<branch>]会使该分支变成新的当前分支,它改变了工作树文件和目录结构来匹配给定的分支状态。
- 如果当前工作目录下有尚未提交的数据,git checkout [<branch>]命令会执行失败,以避免尚未提交的数据丢失。(新版本的git可能不一样)
- git checkout可以访问版本库的所有状态,从分支的最前端到项目的最开始。
6.1、检出分支的简单示例
示例1-7:
//(1)查看分支,可以看到当前分支是bug/pr-1(星号标记) ]# git branch * bug/pr-1 bug/pr-2 master //(2)切换分支(在切换的前后可以看到当前工作目录下的文件是不一样的) ]# git checkout bug/pr-2 Switched to branch 'bug/pr-2' //(3)查看分支,可以看到当前分支是bug/pr-2 ]# git branch bug/pr-1 * bug/pr-2 master
- 选择一个新的当前分支可能会对工作树文件和目录结构产生巨大影响。改变分支的影响有:
- 在要被检出的分支中但不在当前分支中的文件和目录,会从对象库中检出并放置到工作树中。
- 在当前分支中但不在要被检出的分支中的文件和目录,会从工作树中删除。
- 这两个分支都有的文件会被修改为要被检出的分支的内容。
- 如果检出看起来像是几乎瞬间发生的,请不要惊讶。新手一个常见的错误是认为检出没有生效,因为它本应作出巨大变化却瞬间返回了。这是Git的一个真正异于许多其他VCS的特性。Git善于在检出的时候确定要改变的文件和目录的最小集合。
6.2、检出时有未提交的更改
- 如果不明确请求,Git会排除本地工作树中数据的删除和修改。
- 工作目录中的未被追踪的文件和目录始终会置之不管,Git不会删除或修改它们。
- 如果一个文件的本地修改不同于新分支上的变更,Git发出错误消息,并拒绝检出目标分支。
6.2.1、检出时有未提交的更改
示例1-8:
1、查看bug/pr-1分支的当前工作目录
]# git branch * bug/pr-1 bug/pr-2 master ]# ls -l total 12 -rw-r--r-- 1 root root 14 Mar 20 18:05 hello2.txt -rw-r--r-- 1 root root 14 Mar 20 18:05 hello3.txt -rw-r--r-- 1 root root 12 Mar 20 17:57 hello.txt
2、查看bug/pr-2分支的当前工作目录
]# git branch bug/pr-1 * bug/pr-2 master ]# ls -l -rw-r--r-- 1 root root 12 Mar 20 17:57 hello.txt
3、在bug/pr-2分支中创建一个文件
//(1)检出分支 ]# git checkout bug/pr-2 //(2)创建一个已经存在bug/pr-1分支中的同名文件,但内容不同 ]# echo "hello world 3-2" > hello3.txt
4、检出分支到bug/pr-1
- 在这种情况下,git发出一条警告信息,某些原因造成Git停止了检出请求。
]# git checkout bug/pr-1 error: The following untracked working tree files would be overwritten by checkout: hello3.txt Please move or remove them before you switch branches. Aborting
- 如果Git草率兑现了检出分支bug/pr-1的请求,在工作目录中hello3.txt文件的本地修改将被bug/pr-1中的版本覆盖。默认情况下,Git会检测到这种潜在的损失,并防止它发生。
- 如果你真的不在乎丢失对工作目录的修改并且愿意把它们丢掉,可以通过使用-f选项强制Git执行检出。
5、暂存后,再检出
- 使用git add更新hello3.txt的内容到索引中只是将该文件的内容放到索引中,它不会将它提交给任意分支。Git还是无法在检出新分支时保存修改,因此再次失败。
//(1)暂存文件 ]# git add hello3.txt //(2)检出分支到bug/pr-1 ]# git checkout bug/pr-1 error: Your local changes to the following files would be overwritten by checkout: hello3.txt Please commit your changes or stash them before you switch branches. Aborting
- 显然,只将它添加到索引中是不够的。这时可以使用git commit命令来提交修改到当前分支(bug/pr-2分支)。
- 但是假如你希望变更作用于bug/pr-1分支上。你似乎被卡住了:你不能在检出前把变更放在bug/pr-1分支上,但Git不让你检出,因为变更已经存在。
- 幸运的是,有办法走出这种两难境地。一种方法是使用stash。另一种方法是git checkout -m。
6.2.2、使用git checkout -m(有问题)
- 需要的是一个合并:工作目录中的改变必须和被检出的文件合并。
- 如果可能,可以使用-m选项特别要求,Git通过在你的本地修改和对目标分支之间进行一次合并操作,尝试将你的本地修改加入到新工作目录中。
示例1-9:
]# git checkout -m bug/pr-1023 fatal: cannot continue with staged changes in the following files: hello3.txt
6.2.3、创建并检出新分支
- 创建一个新的分支并同时切换到它。Git提供了一个快捷方式git checkout -b new-branch来处理这种情况。
示例1-9:
- 从“示例1-8”开始,现在必须开始一个新的分支,而不是把变更应用到现有分支上。换句话说,你在bug/pr-2分支中编辑文件,突然意识到你希望将所有修改提交到一个名为bug/pr-3的全新分支。
//(1)创建并检出新分支 ]# git checkout -b bug/pr-3 Switched to a new branch 'bug/pr-3' //(2)查看索引状态 ]# git status On branch bug/pr-3 Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: hello3.txt
6.3、分离HEAD分支
- 通常情况下,通过直接指出分支名来检出分支的头部是明智的。因此,默认情况下git checkout会改变期望的分支的头部。
- 可以检出到任何提交,Git会自动创建一种匿名分支,称为一个分离的HEAD(detached HEAD)。
- 在下面的情况下,Git会创建一个分离的HEAD:
- 检出的提交不是分支的头部。
- 检出一个追踪分支。为了探索最近有什么变更从远程版本库带入到你的版本库中你可能会这样做。
- 检出标签引用的提交。为了将基于文件标签的版本放在一起发布你可能会这样做。
- 启动一个git bisect的操作。
- 使用git submodule update命令。
示例1-9:
- 从“示例1-8”开始
//(1)检出到提交 ]# git checkout 42a86bf0786741a510236062db76afa0b63c2f95 Warning: you are leaving 1 commit behind, not connected to any of your branches: ff7361b Commit a file that says hello3 If you want to keep it by creating a new branch, this may be a good time to do so with: git branch <new-branch-name> ff7361b HEAD is now at 42a86bf Commit a file that says hello2 //(2)查看是否在一个分离的HEAD上 ]# git branch * (HEAD detached at 42a86bf) bug/pr-1023 bug/pr-17 bug/pr-3 master
- 如果你发现自己在一个分离的头部,然后你决定在该点用新的提交留住它们,那么你必须首先创建一个新分支。
- 这会给你一个基于分离的HEAD所在提交的新的正确分支。然后,你可以继续正常开发。从本质上讲,命名的分支以前也是匿名的。
//(1)创建一个新分支 ]# git checkout -b bug/pr-4 Switched to a new branch 'bug/pr-4' //(2)查看分支 ]# git branch bug/pr-1023 bug/pr-17 bug/pr-3 * bug/pr-4 master
7、删除分支
- 从版本库中删除分支。Git禁止删除当前分支,只能删除非当前分支。
- 删除当前分支会导致Git无法确定工作目录树是什么样的。
git branch -d <branchname>... -d:执行Git的安全检查 -D:不执行Git的安全检查,只有确定不需要该分支额外的内容时才可以这样做。
- 还有一个十分微妙的问题。Git不会让你删除一个包含不存在于当前分支中的提交的分支。也就是说,如果分支被删除则开发的提交部分就会丢失,Git会阻止你意外删除提交中的开发。
- Git不强制要求所有分支在可以删除之前合并到master分支。请记住,分支只是简单的名称或指向有实际内容的提交的指针。相反,Git防止你在不合并到当前分支的分支被删除时,不小心丢失其内容。
- 如果已删除分支的内容已经存在于另一个分支里,那就可检出该分支,然后要求从上下文中删除分支。另一种方法是把你要删除分支的内容合并到当前分支中。然后其他分支可以安全删除了。
- Git不会保持任何形式的关于分支名创建、移动、操纵、合并或删除的历史记录。一旦某个分支名删除了,它就没了。
- 然而,该分支上的提交历史记录是一个独立的问题。Git始终会删除那些不再被引用的提交和不能从某些命名的引用(如分支或标签名)可达的提交。如果你想保留那些提交,你必须将它们合并到不同的分支,为它们创建一个分支,或用标签指向它们。否则,如果没有对它们的引用和提交,blob是不可达的,最终将被git gc工具当成垃圾回收。
- 提示,意外删除分支或其他引用后,可以使用git reflog命令恢复它。其他命令(如git fsck和配置选项[如gc.reflogExpire和gc.pruneExpire])同样可以帮助恢复丢失的提交、文件和分支的头部。
# #