Git More --3
下面是阅读Pro Git的一些笔记,涵盖了第三章关于分支的内容。比较长,其实可以直接阅读小结。
在Git中,任何文件、目录、提交等等其实都是包含一定信息的对象,这些对象通过SHA-1哈希值进行唯一标识。Git使用四种类型的对象:blob(文件数据)、tree(目录结构)、commit(提交信息)和tag(标签)。这些对象通过指针相互关联,形成一个有向无环图(DAG)。详见
Git的分支,实际上仅仅是一个指向某个提交对象的指针。创建新分支时,Git只是创建了一个新的指针,指向当前的提交对象。而每次提交时,当前分支的指针会自动向前移动,指向新的提交对象。这种设计使得分支操作非常轻量级和高效。


创建分支
创建分支的命令是 git branch <branch-name>。这会创建一个新的分支指针,指向当前的提交对象。可以使用 git branch 查看所有分支,当前所在分支会有一个星号标记。
在上图的例子中,我们输入git brach testing,创建了一个名为testing的新分支,默认指向当前提交对象f30ab。
Git是怎么知道当前分支的呢?这是通过一个叫做HEAD的指针实现的。HEAD指针指向当前所在的分支,当我们切换分支时,HEAD会更新为指向新的分支。在输入git branch命令时,Git会检查HEAD指针,确定当前分支,但不会立即将HEAD指向新创建的分支。git bracnch命令只是创建了一个新的分支指针,并没有切换到该分支。
通过git log --decorate命令可以看到分支指针和提交对象的关系。
切换分支
切换分支的命令是 git checkout <branch-name>。这会将HEAD指针更新为指向指定的分支,并将工作目录和暂存区更新为该分支对应的提交对象的状态。
在上图的例子中,我们输入git checkout testing,切换到testing分支。此时,HEAD指针更新为指向testing分支,而工作目录和暂存区也更新为f30ab提交对象的状态。
切换分支时,Git会检查工作目录和暂存区的状态。如果有未提交的更改,Git会阻止切换分支,以避免丢失更改。如果确实需要切换分支,可以使用git stash命令将更改保存到栈中,切换分支后再使用git stash pop恢复更改。
此时,我们如果对其中的文件进行了修改,并提交了更改,testing分支的指针会向前移动,指向新的提交对象,而master分支的指针仍然指向旧的提交对象。
这时我们再切回master分支,HEAD指针会更新为指向master分支,而工作目录和暂存区也会更新为master分支对应的提交对象的状态。
这时,我们再对文件进行修改并提交,master分支的指针会向前移动,指向新的提交对象,而testing分支的指针仍然指向旧的提交对象。
通过这种方式,Git每次只需要创建并保存一个包含40字节的哈希值指针就可以创建一个新的分支,这就是它飞快的原因。
若要同时创建并切换到新分支,可以使用 git checkout -b <branch-name> 命令。这相当于先执行 git branch <branch-name> 创建分支,然后执行 git checkout <branch-name> 切换分支的组合命令。
分支的新建与合并
在一个具体的工作流中,可能会频繁地创建和切换分支。例如,开发新功能时,可以创建一个新的分支进行开发,完成后再将该分支合并回主分支。这样可以避免在主分支上直接进行开发,减少冲突和错误的风险。
合并分支的命令是 git merge <branch-name>。这会将指定分支的更改合并到当前分支。如果两个分支没有冲突,Git会自动完成合并,并创建一个新的提交对象,包含两个分支的更改。
在合并的时候,通常会遇到以下两种情况中的一种:需要合并的分支是当前分支的直接后继,或者两个分支有共同的祖先提交对象。
如果需要合并的分支是当前分支的直接后继,Git会简单地将当前分支的指针向前移动,指向需要合并的分支的提交对象,而不会创建新的提交对象。这种情况称为“快进合并”(fast-forward merge)。
如果两个分支有共同的祖先提交对象,Git会使用“三方合并”(three-way merge)算法来合并更改。Git会找到两个分支的共同祖先提交对象,然后比较两个分支与该祖先提交对象的差异,生成一个新的提交对象,包含两个分支的更改。
这时,如果两个分支同时对同一个文件进行了修改,则可能存在冲突。Git会标记冲突的文件,并要求用户手动解决冲突。解决冲突后,可以使用 git add <file> 命令将解决后的文件添加到暂存区,然后使用 git commit 命令完成合并。
一般来讲,解决冲突的方式有两种,一种是手动编辑冲突文件,另一种是使用图形化工具来解决。
当进行手动编辑的时候,Git会在冲突文件中插入一些特殊表记,比如:
1 | <<<<<<< HEAD: index.html |
这个标记中,<<<<<<< HEAD 表示当前分支的内容,======= 是分隔符,>>>>>>> feature 表示需要合并的分支的内容。用户需要根据实际情况编辑文件,删除这些标记,并保留最终的内容。
当使用图形化工具时,输入 git mergetool 命令,Git会输出如下内容:
1 | Merging: |
它会要求用户选择一个合并工具来解决冲突。选择后,图形化工具会显示冲突的内容,用户可以通过图形界面进行编辑和解决冲突。完成后,保存文件并关闭工具,Git会自动将解决后的文件添加到暂存区。
解决冲突后,你可以使用git status来检查所有冲突是否已解决,然后使用git commit完成合并。
分支管理
git branch命令不仅可以创建分支,话可以用于查看现有所有分支。
1 | git branch |
上面的输出显示了当前仓库中的所有分支,当前所在分支前有一个星号标记。
加入-v选项,可以显示每个分支的最新提交信息:
1 | git branch -v |
使用--merged和--no-merged选项,可以查看已经合并和未合并到当前分支的分支:
1 | git branch --merged |
1 | git branch --no-merged |
删除分支的命令是 git branch -d <branch-name>。这会删除指定的分支指针。如果该分支未被合并到当前分支,Git会阻止删除操作,以避免丢失更改。如果确实需要删除未合并的分支,可以使用 -D 选项强制删除:
1 | git branch -D <branch-name> |
远程分支
远程引用是对远程仓库中分支的引用。远程引用以 refs/remotes/<remote-name>/<branch-name> 的形式存储在本地仓库中。远程引用是只读的,不能直接修改。要更新远程引用,需要使用 git fetch 命令从远程仓库获取最新的分支信息。你可以用git ls-remote命令列出所有远程引用,或者使用git remote show <remote-name>查看特定远程仓库的引用信息。
远程分支是远程引用的一个子集,表示远程仓库中的分支。远程分支以 refs/remotes/<remote-name>/<branch-name> 的形式存储在本地仓库中。远程分支是只读的,不能直接修改。要更新远程分支,需要使用 git fetch 命令从远程仓库获取最新的分支信息。远程分支不会自动跟踪远程仓库中的分支。
要创建一个跟踪远程分支的本地分支,可以使用 git checkout -b <branch-name> <remote-name>/<branch-name> 命令。
例如,假设有一个远程仓库名为origin,其中有一个分支名为feature。要创建一个跟踪该远程分支的本地分支,可以使用以下命令:
1 | git checkout -b feature origin/feature |
这会创建一个名为feature的本地分支,并将其设置为跟踪origin/feature远程分支。此后,你可以在这个基础上进行你本地的工作。
通过git branch -r命令可以查看所有远程分支:
1 | git branch -r |
假设你拥有一个服务器git.example.com,上面有一个仓库project.git,你可以将其克隆到本地:
1 | git clone git.example.com:project.git |
这会创建一个名为project的本地目录,并将远程仓库中的所有分支和提交对象克隆到本地。默认情况下,Git会将远程仓库命名为origin,并将其设置为跟踪远程分支,即origin/main。这样你就拥有了一个可以在本地进行开发和管理的完整仓库副本。
此时你对这个仓库进行开发,产生了一些提交对象。同时,你的同事也在远程仓库上进行了开发,产生了一些新的提交对象。要将这些更改同步到本地仓库,可以使用git fetch命令:
1 | git fetch origin |
这将会从origin远程仓库拉取最新的分支和提交对象,并将origin/main分支更新到最新状态。此时将会产生两个分支指针:本地的main分支和远程的origin/main分支。origin/main分支指向远程仓库中的最新提交对象,而本地的main分支仍然指向你提交的最新提交对象。此时你无法直接在origin/main分支上进行操作。利用git merge命令可以将origin/main分支的更改合并到本地的main分支中;或者利用上面的git checkout -b <branch-name> <remote-name>/<branch-name>命令创建一个跟踪origin/main分支的本地分支进行操作。
假设你另有一个git仓库git.other.com:project.git,你可以将其添加为另一个远程仓库:
1 | git remote add upstream git.other.com:project.git |
这会将upstream远程仓库添加到本地仓库中。要从upstream远程仓库拉取最新的分支和提交对象,可以使用以下命令:
1 | git fetch upstream |
这将会从upstream远程仓库拉取最新的分支和提交对象,并将upstream/main分支更新到最新状态。此时将会产生两个远程分支指针:origin/main和upstream/main,分别指向两个远程仓库中的最新提交对象。假如upstream/main分支是origin/main分支的子集,那么upstream/main不会创建新的分支,而是指向origin/main分支的某个提交对象。
推送
当你完成了项目的某个功能开发想要将它与其他同事分享时,你需要将它推送至具有写入权限的远程仓库。推送的命令是 git push <remote-name> <branch-name>。这会将本地分支的更改推送到指定的远程仓库中的对应分支。
跟踪分支
跟踪分支是本地分支与远程分支之间的关联关系。跟踪分支允许你在本地分支上进行开发,并将更改推送到远程分支,或者从远程分支拉取最新的更改到本地分支。
要创建一个跟踪远程分支的本地分支,可以使用 git checkout --track <remote-name>/<branch-name>命令。例如,假设有一个远程仓库名为origin,其中有一个分支名为feature。要创建一个跟踪该远程分支的本地分支,可以使用以下命令:
1 | git checkout --track origin/feature |
这会创建一个名为feature的本地分支,并将其设置为跟踪origin/feature远程分支。此后,你可以在这个基础上进行你本地的工作。
通过git branch -vv命令可以查看所有本地分支及其跟踪的远程分支:
1 | git branch -vv |
上面的输出显示了当前仓库中的所有本地分支及其跟踪的远程分支,当前所在分支前有一个星号标记。
拉取
使用git pull <remote-name> <branch-name>命令可以将远程分支的更改拉取到本地分支。git pull实际上是git fetch和git merge的组合命令,先从远程仓库获取最新的分支信息,然后将其合并到当前分支。
删除远程分支
删除远程分支的命令是 git push <remote-name> --delete <branch-name>。这会将指定的分支从远程仓库中删除。例如,要删除origin远程仓库中的feature分支,可以使用以下命令:
1 | git push origin --delete feature |
这会将feature分支从origin远程仓库中删除。请注意,删除远程分支是一个不可逆的操作,删除后无法恢复。
变基
变基(rebase)是将一个分支的更改应用到另一个分支的过程。变基的命令是 git rebase <branch-name>。这会将当前分支的更改应用到指定分支的最新提交对象上。
变基的过程实际上是将当前分支的提交对象逐个应用到指定分支的最新提交对象上,形成一个新的提交对象链。这样可以保持提交历史的线性,避免出现分叉的提交历史。
在变基过程中,可能会遇到冲突。Git会标记冲突的文件,并要求用户手动解决冲突。解决冲突后,可以使用 git add <file> 命令将解决后的文件添加到暂存区,然后使用 git rebase --continue 命令继续变基过程。
例如,假设有一个分支feature,其提交对象链如下:
1 | A---B---C feature |
要将feature分支的更改应用到main分支的最新提交对象上,可以使用以下命令:
1 | git checkout feature |
这会将feature分支的提交对象A、B、C逐个应用到main分支的最新提交对象F上,形成一个新的提交对象链:
1 | A'--B'--C' feature |
变基后,feature分支的提交对象A、B、C被重新应用为新的提交对象A'、B'、C',并且这些新的提交对象基于main分支的最新提交对象F。然后再利用fast-forward合并将feature分支合并到main分支上:
1 | git checkout main |
这会将main分支的指针向前移动,指向feature分支的最新提交对象C',形成一个线性的提交历史:
1 | D---E---F---A'--B'--C' main, feature |
对于更复杂的例子,比如:
1 | A--B--C main |
你想要将bugfix分支的更改应用到main分支上,而不动feature分支,可以利用git rebase --onto命令:
1 | git checkout bugfix |
它的意思是将bugfix分支上从feature分支开始的更改应用到main分支上。变基后,提交对象链如下:
1 | D'--E'--F' bugfix |
这样,bugfix分支的更改被重新应用为新的提交对象D'、E'、F',并且这些新的提交对象基于main分支的最新提交对象C。然后再利用fast-forward合并将bugfix分支合并到main分支上:
1 | git checkout main |
这会将main分支的指针向前移动,指向bugfix分支的最新提交对象F',形成一个线性的提交历史:
1 | A--B--C--D'--E'--F' main |
变基的风险
如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。
变基的本质是丢弃旧的提交对象,创建新的提交对象。如果其他人基于旧的提交对象进行开发,那么变基会导致他们的提交对象无法再与新的提交对象关联,造成混乱和冲突。
因此,变基通常只适用于个人分支或尚未共享的分支。在与他人协作的分支上,建议使用合并(merge)来整合更改,以保持提交历史的完整性和一致性。
用变基解决变基
如果真的遇到有同事对远程仓库进行了变基操作,导致你本地工作目录基于的提交对象不再存在,可以使用git rebase <remote-name>/<branch-name>命令将你的更改重新应用到远程分支的最新提交对象上,Git将自动处理变基过程中遇到的冲突。
另一种方法是使用git pull --rebase <remote-name> <branch-name>命令将远程分支的更改拉取到本地分支,并将你的更改重新应用到远程分支的最新提交对象上。
变基与合并
变基和合并都是将一个分支的更改整合到另一个分支的过程,但它们的实现方式不同。
变基通过将当前分支的提交对象逐个应用到指定分支的最新提交对象上,形成一个新的提交对象链,从而保持提交历史的线性。变基适用于个人分支或尚未共享的分支,可以使提交历史更加清晰和易读。
合并通过创建一个新的提交对象,包含两个分支的更改,从而形成一个分叉的提交历史。合并适用于与他人协作的分支,可以保持提交历史的完整性和一致性。
选择变基还是合并取决于具体的工作流程和团队协作方式。对于个人分支或尚未共享的分支,变基是一个不错的选择;而对于与他人协作的分支,合并更为合适。
小结
- Git中的分支实际上是指向提交对象的指针,创建和切换分支非常轻量级和高效。
- 使用
git branch命令创建分支,使用git checkout命令切换分支。 - 使用
git merge命令合并分支,可能会遇到冲突,需要手动解决。 - 使用
git branch -d命令删除分支,使用-D选项强制删除未合并的分支。 - 远程分支是远程引用的子集,表示远程仓库中的分支。使用
git fetch命令更新远程分支信息。 - 使用
git push命令将本地分支的更改推送到远程仓库,使用git pull命令将远程分支的更改拉取到本地分支。 - 跟踪分支是本地分支与远程分支之间的关联关系,使用
git checkout --track命令创建跟踪分支。 - 变基通过将当前分支的提交对象逐个应用到指定分支的最新提交对象上,保持提交历史的线性。使用
git rebase命令进行变基操作。 - 变基适用于个人分支或尚未共享的分支,合并适用于与他人协作的分支。
2026年1月21日。