git 的使用
在 Git 本地仓库中将你新添加的文件纳入版本控制,通常需要执行以下步骤:
步骤 1: 检查新文件的状态
运行以下命令查看当前仓库的状态,确保你的新文件未被 Git 跟踪:
git status
输出示例:
Untracked files:
(use "git add <file>..." to include in what will be committed)
new_file.txt
未追踪的文件会出现在 Untracked files
列表中。
步骤 2: 添加文件到暂存区
使用 git add
命令将文件添加到暂存区。你有以下几种方式:
添加单个文件:
git add <file_name>
例如:
git add new_file.txt
添加多个文件:
如果有多个文件,可以一次性添加:
git add file1.txt file2.txt
添加所有未追踪文件:
使用通配符 .
添加所有未追踪的文件和修改:
git add .
步骤 3: 提交文件到版本库
文件添加到暂存区后,需要提交到版本库:
git commit -m "描述本次添加的文件或修改"
例如:
git commit -m "添加新文件 new_file.txt"
当然你也可以全部添加
git add * //添加监管目录下的所有文件
完整操作示例
假设新添加了一个文件 example.txt
,完整操作流程如下:
git status #检查文件状态
git add example.txt # 将文件添加到暂存区
git commit -m "添加 example.txt" # 提交更改
注意事项
忽略不需要的文件: 如果有文件不需要被追踪,可以在
.gitignore
文件中添加规则:然后运行:
git add .gitignore git commit -m "更新 .gitignore 文件"
检查提交结果: 提交后可通过以下命令查看提交记录:
git log
按照这些步骤,就可以成功将本地添加的文件纳入 Git 的版本管理。
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有 parent 节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
Git Commit Git 提交
git commit首先就是简单的提交,基于上一个节点往下衍生一个子节点。
格式:
git branch 节点名字 创建分支的节点位置(默认为head)
//示例
git branch bugFix HEAD~^2~
一般就是(括号是可加可不加)
git commit (-m“备注”) //备注就是提交时候会附带你的更改说明
里面的c2什么的其实是标记的哈希码,是一段很长的字母和数字,一般用前三位就能表示
git branch
分支其实就是一个新的指针。
就是在当前的head节点创建一个分支,最开始的都是main。
然后就是
git branch newImage //创建一个新的分支(指针)
但是注意head指针依然附在main上所以你提交依然是在main上操作
git commit
git checkout
切换head跟踪的分支或者节点
git checkout newImage(或者对应节点的哈希值)
然后我们提交就会如图,在新的分支上提交。
注意:在 Git 2.23 版本中,引入了一个名为 git switch
的新命令,最终会取代 git checkout
,因为 checkout
作为单个命令有点超载(它承载了很多独立的功能)。 由于现在很多人还无法使用 switch
,本次课程仍然使用 checkout
而不是 switch
, 但是如果你想尝试一下新命令,我们的应用也是支持的!并且你可以从这里学到更多关于新命令的内容。
对了,有个更简洁的方式:如果你想创建一个新的分支同时切换到新创建的分支的话,可以通过
git checkout -b <your-branch-name>
来实现
接下来咱们看看如何将两个分支合并到一起。就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
git merge
在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个 parent 节点。翻译成自然语言相当于:“我要把这两个 parent 节点本身及它们所有的祖先都包含进来。”
咱们先来看一下第一种方法 —— git merge
。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个 parent 节点。翻译成自然语言相当于:“我要把这两个 parent 节点本身及它们所有的祖先都包含进来。”
当前处于main分支
git merge bugFix
表示将bugFix融合至当前的head指向的节点来。如果当前head指向的是一个分支,那么这个分支也会指向新的合并点
这意味着 main
包含了对代码库的所有修改。
此时我们将main融合至bugFix中,我们会发现Git 什么都不做,只是简单地把 bugFix
移动到 main
所指向的那个提交记录,因为 main
继承自 bugFix
git checkout bugFix
git merge main
git rebase
第二种合并分支的方法是 git rebase
Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
git rebase 复制到的目标节点 源节点(默认为head)
注意如果复制的源节点是分支,那么分支会指向新的复制节点。
但是如果复制到的目标节点如果是分支,他的分支指向的节点是不会变的(就是说我复制到bugFix节点下方,但是bugFix节点是不动的)
此时我执行
git rebase main bugFix
main不变,bugFix显然变化了(不论head在哪,head是肯定会指向复制的最后一个)
这样会把俩个节点的不同点,全部复制到目标节点下,这么说可能有点抽象
我们看到如图的结构,我现在执行下面的命令
git rebase bugFix side
我们就可以看见从side到c4的节点全部复制过来了
git rebase main //将head此时指向的bugFix分支上的不同点复制到mian下面
注意此时的head如果指向的是分支,那么分支也会指到新的复制点上
假设此时将head指向main,然后复制到bugFix,他会复制吗。
git checkout main
git rebase bugFix
答案是不会,只会将main指向bugFix指向的节点,因为bugFix继承mian
HEAD
我们首先看一下 “HEAD”。 HEAD 是一个对当前所在分支的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
记住,如果是单纯的push,head是不能从分支上分离。当然也有分离提交的方法
我们执行下面的方式,可以看到,此时HEAD就是分离出来了
git checkout c2
相对引用^
和~
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,可不像示例的c1,c2。是非常复杂的哈希值,我们必须使用git log
去查询提交记录和哈希值(例如fed2da64c0efc5293610bdd892f82a58e8cbc5d8
)。
Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2
而不是上面的一长串字符。
正如前面所说,通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。这个就很厉害了!
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix
分支或 HEAD
)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
- 使用
^
向上移动 1 个提交记录 - 使用
~<num>
向上移动多个提交记录,如~3
二者还是有差别的,后面会介绍的
所以 main^
相当于“main
的 parent 节点”。
main^^
是 main
的第二个 parent 节点
现在咱们切换到 main 的 parent 节点,此时HEAD就指向了父节点
git checkout main^
你也可以将 HEAD
作为相对引用的参照。下面咱们就用 HEAD
在提交树中向上移动几次。
git checkout head^^ //指移动俩次
“~”操作符
该操作符后面可以跟一个数字(可选,不跟数字时与 ^
相同,向上移动一次),指定向上移动多少次。
然后我们移动四次,直接移动到c0
git checkout HEAD~4
选择 parent 提交记录
操作符 ^
并不是用来指定向上返回几代,而是指定合并提交记录的某个 parent 提交。一个合并提交有两个 parent 提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个” parent 提交,在操作符 ^
后跟一个数字可以改变这一默认行为。
如果我们想回到c1
git checkout main^
但是如果我们想要回到c2,就要使用下面的指令
git checkout main^2
这些操作符还支持链式操作!试一下这个:
git checkout HEAD~^2~2 //往上一个,选择父节点2,再次往上俩个
强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。
git branch -f 分支名字 分支指向的节点位置(可以是节点,也可以相对引用的位置,也可以是别的分支)
例如:
git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级 parent 提交。
撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset
,还有就是 git revert
。
Git Reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
Git 把 main 分支移回到 C1
;现在我们的本地代码库根本就不知道有 C2
这个提交了。
(在reset后, C2
所做的变更还在,就是你目前本地库的代码依然是修改过后的,但是只不过main指向的,但是处于未加入暂存区状态。)
git reset HEAD~1
虽然在你的本地分支中使用 git reset
很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!我们是无法提交这种更改的,因为假设远程仓库不可能让你去动已经是中间节点的c1,在远程仓库c1已经被继承过了,所以无法推送。
Git Revert
所以我们将回撤的节点作为新的提交推送就行了。
git revert HEAD
我们可以看到不想reset,我们而是拥有了一个新的提交,是因为新提交记录 C2'
引入了更改 —— 这些更改刚好是用来撤销 C2
这个提交的。也就是说 C2'
的状态与 C1
是相同的。
这样之后,我们就可以推送到远程仓库
整理提交记录
然而, 剩余的 10% 在处理复杂的工作流时(或者当你陷入困惑时)可能就显得尤为重要了。接下来要讨论的这个话题是“整理提交记录” —— 开发人员有时会说“我想要把这个提交放到这里, 那个提交放到刚才那个提交的后面”, 而接下来就讲的就是它的实现方式,非常清晰、灵活,还很生动。
看起来挺复杂, 其实是个很简单的概念。
Git Cherry-pick
本系列的第一个命令是 git cherry-pick
, 命令形式为:
git cherry-pick 节点1 节点2 ...(按顺序复制到head下方)
这里有一个仓库, 我们想将 side
分支上的工作复制到 main
分支,你立刻想到了之前学过的 rebase
了吧?但是咱们还是看看 cherry-pick
有什么本领吧。
git cherry-pick c2 c4
就是在当前的分支下面复制c2和c4的提交,按顺序写。
(如果head指向是一个分支(未分离状态),那么分支也会随着复制向下走,head是肯定会指向复制的最后一个)
交互式的 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
互式 rebase 指的是使用带参数 --interactive
的 rebase 命令, 简写为 -i
git rebase -i 你需要回溯到的节点 开始的节点(默认为head)
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。 考虑到课程的初衷,我弄了一个对话框来模拟这些操作。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
git rebase -i HEAD~4
教程就会出现这样的UI,供你重新调整,或者丢弃。注意我回溯4的节点,但是注意最后一个c1是不包含在下面的修改列表。
随后就是从c1,重新创建分支将修改后进行复制成如图的逻辑。
(注意此时不管head在哪里,head都会指向此操作后最后一个,此处的c4‘)
本地栈式提交
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix
分支里的工作合并回 main
分支了。你可以选择通过 fast-forward 快速合并到 main
分支上,但这样的话 main
分支就会包含我这些调试语句了。
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -i
git cherry-pick
来达到目的,实现一个如图的效果
git checkout main
git cherry-pick bugFix
提交的技巧 #1
接下来这种情况也是很常见的:你之前在 newImage
分支上进行了一次提交,然后又基于它创建了 caption
分支,然后又提交了一次。
此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage
中图片的分辨率,尽管那个提交记录并不是最新的了。
(就是想修改一下父节点)
我们可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
git commit --amend
来进行一些小修改(仅仅限于此教程) - 接着再用
git rebase -i
来将他们调回原来的顺序 - 最后我们把 main 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
本质上还是因为推送到远程仓库只能基于最前端节点修改
提交的技巧 #2
我们可以使用 rebase -i
对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend
修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。
其实就是利用
cherry-pick
去调换顺序修改
Git Tags
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
咱们先建立一个标签,指向提交记录 C1
,表示这是我们 1.0 版本。
git tag v1 c1
我们将这个标签命名为 v1
,并且明确地让它指向提交记录 C1
,如果你不指定提交记录,Git 会用 HEAD
所指向的位置。
Git Describe
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe
的语法是:
git describe <ref>
<ref>
可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会使用你目前所在的位置(HEAD
)。
它输出的结果是这样的:
<tag>_<numCommits>_g<hash>
tag
表示的是离 ref
最近的标签, numCommits
是表示这个 ref
与 tag
相差有多少个提交记录, hash
表示的是你所给定的 ref
所表示的提交记录哈希值的前几位。
当 ref
交记录上有某个标签时,则只输出标签名称
对这个图使用
git describe main
输出
v1_2_gC2 //最近的标签,差俩个提交,main是c2