Git基本操作学习
本文为可视化Git学习项目的学习笔记。初级操作可视化学习:Learn Git Branching
Git基本操作
基本
1 | git commit # 新建提交记录 |
Rebase
Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
HEAD
HEAD 是一个对当前所在分支的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(用*表示)。分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。
撤销变更
1 | git reset <node> |
通过把分支记录回退几个提交记录来实现撤销改动(对远程分支无效)。
例如:在C2处执行git reset HEAD~1
1 | git revert <node> |
例如:在C2处执行git revert HEAD
要撤销的提交记录后面多了一个新提交。新提交记录 C2’ 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2’ 的状态与 C1 是相同的。revert 之后就可以把你的更改推送到远程仓库。
修改分支树
1 | git cherry-pick <node>... |
将一些提交复制到当前所在位置。cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
1 | git rebase --interactive <node> |
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
可以进行以下操作:
-
调整记录顺序
-
删除不想要的提交
-
合并提交
本地栈式提交
一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因。最后就差把
bugFix
分支里的工作合并回main
分支了。你可以选择通过 fast-forward 快速合并到main
分支上,但这样的话main
分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
答案:使用上述命令修改分支树接下来这种情况也是很常见的:你之前在
newImage
分支上进行了一次提交,然后又基于它创建了caption
分支,然后又提交了一次。此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下newImage
中图片的分辨率,尽管那个提交记录并不是最新的了。
答案:我们可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前- 然后用
git commit --amend
来进行一些小修改- 接着再用
git rebase -i
来将他们调回原来的顺序- 最后我们把 main 移到修改的最前端(用你自己喜欢的方法)
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。尝试使用cherry-pick。
Tag与Describe
1 | git tag <your_tag> [node] |
分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git 的 tag 可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
1 | git describe <ref> |
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
远程仓库操作
基本操作
远程仓库实际上它们只是你的仓库在另个一台计算机上的拷贝。
远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别。
远程分支有一个特别的属性,在你切换到远程分支时,自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作, (更新了远程分支之后)再用远程分享你的工作成果。
远程分支有一个命名规范 —— 它们的格式是<remote name>/<branch name>
1 | git fetch |
git fetch
完成了仅有的但是很重要的两步:
- 从远程仓库下载本地仓库中缺失的提交记录
- 更新远程分支指针(如
origin/main
)
git fetch
实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。git fetch
并不会改变你本地仓库的状态。它不会更新你的 main
分支,也不会修改你磁盘上的文件。理解这一点很重要,因为许多开发人员误以为执行了 git fetch
以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。所以, 你可以将 git fetch
的理解为单纯的下载操作。
1 | git pull |
实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。
1 | git push |
git push
负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。git push
不带任何参数时的行为与 Git 的一个名为 push.default
的配置有关。它的默认值取决于你正使用的 Git 的版本,但是在教程中我们使用的是 upstream
。
冲突处理
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下, git push
就不知道该如何操作了。如果你执行 git push
,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,亦或由于你的提交已经过时而直接忽略你的提交?
因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push
变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。
那该如何解决这个问题呢?很简单,你需要做的就是使你的工作基于最新的远程分支。有许多方法做到这一点呢,不过最直接的方法就是通过 rebase 调整你的工作:
1 | git fetch |
我们还可以使用 merge
尽管 git merge
不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化:
1 | git fetch |
在开发社区里,有许多关于 merge 与 rebase 的讨论。以下是关于 rebase 的优缺点:
优点:Rebase 使你的提交树变得很干净, 所有的提交都在一条线上
缺点:Rebase 修改了提交树的历史
选择哪个,仁者见仁智者见智。
远程跟踪分支
跟踪:
- pull 操作时,提交记录会被先下载到 origin/main 上,之后再合并到本地的 main 分支。隐含的合并目标由这个关联确定的。
- push 操作时,我们把工作从
main
推到远程仓库中的main
分支(同时会更新远程分支origin/main
) 。这个推送的目的地也是由这种关联确定的!
当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 origin/main
)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 main
。
我们可以自己指定这种属性:
有两种方法:
第一种就是通过远程分支切换到一个新的分支,执行:
1 | git checkout -b totallyNotMain origin/main |
就可以创建一个名为 totallyNotMain
的分支,它跟踪远程分支 origin/main
。
第二种工作方法:
另一种设置远程追踪分支的方法就是使用:git branch -u
命令,执行:
1 | git branch -u origin/main foo # 如果当前就在 foo 分支上, 还可以省略 foo |
Git Push的参数
1 | git push <remote> <place> |
例子:git push origin main
含义:*切到本地仓库中的“main”分支,获取所有的提交,再到远程仓库“origin”中找到“main”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。*我们通过“place”参数来告诉 Git 提交记录来自于 main, 要推送到远程仓库中的 main。它实际就是要同步的两个仓库的位置。需要注意的是,因为我们通过指定参数告诉了 Git 所有它需要的信息,所以它就忽略了我们所切换分支的属性(就是说不管你的HEAD在哪)!
当为 git push 指定 place 参数为 main
时,我们同时指定了提交记录的来源和去向。要同时为源和目的地指定 <place>
的话,只需要用冒号 :
将二者连起来就可以了:
1 | git push origin <source>:<destination> |
这个参数实际的值是个 refspec,“refspec” 是一个自造的词,意思是 Git 能识别的位置(比如分支 foo
或者 HEAD~1
)。如果你要推送到的目的分支不存在,Git 会在远程仓库中根据你提供的名称帮你创建这个分支。
Git Fetch的参数
git fetch
的参数和 git push
极其相似。他们的概念是相同的,只是方向相反罢了(因为现在你是下载,而非上传)如果你像如下命令这样为 git fetch 设置 的话:
1 | git fetch origin foo |
Git 会到远程仓库的 foo
分支上,然后获取所有本地不存在的提交,放到本地的 origin/foo
上。
“如果我们指定 <source>:<destination>
会发生什么呢?”
如果你觉得直接更新本地分支很爽,那你就用冒号分隔的 refspec 吧。不过,你不能在当前切换的分支上干这个事,但是其它分支是可以的。
这里有一点是需要注意的 —— source
现在指的是远程仓库中的位置,而 <destination>
才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。
理论上虽然行的通,但开发人员很少这么做。我在这里介绍它主要是为了从概念上说明 fetch
和 push
的相似性,只是方向相反罢了。
如果执行命令前目标分支不存在,Git 会在 fetch 前自己创建立本地分支,就像是 Git 在 push 时,如果远程仓库中不存在目标分支,会自己在建立一样。
如果 git fetch
没有参数,它会下载所有的提交记录到各个远程分支。
不指定<source>
在 git push 或 git fetch 时不指定任何 source
,方法就是仅保留冒号和 destination 部分,source 部分留空。
例如:git push origin :side
和git fetch origin :bugFix
-
给 push 传空值 source,成功删除了远程仓库中的
side
分支 -
如果 fetch 空 到本地,会在本地创建一个新分支。
Git Pull
1 | git pull origin foo # git fetch origin foo; git merge origin/foo |