Git分支管理
Git最重要的运用场景是多人协同开发,但是如何能保证每个人之间的开发不影响其他人的开发进程,Git 分支的出现就是解决了这个问题,使得每个人之间的开发是独立的,互不影响的。
与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。
分支操作
使用 git branch
来查看现有的分支或创建新的分支。当不带任何命令参数时,输入 git branch
可以帮助我们查看当前项目所拥有的全部分支。并且 Git 会使用 *
来标明我们当前所处的分支上。
当我们想要新增加新的分支时,只需要在git branch命令后面加上我们想要新建的分支的名称即可。
创建issue102的分支
git branch issue102
查看现有的所有分支
git branch
虽然创建了issue102的分支,但是当前分支还是在master上。我们可以通过 git checkout
命令来进行切换分支。
git checkout issue102
Switched to branch 'issue102'
git branch
* issue102
master
切换分支后,我们就可以进行自己的开发。分支上的文件状态是不同的。
touch issue102.md
git add issue102.md
git commit -m "update issue102.md"
touch issue102.html
git add issue102.html
git commit -m "update issue102.html"
在完成上述命令后,
git log --oneline
cd836b0 (HEAD -> issue102) update issue102.html
7575f02 update issue102.md
242c407 (master) update hello.md
issue102分支上的记录与master的记录间隔开了。除此之外,当我们切换回主分支后,我们还会发现master分支下没有新建的issue102.md和issue102.html两个文件。
当我们在分支上完成来开发工作后,我们需要将我们在当前分支进行的工作合并到主分支上。首先我们需要切回需要合并到的分支上,此处以issue102合并到master上为例子进行演示。
# 切换回主分支
git checkout master
# 使用git merge 进行合并
git merge issue102
# 查看所有未合并工作的分支
git branch --no-merged
有时候分支的合并不会一番顺利,当我们在两个分支中对同一个文件的同一个部分进行了不同的修改,Git就没有办法顺利的合并他们,会在合并的时候产生合并冲突。比如我们在issue102分支和master分支下对issue102.md文件进行了修改,当我们将issue102分支融合到主分支上时就会发生冲突。]
可以通过 git status
查看命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件。当出现矛盾后,合并的文件内容将会出现 "<<<<<<","=======",">>>>>>"
等分割线来进行标记。
当出现了矛盾时,我们需要进行手动解决或者放弃合并。
- 手动合并的方法很简单,就是我们选择我们要保留的代码,然后再把>>>>>, ======, <<<<<<这些提示行给去掉。最后重新进行add commit的操作即可。
- 当我们发现冲突所导致的改动量很大时,我们可以选择放弃该次的合并。我们可以使用
git merge --abort
放弃此次的融合。如果我们在运行了git merge之后又进行了一些人为的改动,那么在abort之后,所进行的改动也会被回滚掉。
除了手动合并以及放弃合并之外,我们还有一些其他的合并工具。git官方开发了一个专门用来合并的工具,叫做git mergetool(下图所示),它会将找到一份两个分支的祖先代码作为base(基准),然后再将两个分支的改动都列举出来作为对比,让我们在git编辑器当中决定要留下什么。
git remote -v查看远程库的详细信息。会显示我们可以抓取或推送的origin地址。
$ git remote -v
origin git@github.com:ProjectOwner/ProjectName.git (fetch)
origin git@github.com:ProjectOwner/ProjectName.git (push)
当我们需要推送本地分支到远程时,需要指定具体的本地分支。
# 推送本地的master分支到远程
git push origin master
# 推送本地的issue102分支到远程
git push origin issue102
在Git中没有什么分支是不可以删除的(除了当前所在的分支不能删除),包括master分支也是可以进行删除。 Git的分支删除可以分为删除本地分支和远程分支。
# 删除本地分支
# branchName 是需要删除的本地分支名字
git branch -d branchName
当我们想强行删除分支时,只需要将参数d改为D即可。
删除远程分支
# origin 是远程的主机名
# branch 需要删除的远程分支
git push origin --delete branch
当我们需要重命名分支的名称时,也使用git branch命令来进行,具体方式如下:
# oldBranchName: 旧分支名
# newBranchName :新分支名
git branch -m oldBranchName newBranchName
当我们想要将改名后的分支推送到远程时,我们需要进行如下操作:
git branch -m oldBranchName newBranchName # 将本地的分支进行重命名
git push origin newBranchName # 将新的分支推送到远程
git push --delete origin oldBranchName # 删除远程的旧的分支
分支开发工作流
在整个项目开发周期的不同阶段,我们可以同时拥有多个分支;然后我们可以定期地把某些主题分支合并入其他分支中。许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 他们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。这样,在确保这些已完成的主题分支(短期分支,比如之前的 issue102 分支)能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。
短期分支也可以叫做主题分支,它的作用是用来实现某一种特性或者相关工作(修复bug,开发产品新特性)。比如当我们的产品出现了bug时,我们应该新建一个分支并起名为bug分支,并在该分支上进行bug的修复,等我们的代码确定不会引起其他bug时,我们就可以合并到主分支上进行修复。当我们看见issue时,我们也可以使用同样的方式来解决issue的问题。常见的短期分支还有上面提到的develop,topic分支。在实际开发中,我们应该按照以下几个基本原则进行分支开发工作流程
master分支应该是最稳定的,也就是仅用来发布新版本,平时不能直接在上面进行操作,应该保存在远程。
短期分支是我们干活的分支,短期分支可以不用上传到远程,当我们完成了bug的修复,新功能的开发时才需要合并到主分支上。
Git 工具
引用修订版本和分支
Git 支持多种方式来引用单个提交、一组提交或一定范围内的提交。
切换至项目工作目录,执行 git log 能看到类似提交日志的输出。
从日志能明显的看到多次提交的记录,每次包括 commit + 一串字符、作者、提交时间 和详细信息等。
想查看某次提交信息,可以通过 git show 来查看。
通过在 git log后增加 --pretty=oneline简化输出内容。
如果你要查看一个分支的最后一次对象,可以通过分支名查看。查看本地分支列表通过git branch查看。
查看远程分支通过后加参数 -r
通过 git show stable
查看指定分支最后一次提交信息
暂存文件
git stash
会处理工作目录的的状态,跟踪文件的修改和暂存的改动,然后将未完成的修改保存至一个栈上,后续再切换回来.
通过 git stash list 查看所有 stash 的列表。
切换至最后 stash 变更,直接执行 git stash apply 即可,当然如果有多个,可以通过 git stash apply stash@{n} 中的 n 来获取指定的的变更。
可以通过 git stash drop 或者 git stash pop 来删除 stash 的内容。
想同时获取变更,并删除 stash 的内容的话, git stash pop 是比较好的用的方式,也是个人最喜欢的方式。
清理工作目录
使用 git clean -f -d 命令来移除工作目录中所有未追踪的文件以及空的子目录。 -f 意味着强制移除,使用它需要 Git 配置变量 clean.requireForce 没有显式设置为 false。
如果你只是想看下或者删除前小心翼翼的确认: 它到底会删除那些东西. 可以通过–dry-run或者-n选项来执行命令,这只是告诉你会删除什么,而不会真的删除.
默认情况下,git clean 命令只会移除没有忽略的未跟踪文件。 任何与 .gitignore 或其他忽略文件中的模式匹配的文件都不会被移除。如果你也想移除,可以通过增加选项-x
同样增加选项-d可以删除目录
如果你想交互删除,可以通过选项 -i来操作
通过What now后输入命令或序号交互操作,不知道可以输入 help 查看具体的描述
搜索
Git 提供了一个grep命令,可以很方便的从提交历史,工作目录,甚至索引中查找一个字符串或者正则表达式。
默认情况下git grep会查找你的工作目录文件。
$ git grep a.percentileBoundary
src/trace/histogram.go:func (h *histogram) percentileBoundary(percentile float64) int64 {
src/trace/histogram.go: return h.percentileBoundary(0.5)
src/trace/histogram_test.go: percentile := a.percentileBoundary(test.fraction)
通过-n或者–line-number显示匹配的行号
$ git grep -n percentileBoundary
src/trace/histogram.go:120:func (h *histogram) percentileBoundary(percentile float64) int64 {
src/trace/histogram.go:166: return h.percentileBoundary(0.5)
src/trace/histogram_test.go:181: percentile := a.percentileBoundary(test.fraction)
通过-c或者–count输出统计信息
git grep -c percentileBoundary
src/trace/histogram.go:2
src/trace/histogram_test.go:1
通过-p 或者 --show-function 显示每个匹配字符串所在的方法或函数
git grep -p percentileBoundary
src/trace/histogram.go=func (h *histogram) standardDeviation() float64 {
src/trace/histogram.go:func (h *histogram) percentileBoundary(percentile float64) int64 {
src/trace/histogram.go=func (h *histogram) median() int64 {
src/trace/histogram.go: return h.percentileBoundary(0.5)
src/trace/histogram_test.go=func TestPercentileBoundary(t *testing.T) {
src/trace/histogram_test.go: percentile := a.percentileBoundary(test.fraction)
子模块
基础操作
项目中经常会遇到包含另外一个项目,如:第三方库,或者你将自己的项目切分成多个子项目,然后在其他项目中引用,如,将项目中的 model 独立处理,独立维护;其他项目组引用这个项目,并不维护 model。这里我们可以将 model 做子项目添加到当前项目中。
通过 git submodule add 添加子模块,使用 https://github.com/datawhalechina-git-samples/model 进行测试,如
$ git submodule add https://github.com/datawhalechina-git-samples/model
Cloning into '/Users/martin/project/datawhalechina/app/model'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (5/5), done.
参数同 clone,默认是 repo 的名称,如果你想改名,可以在后续增加新的名称或路径。
通过 git status 能看到新的 model 库。
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitmodules
new file: model
能看到有个新增的 model 和 .gitmodules文件,该配置文件保存了项目 URL 和本地目录的 mapping 关系。
$ cat .gitmodules
[submodule "model"]
path = model
url = https://github.com/datawhalechina-git-samples/model
如果有多个子模块,这里会列出多条。
通过git diff能看到更详细的信息
$ git diff --cached model
diff --git a/model b/model
new file mode 160000
index 0000000..a8328fd
--- /dev/null
+++ b/model
@@ -0,0 +1 @@
+Subproject commit a8328fd6ee683ef8f6a2d7c4edfefed2923b0795
虽然 model 是工作目录中的一个子目录,但 Git 还是会将它视作一个子模块。当你不在那个目录中时,Git 并不会跟踪它的内容, 而是将它看作子模块仓库中的某个具体的提交。
如果你想看到更漂亮的差异输出,可以给 git diff 传递 --submodule 选项。
$ git diff --cached --submodule
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..f9d131a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "model"]
+ path = model
+ url = https://github.com/datawhalechina-git-samples/model
Submodule model 0000000...a8328fd (new submodule)
当你提交时,会看到类似下面的信息:
$ git commit -am 'add model module'
[main 4432854] add model module
2 files changed, 4 insertions(+)
create mode 100644 .gitmodules
create mode 160000 model
注意 app 记录的 160000 模式。这是 Git 中的一种特殊模式,它本质上意味着你是将一次提交记作一项目录记录的,而非将它记录成一个子目录或者一个文件。
然后推送至服务端
git push origin main
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 457 bytes | 457.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/datawhalechina-git-samples/app
a55ea12..4432854 main -> main
我们在 clone 一个含子模块的项目时,默认是不会包含子模块内容的,只有目录,如重新 clone 上述的 app 项目
$ git clone https://github.com/datawhalechina-git-samples/app.git new_app
Cloning into 'new_app'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 8 (delta 1), reused 3 (delta 0), pack-reused 0
Receiving objects: 100% (8/8), done.
Resolving deltas: 100% (1/1), done.
$ cd new_app/model
$ ls -alh
total 0
drwxr-xr-x 2 martin staff 64B May 4 13:46 .
drwxr-xr-x 8 martin staff 256B May 4 13:46 ..
会发现什么也没有,需要通过如下两个命令来获取内容
git submodule init 初始化本地配置文件
git submodule update 则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
$ git submodule init
Submodule 'model' (https://github.com/datawhalechina-git-samples/model) registered for path './'
$ git submodule update
Cloning into '/Users/martin/project/datawhalechina/new_app/model'...Cloning into '/Users/martin/project/datawhalechina/new_app/model'...
Submodule path './': checked out 'a8328fd6ee683ef8f6a2d7c4edfefed2923b0795'
不过还有更简单一点的方式。 如果给 git clone 命令传递 --recurse-submodules
选项,它就会自动初始化并更新仓库中的每一个子模块, 包括可能存在的嵌套子模块。
$ git clone --recurse-submodules https://github.com/datawhalechina-git-samples/app.git new_app2
Cloning into 'new_app2'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 8 (delta 1), reused 3 (delta 0), pack-reused 0
Receiving objects: 100% (8/8), done.
Resolving deltas: 100% (1/1), done.
Submodule 'model' (https://github.com/datawhalechina-git-samples/model) registered for path 'model'
Cloning into '/Users/martin/project/datawhalechina/new_app/model/new_app2/model'...
...
如果你已经克隆了项目但忘记了 --recurse-submodules,那么可以运行 git submodule update --init
将 git submodule init 和 git submodule update 合并成一步。如果还要初始化、抓取并检出任何嵌套的子模块。
当子模块有更新的时候,执行 git submodule update --remote
$ git submodule update --remote
该命令默认会更新 main 分支,如果你想设置为其他分支,可以在 .gitmodules 文件中设置 (这样其他人也可以跟踪它),也可以只在本地的 .git/config 文件中设置,我们在.gitmodules中配置它
$ git config -f .gitmodules submodule.model.branch stable
$ cat .gitmodules
[submodule "model"]
path = model
url = https://github.com/datawhalechina-git-samples/model
branch = stable
很明显很看到 branch 已经变化。当运行 git submodule update --remote 时,Git 默认会尝试更新 所有 子模块, 所以如果有很多子模块的话,你可以传递想要更新的子模块的名字。如 git submodule update --remote model
$ git submodule update --remote model
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 266 bytes | 133.00 KiB/s, done.
From https://github.com/datawhalechina-git-samples/model
a8328fd..ca79fae main -> origin/main
* [new branch] stable -> origin/stable
Submodule path 'model': checked out 'ca79fae869c9b4ddd7999f06ffd48ac25971b9dd'
打包
Git 提供了多种网络传输的方法,如 SSH、HTTP 等,但是还有种不太常用的功能又什么有效。
Git 可以就将它的数据"打包"到一个文件中,通过 git bundle来实现。bundle 命令会将git push命令所传输的所有内容打包成一个二进制文件,你可以将这个文件转发给别人,然后解包到仓库中。
$ git bundle create repo.bundle HEAD main
Enumerating objects: 90, done.
Counting objects: 100% (90/90), done.
Compressing objects: 100% (83/83), done.
Total 90 (delta 12), reused 24 (delta 3), pack-reused 0
$ ls -alh repo.bundle
-rw-r--r-- 1 martin staff 6.2M May 4 12:05 repo.bundle
这个 repo.bundle 就是打包之后的文件,改文件包含了所有重建仓库 main 分支所需要的数据。在使用 bundle 命令时,你需要列出所有你希望打包的引用或者提交的区间。 如果你希望这个仓库可以在别处被克隆,你应该像例子中那样增加一个 HEAD 引用。
别人就可以从这个二级制文件 clone 一个目录,就像从git clone https/ssh 一样的功能
$ git clone repo.bundle repo
Cloning into 'repo'...
Receiving objects: 100% (90/90), 6.20 MiB | 88.21 MiB/s, done.
Resolving deltas: 100% (12/12), done.
$ git log --oneline
...
如果你在打包时没有包含 HEAD 引用,你还需要在命令后指定一个 -b main 或者其他被引入的分支, 否则 Git 不知道应该检出哪一个分支。
如果只是要提交最新提交的修改,这需要我们手工计算,可以通过如下的指令计算差别
$ git log --oneline origin/main..main
或者
$ git log --oneline main ^origin/main
这里将获得到我们希望被打包的提交列表,将这些提交打包,通过 git bundle create操作
$ git bundle create commits.bundle main ^5de18d5
可以将 commits.bundle 文件分享给合作者,他可以将这个文件导入到原始仓库中。在导入前可通过bundle verify 命令检查这个文件是否是一个合法的 Git 包,是否拥有共同的祖先。
git bundle verify commits.bundle
如果打包工具打包的并不是全部的变更,而是最后几个变更,原始仓库则无法导入这个包,因为这个包缺失必要的提交信息。