Git学习笔记——1. 基础
关于 Git 的学习笔记整理,这部分资料准备了很久,因为中文的教程大都过于简略,并且不涉及原理,学不明白,至于英文的文档也过于复杂,各种参数选项非常复杂。学习笔记的内容并不是由浅入深地介绍Git,而是一个梳理笔记,主要参考Git 官方手册
Git 必要配置
Git 的配置分成三层:
- 第一层是仓库级,在仓库目录下,文件为
.git/config
- 第二层是用户级,在用户主目录下,文件为
~/.gitconfig
(主要修改的是用户级配置) - 第三层是系统级,在安装目录下,安装时的选项会保存在这里
范围越小的配置文件优先级越高。除了这几个配置文件,还有一些可选的辅助配置文件,例如
- 关于排除文件的规则配置
.gitignore
- 关于文件格式的配置
.gitattributes
- 关于子模块的配置
.gitsubmodule
值得注意的,Git在每次操作时,会检查当前工作目录中实际存储的
.gitignore
以及其它配置文件,而不是Git保存的最新版本,即使我们修改了.gitignore
,也不需要先单独提交.gitignore
,它的规则会立即生效。
我们主要关注用户级配置文件,有如下常用的设置:
- 添加用户名和邮箱,这是用于在提交信息上署名
1 | git config --global user.name "xxxx" |
- 将默认分支设置为 main(由于历史原因,默认为 master)
1 | git config --global init.defaultBranch main |
- 指定编辑提交信息使用的编辑器,默认可能是 vim 或者 Nano(ctrl+x 退出)或者记事本
1 | git config --global core.editor vim |
如果在编辑提交信息时,突然想要取消,可以留下空的文本并退出编辑器,git 会拒绝没有提交信息的 commit。
除此之外,还有一些编码设置和换行符设置,一个 .gitignore
配置文件示例如下
1 | [user] |
注意:
- 即使这里进行了很多编码配置,实践中在Windows下仍然可能出现乱码问题(虽然git输出的是UTF-编码,但是被终端根据当前代码页将其作为GBK编码输出),一个解决办法是开启实验性选项:将默认代码页修改为 65001 也就是 UTF-8,但是这也可能导致某些旧的中文程序出现乱码。总之编码问题就是一个大坑,最好的办法只有一个:Git仓库中尽量不要使用中文文件名和提交信息。
- 虽然在提交信息中是可以包括emoji的,有的规范也建议使用emoji,但是在某些环境下可能无法正常显示emoji,因此决定避免使用。
Git 基础概念
在Git的逻辑层面上,主要的命令基于下列三个区域进行操作:
- 工作目录:真实文件系统中的目录
- index:缓冲区,暂存区,存储信息是下一次正式提交的草稿
- 版本链表:由每一次的正式提交组成,通过HEAD和分支来指向和操作,链表体现了每一次提交之间的继承关系
其中index是介于当前工作目录和正式的版本链表之间的,发挥着重要作用。
正常的本地工作流程是:
- 在工作目录中新建或修改文件
- 将改动后的文件从工作目录缓存到 index
- 基于index的草稿生成一次正式提交,添加到版本链表,HEAD操作分支指向最新的提交
信息的传递顺序通常是:工作目录,index,HEAD。但是也可以反过来进行,这相当于撤销了新的改动,撤销操作在后续的笔记中,现在主要关注正向的操作。
创建项目(init)
git init
创建空仓库并初始化,当前位置会生成一个.git/
文件夹,里面的信息就是Git仓库的一切,不建议直接读写.git/
里面的文件,应当通过Git提供的命令进行操作。
加上--bare
选项会创建裸的空仓库。如果在服务器搭建远程仓库,通常会选择裸仓库,裸仓库的名称习惯为
xxx.git
,此时的仓库相当于只是一个 .git/
文件夹,没有工作目录,只有index和版本链表,例如
1 | git init --bare xxx.git |
注意:
- 对于已经存在的仓库,再次执行
git init
没有任何效果,不会清空仓库或进行任何重置! - 如果希望从头再来,直接且有效的办法是移除整个
.git/
文件夹,然后执行git init
- 如果在本地希望复制整个仓库到另一个位置,可以考虑只复制整个
.git/
文件夹到新的位置,然后检出 - 如果在云服务器上创建裸仓库,则需要注意裸仓库及其子文件的用户权限,例如使用下面的命令将权限提供给git用户(当前为root用户)
1
2
3git init --bare xxx.git
chown git:git -R xxx.git
克隆仓库(clone)
git clone
用于从 URL 克隆现有仓库:
1 | git clone https://github.com/libgit2/libgit2 |
可以指定本地仓库的名称,缺省名称时使用与 URL 相同名称创建本地文件夹,但是如果本地文件夹已经存在并且非空,Git 会拒绝克隆以避免误操作。 创建的本地仓库会自动添加URL为远程仓库,并拉取远程仓库的内容到本地。
如果克隆的仓库过大,考虑网络原因,可以选择舍弃 Git 的历史数据,使用
--depth 1
选项只会获取最新的提交版本。
从Github克隆仓库可能因为网络原因而失败,直接下载zip压缩包则更容易成功,但是下载的压缩包不含
.git/
,解压后的文件夹不是一个仓库,只包含最新版本的所有文件。
添加快照(add)
git add
命令可以添加未记录的或已记录但发生变化的文件到
index
1 | git add <filename> |
可以添加路径的所有项,也支持通配符
1 | git add <path> |
一个很重要的事实是:快照是在git add
时就被添加到数据库中的,而非在git commit
正式提交时添加到数据库。
如果无意间添加大文件和垃圾文件到数据库中,即使可以撤销
git add
,但是这些数据仍然会作为垃圾存在于数据库中,因此git add
在提交时要慎重,不建议直接git add *
。(Git实际上存在针对大文件进行额外支持的组件,这里不做讨论)
git add
只会记录当前文本的快照,如果在
git add
之后进行了修改,那么必须再次执行
git add
才会把最新的快照保存到数据库,并更新
index,index是下一次的正式提交的草稿。
Git 只会记录文件的内容以及有限的元数据,包括执行权限、是否是链接等,不会记录其它元数据,包括创建和修改时间,以及windows文件系统中的文件其它属性。
查看状态(status)
git status
是非常常用的命令,可以查看当前的状态,工作目录的所有文件被 git
分成两类:
- 未跟踪的项
- 已跟踪的项
除此之外, .gitignore
文件所匹配的项会被 Git
直接忽略。状态信息主要包括:
- 工作目录和 index 的对比
- 未追踪的项
- 已追踪的项,未提交到 index 的变化(修改,重命名,删除等)
- index 和 HEAD 对应的提交版本的对比
- 新追踪的项
- 已追踪的项,提交到 index 的变化(修改,重命名,删除等)
如果 git status
信息过多,可以加选项
--short
( -s
)得到更简短的状态信息,常见的信息例如
D
代表删除A
代表添加M
代表修改- 处于第一列对应 index 状态和 HEAD 的比较结果
- 处于第二列对应工作目录中的当前状态和 index 的比较结果
??
代表未跟踪的项
例如
1 | M README |
除了简明的信息,还可以使用 git diff
直接显示它们具体的差异
git diff
默认对比 index 和当前工作目录的差异git diff --cached
对比 HEAD 指向的提交版本和 index 的差异
也可以加上文件名来显示指定文件的差异(index和工作目录,或HEAD指向的提交版本和index)
1 | git diff filename |
这里是直接针对文本文件进行的比较,对于那些具有特定数据结构的文件(例如
WORD 的*.docx
文件, Python
的*.ipynb
文件,Mathematica
的*.nb
文件),如果直接使用git diff
,会得到非常混乱的差异比较结果,需要特定的工具提供支持。
注:通常情况下,--cached
和 --staged
是等价的,--staged
是--cached
的别名。但是对于某些命令,输出的结果略有不同,或者只支持其中一个选项。
移除已跟踪项(rm)
如果需要移除已经被 Git 纳入版本控制的文件,单纯在文件系统中执行
rm
是不够的:此时 index
显示文件存在(基于上一次的提交版本),工作目录显示文件不存在,需要将修改反馈给
index,执行git add
会记录这次变化行为到index
1 | rm filename |
除此之外,Git
还提供了专门用于删除的命令:git rm
,可以用于删除已跟踪的项,包括如下两类情景:
git rm
删除工作目录和index中的目标文件,此时目标文件不在工作目录中,index记录该文件被删除1
git rm filename
git rm --cached
删除指定index中的目标文件,此时目标文件仍然存在于工作目录中,但是状态为未跟踪,index记录该文件被删除1
git rm --cached filename
注:
git rm
出于数据安全的考虑,要求被删除的项必须保持在HEAD指向的提交版本/index/工作目录中保持一致,不能存在任何的改动,否则必须加入强制选项才能删除- 如果只希望在工作目录中移除,但是不影响index,直接使用
/bin/rm
即可
移动已跟踪项(mv)
如果需要移动或重命名已经被 Git
纳入版本控制的文件,可以直接在文件系统中 mv
,这个操作会被
Git 检测到并视作移动,但是并没有更新到 index。
Git提供 git mv
命令可以在工作目录中移动或重命名的同时,立刻更新 index
1 | git mv a b |
等效于如下指令
1 | mv a b |
提交快照(commit)
git commit
可以基于当前的index生成一次正式提交:一个
commit 对象,新的commit 对象会指向前一个快照的 commit
对象,将版本链表进一步延申, 然后HEAD指向的当前分支自动前移
1
git commit
commit 对象需要一个非空的 message,Git 会自动启动编辑器来输入 message,通常会启动vim或者记事本打开一个临时文件,临时文件中包括一些注释行,我们可以添加一些文本信息,然后退出编辑器。 如果退出时临时文件是空的或者仅含注释行,即提交信息为空,那么Git不会生成正式提交。
可以使用 -m
选项直接添加 message 字符串
1 | git commit -m "add something" |
可以使用 -a
选项在提交之前,对所有已追踪的项自动添加到
index,相当于自动跳过了 git add
过程,但是注意不会处理未被跟踪的项
1 | git commit -a -m "add all files" |
注意:
- 如果index与上一次提交完全一样时,Git会拒绝这次无意义的提交,因为没有做出任何改动
- 提交对象中除了记录我们手动输入的提交信息,还会记录提交的日期,提交的作者等信息
查看日志(log)
git log
可以查看提交日志,默认是按照时间顺序从新到旧的,呈现每一次提交的commit对象的详细信息(主要是日期,作者和提交信息),如果日志信息过多可以输入q
退出。
有如下常用选项:
--oneline
,简略输出,每一次提交的信息只占据一行,会省略日期--graph
,分支绘图,会通过字符绘制分支的状态,包括合并分叉等等--all
,展示所有分支的日志,否则只会呈现当前分支回溯的日志-<n>
,例如-3
查看最近的 3 次提交--since=2.weeks
--since="2020-01-01"
可以在提交日志中按照时间或其他条件筛选-S key_word
可以在提交日志中筛选:含有字符串key_word
的添加或删除的提交,可以用来关注某个关键词的修改历史-p filename
可以筛选指定文件的日志
例如
1 | git log --oneline --graph --all -5 |
可以使用 HEAD,分支名,或者标签,也可以使用提交对象的哈希值来查看指定的一次提交
1 | git log develop |
清理工作目录(clean)
递归地删除当前目录下所有未被跟踪的文件,但是不包括那些被
.gitignore
匹配忽略的文件
1 | git clean |
这个操作非常危险,因此通常的配置都不允许直接执行,非常依赖额外的选项:
- 使用
-f
选项,可以强制删除(危险) - 使用
-x
选项,则那些被.gitignore
匹配忽略的文件也会被删除 - 使用
-n
选项预演一遍,展示将要清理哪些文件,但是不会执行删除 - 使用
-i
选项会交互式地确认删除哪些文件
1 | git clean -f |
git clean
命令主要用于在编译或某些误操作之后,在工作目录中产生了很多的垃圾文件和临时文件,此时可以直接清理并恢复工作目录。
如果需要删除的内容在目录中,需要加上-d
选项,或者指定位置,例如
1
2
3git clean -di
git clean -i .
归档(archive)
git archive
可以将仓库归档导出为压缩包,压缩包格式支持tar
,tgz
,tar.gz
和zip
,需要在Git仓库目录下操作。
例如,导出HEAD
指向的版本 1
2git archive --format=zip --output=output.zip HEAD
git archive --format=zip -o output.zip HEAD
可以指定导出某个分支的版本 1
git archive --format=tar.gz -o output.tar.gz branch_name
还可以指定导出版本对应的哈希值 1
git archive --format=zip -o output.zip commit_hash
补充:设置命令别名
git 可以给常用的命令以及选项添加别名(不含 git
前缀),直接以字符串形式进行替换。
例如
1 | git config --global alias.ci commit |
这个别名设置使得 ci
被直接替换为 commit
,下面两个命令等效
1 | git ci ... |
例如
1 | git config --global alias.last 'log -1 HEAD' |
这个别名设置使我们可以使用下面的语句直接查看最后一次提交
1 | git last |
这些别名实际会被存储到对应层级的配置文件中,例如 1
2
3[alias]
trace = log --oneline --all --graph
now = status -s -b