如何优雅的玩转 Git
如何优雅的玩转 Git
Git 简介
Git 是什么
Git 是一个开源的分布式版本控制系统。
Git 和其它版本控制系统(包括 Subversion 和近似工具)的主要差别在于 Git 对待数据的方式。 从概念上来说,其它大部分系统以文件变更列表的方式存储信息,而 Git 是把数据看作是对小型文件系统的一系列快照。
什么是版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
集中化的版本控制系统
介绍分布式版本控制系统前,有必要先了解一下传统的集中式版本控制系统。
集中化的版本控制系统,诸如 CVS,Subversion 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。
这么做最显而易见的缺点是中央服务器的单点故障。如果宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。要是中央服务器的磁盘发生故障,碰巧没做备份,或者备份不够及时,就会有丢失数据的风险。最坏的情况是彻底丢失整个项目的所有历史更改记录。
分布式版本控制系统
分布式版本控制系统的客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份。
为什么使用 Git
Git 是分布式的。这是 Git 和其它非分布式的版本控制系统(例如 svn,cvs 等),最核心的区别。分布式带来以下好处:
- 工作时不需要联网 - 首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件 A,你的同事也在他的电脑上改了文件 A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。
- 更加安全
- 集中式版本控制系统,一旦中央服务器出了问题,所有人都无法工作。
- 分布式版本控制系统,每个人电脑中都有完整的版本库,所以某人的机器挂了,并不影响其它人。
Git 的工作原理
个人认为,对于 Git 这个版本工具,再不了解原理的情况下,直接去学习命令行,可能会一头雾水。所以,本文特意将原理放在命令使用章节之前讲解。
版本库
当你一个项目到本地或创建一个 git 项目,项目目录下会有一个隐藏的 .git
子目录。这个目录是 git 用来跟踪管理版本库的,如果不熟悉其工作机制,千万不要手动修改。
hooks
目录:包含客户端或服务端的钩子脚本(hook scripts)info
目录:包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在.gitignore
文件中的忽略模式(ignored patterns)。objects
目录:存储所有数据内容。refs
目录:存储指向数据(分支、远程仓库和标签等)的提交对象的指针HEAD
文件:指向目前被检出的分支。index
文件保存暂存区信息。config
文件:包含项目特有的配置选项。description
文件:description
文件仅供 GitWeb 程序使用,我们无需关心。
哈希值
Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能构筑在 Git 底层,是 Git 的关键组件。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。
Git 计算校验和的使用 SHA-1 哈希算法。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希值看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
Git 中使用这种哈希值的情况很多,你将经常看到这种哈希值。 实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
文件状态
在 GIt 中,你的文件可能会处于三种状态之一:
- 已修改(modified) - 已修改表示修改了文件,但还没保存到数据库中。
- 已暂存(staged) - 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
- 已提交(committed) - 已提交表示数据已经安全的保存在本地数据库中。
工作区域
与文件状态对应的,不同状态的文件在 Git 中处于不同的工作区域。
- 工作区(working) - 当你
git clone
一个项目到本地,相当于在本地克隆了项目的一个副本。工作区是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。 - 暂存区(staging) - 暂存区是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作`‘索引’',不过一般说法还是叫暂存区。
- 本地仓库(local) - 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 本地仓库。
- 远程仓库(remote) - 以上几个工作区都是在本地。为了让别人可以看到你的修改,你需要将你的更新推送到远程仓库。同理,如果你想同步别人的修改,你需要从远程仓库拉取更新。
分支管理
Git Flow
Git Flow 应该是目前流传最广的 Git 分支管理策略。Git Flow 围绕的核心点是版本发布(release),它适用于迭代版本较长的项目。
详细内容,可以参考这篇文章:Git 在团队中的最佳实践--如何正确使用 Git Flow
Git Flow 常用分支:
master
- 主线分支develop
- 开发分支feature
- 特性分支release
- 发布分支hotfix
- 问题修复分支
Git Flow 工作流程
2.1. 主干分支
主干分支有两个,它们是伴随着项目生命周期长期存在的分支。
master
- 这个分支对应发布到生产环境的代码。这个分支只允许从其他分支合入代码,不能在这个分支直接修改。所有在 master 分支上的 Commit 都应该打 Tag。develop
- 这个分支包含所有要发布到下一个 release 的代码,这个分支主要是从其他分支合入代码,比如 feature 分支。
feature
分支
2.2. 这个分支主要是用来开发一个新的功能,一旦开发完成,我们合并回 develop 分支进入下一个 release。feature 分支开发结束后,必须合并回 develop 分支, 合并完分支后一般会删点这个 feature 分支,但是我们也可以保留。
release
分支
2.3. release 分支基于 develop 分支创建,创建后,我们可以在这个 release 分支上进行测试,修复 Bug 等工作。同时,其它开发人员可以基于它开发新的 feature (记住:一旦创建了 release 分支之后不要从 develop 分支上合并新的改动到 release 分支)。
发布 release 分支时,合并 release 到 master 和 develop, 同时在 master 分支上打个 Tag 记住 release 版本号,然后可以删除 release 分支了。
hotfix
分支
2.4. 当出现线上 bug 时,也意味着 master 存在 Bug。这时,我们需要基于 master 创建一个 hotfix 分支,在此分支上完成 bug 修复。修复后,我们应该将此分支合并回 master 和 develop 分支,同时在 master 上打一个 tag。所以,hotfix 的改动会进入下一个 release。
2.5. 如何应用 Git Flow
在实际开发中,如何具体落地 Git Flow 流程呢?
git 提供了 git flow
命令来手动管理,但是比较麻烦,所以还是建议使用 Git Flow 的 GUI 工具。比如:SourceTree、VScode 的 GitFlow 插件、Intellij 的 GitFlow 插件等。
想了解更详细的 Git Flow 介绍,可以参考:
Github Flow
对于简单且迭代频繁的项目来说,Git Flow 可能有些太复杂了。这时,可以考虑 Github Flow。
在 Github Flow 策略中,所有分支都是基于 master 创建。在 Feature 或 Bugfix 分支中完成工作后,将其合入 master,然后继续迭代。
想了解更详细的 Github Flow 介绍,可以参考:GitHub Flow
Git Commit 规范
Git 每次提交代码,都要写 Commit message(提交说明),否则就不允许提交。
好的 Commit message 可以让人一眼就明白提交者修改了什么内容,有什么影响;而不好的 Commit message 写了和没写一样,甚至还可能误导别人。
先来看下图中不好的 Commit message 范例,从提交信息完全看不出来修改了什么。
再来一张较好的 Commit message 范例,每次提交的是什么内容,做了什么一目了然。
Commit message 的作用
从前面,我们不难看出,完善的 Commit message 非常有利于项目维护。即时是个人维护的项目,时间久了,可能也会忘记当初自己改了什么。
Commit message 的作用还不仅仅是理解历史信息,它的主要作用如下:
(1)提供易于理解的历史信息,方便检索
(2)可以过滤某些 commit(比如文档改动),便于快速查找信息。
(3)可以直接从 commit 生成 Change log。
Commit message 的规范
开源社区有很多 Commit message 的规范,个人推荐使用 Angular Git Commit 规范,这是目前使用最广的写法,比较合理和系统化,并且有配套的工具。
它主要有以下组成部分:
- 标题行:必填, 描述主要修改类型和内容
- 主题内容:描述为什么修改, 做了什么样的修改, 以及开发的思路等等
- 页脚注释:放 Breaking Changes 或 Closed Issues
常用的修改项
type
:commit 的类型feat
:新特性fix
:修改问题refactor
:代码重构docs
:文档修改style
:代码格式修改, 注意不是 css 修改test
:测试用例修改chore
:其他修改, 比如构建流程, 依赖管理.scope
:commit 影响的范围, 比如:route, component, utils, build...subject
:commit 的概述body
:commit 具体修改内容, 可以分为多行footer
:一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接
Git Commit Template
Intellij 中有集成 Angular Git Commit 规范 的插件,可以帮助我们快速创建符合 Angular Git Commit 规范 的 Git Commit Message。
其使用步骤如下:
第一步,安装插件
第二步,提交代码时,按照模板填写 commit message
生成 Change log
如果你的所有 Commit 都符合 Angular Git Commit 规范,那么发布新版本时,就可以用脚本自动生成 Change log。
生成的文档包括以下三个部分。
- New features
- Bug fixes
- Breaking changes.
每个部分都会罗列相关的 commit ,并且有指向这些 commit 的链接。当然,生成的文档允许手动修改,所以发布前,你还可以添加其他内容。
conventional-changelog 就是生成 Change log 的工具,运行下面的命令即可。
$ npm install -g conventional-changelog
$ cd my-project
$ conventional-changelog -p angular -i CHANGELOG.md -w
上面命令不会覆盖以前的 Change log,只会在CHANGELOG.md
的头部加上自从上次发布以来的变动。
如果你想生成所有发布的 Change log,要改为运行下面的命令。
$ conventional-changelog -p angular -i CHANGELOG.md -w -r 0
为了方便使用,可以将其写入package.json
的scripts
字段。
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0"
}
}
以后,直接运行下面的命令即可。
$ npm run changelog
Git 奇技淫巧
生成 SSH 公钥
许多 Git 服务器都使用 SSH 公钥进行认证。 为了向 Git 服务器提供 SSH 公钥,如果某系统用户尚未拥有密钥,必须事先为其生成一份。 这个过程在所有操作系统上都是相似的。 首先,你需要确认自己是否已经拥有密钥。 默认情况下,用户的 SSH 密钥存储在其 ~/.ssh
目录下。 进入该目录并列出其中内容,你便可以快速确认自己是否已拥有密钥:
$ cd ~/.ssh
$ ls
authorized_keys2 id_dsa known_hosts
config id_dsa.pub
我们需要寻找一对以 id_dsa
或 id_rsa
命名的文件,其中一个带有 .pub
扩展名。 .pub
文件是你的公钥,另一个则是私钥。 如果找不到这样的文件(或者根本没有 .ssh
目录),你可以通过运行 ssh-keygen
程序来创建它们。在 Linux/Mac 系统中,ssh-keygen
随 SSH 软件包提供;在 Windows 上,该程序包含于 MSysGit 软件包中。
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/schacon/.ssh/id_rsa):
Created directory '/home/schacon/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/schacon/.ssh/id_rsa.
Your public key has been saved in /home/schacon/.ssh/id_rsa.pub.
The key fingerprint is:
d0:82:24:8e:d7:f1:bb:9b:33:53:96:93:49:da:9b:e3 schacon@mylaptop.local
首先 ssh-keygen
会确认密钥的存储位置(默认是 .ssh/id_rsa
),然后它会要求你输入两次密钥口令。如果你不想在使用密钥时输入口令,将其留空即可。
现在,进行了上述操作的用户需要将各自的公钥发送给任意一个 Git 服务器管理员(假设服务器正在使用基于公钥的 SSH 验证设置)。 他们所要做的就是复制各自的 .pub
文件内容,并将其通过邮件发送。 公钥看起来是这样的:
$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@mylaptop.local
在你的 Github 账户中,依次点击 Settings > SSH and GPG keys > New SSH key
然后,将上面生成的公钥内容粘贴到 Key
编辑框并保存。至此大功告成。
后面,你在克隆你的 Github 项目时使用 SSH 方式即可。
如果觉得我的讲解还不够细致,可以参考:adding-a-new-ssh-key-to-your-github-account
使用 .gitignore 忽略不必提交内容
.gitignore
文件可能从字面含义也不难猜出:这个文件里配置的文件或目录,会自动被 git 所忽略,不纳入版本控制。
在日常开发中,我们的项目经常会产生一些临时文件,如编译 Java 产生的 *.class
文件,又或是 IDE 自动生成的隐藏目录(Intellij 的 .idea
目录、Eclipse 的 .settings
目录等)等等。这些文件或目录实在没必要纳入版本管理。在这种场景下,你就需要用到 .gitignore
配置来过滤这些文件或目录。
.gitignore
配置的规则很简单,也没什么可说的,看几个例子,自然就明白了。
【示例】一份 Java 的 .gitignore
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
【推荐】这里推荐一个 Github 的开源项目:gitignore,在这里,你可以找到很多常用的 .gitignore 模板,如:Java、Nodejs、C++ 的
.gitignore
模板等等。
使用 .gitattributes 解决 LF 和 CRLF 问题
你有没有在和多人协同开发时遇到过以下烦恼?
开发者们分别使用不同的操作系统进行开发,有的人用 Windows,有的人用 Linux/MacOS。众所周知,不同操作系统默认的文件结尾行是不同的:在 Windows 上默认的是回车换行(Carriage Return Line Feed, CRLF),然而,在 Linux/MacOS 上则是换行(Line Feed, LF)。这就可能导致这种情况:明明文件内容一模一样,但是版本比对时仍然存在版本差异。
那么如何解决这个问题呢?Git 提供了 .gitattributes
配置文件,它允许使用者指定由 git 使用的文件和路径的属性。
在 Git 库中,一个普通文本文件的行尾默认是 LF
。对于工作目录,除了 text
属性之外,还可以设置 eol
属性或 core.eol
配置变量。
.gitattributes
文件中,可以用 text 属性指定某类文件或目录下的文件,控制它的行结束标准化。当一个文本文件被标准化时,它的行尾将在存储库中转换为 LF
。要控制工作目录中使用的行结束风格,请使用单个文件的eol
属性和所有文本文件的 core.eol
配置变量。
【示例】一份 .gitattributes 示例
* text=auto eol=lf
*.txt text
*.java text
*.scala text
*.groovy text
*.gradle text
*.properties text
# unix style
*.sh text eol=lf
# win style
*.bat text eol=crlf
# binary
*.jar binary
*.war binary
*.zip binary
*.tar binary
*.tar.gz binary
*.gz binary
*.apk binary
*.bin binary
*.exe binary
【推荐】这里推荐一个 Github 的开源项目:gitignore,在这里,你可以找到很多常用的 .gitignore 模板,如:Java、Nodejs、C++ 的
.gitignore
模板等等。
同时提交代码到不同的远程仓库
如果,你在不同的 Git 远程仓库中维护同一个项目,你可能会有这样的需求:能不能一次提交,同时 push 到多个远程仓库中呢?
这个可以有,解决方案如下:
比如,我有一个 blog 项目,同时维护在 Github 和 Gitee 上。
(1)首先,在 Github 和 Gitee 上配置本地的 ssh 公钥(如果是 Gitlab,也同样如此),这样中央仓库就能识别本地。
生成 SSH 公钥的方法,请参考上文的 “生成 SSH 公钥” 章节。
(2)进入 git 项目的隐藏目录 .git
,打开 config 文件,参考下面配置进行编辑:
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = git@github.com:dunwu/blog.git
url = git@gitee.com:turnon/blog.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[user]
name = dunwu
email = forbreak@163.com
重点在于 remote "origin"
,同时配置了两个 url。配置后,一旦触发 push 远程仓库的动作,就会同时推送提交记录到配置的远程仓库。
Github Issue 和 Gitlab Issue
开发软件,Bug 再所难免,出现问题不可怕,可怕的是放任不管;所以,优秀的软件项目,都应该管理好问题追踪。软件的使用者,在使用中,可能会遇到形形色色的问题,难以解决,需要向维护者寻求帮助。
问题追踪如此重要,所以各种代码托管平台都会提供 Issue 维护机制,如 Github Issue 和 Gitlab Issue。
如果一个项目的开源社区很活跃,在没有任何约束的前提下,提问肯定是五花八门的,让维护者难以招架。
其实,提问也是一门艺术,如果提问者的问题长篇大幅,言不达意,让别人难以理解,就很难得到有效帮助。关于如何高效的提问,推荐参考 提问的智慧 这篇文章,作者整理的非常好。
作为开发者,你不能期望所有提问者都是训练有素的提问者。所以,使用规范化的 Issue 模板来引导提问者提问,可以大大减轻开发者的负担。
Github Issue 模板
如何在 Github Issue 平台上创建 Issue 模板呢?方法如下:
(1)在仓库根目录创建新目录 .github
(2)在 .github
目录中添加 ISSUE_TEMPLATE
目录,在其中添加的 md 文件都会被 Github 自动识,并将其作为 issue 的默认模板。
示例,下面是携程 apollo 的一个 Issue 模板,要求提问者填充 bug 描述、复现步骤、期望、截图、日志等细节。
Gitlab Issue 模板
如何在 Gitlab Issue 平台上创建 Issue 模板呢?方法如下:
(1)在仓库根目录创建新目录 .gitlab
(2)在 .gitlab
目录中添加 issue_templates
目录,在其中添加的 md 文件都会被 Gitlab 自动识,并将其作为 issue 的默认模板。
Git Hook
在执行提交代码(git commit),推送代码(git push)等行为时,我们可能希望做一些代码检查性工作,例如:代码 lint 检查、代码格式化等。当检查发现代码存在问题时,就拒绝代码提交,从而保证项目质量。
Git 提供了 Git Hook 机制,允许使用者在特定的重要动作发生时触发自定义脚本。有两类钩子:客户端钩子和服务器端钩子。客户端钩子由诸如提交和合并等操作所触发调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。钩子都被存储在 Git 项目目录下的 .git/hooks
子目录中。Git 在这个目录下放置了一些示例,这些示例的名字都是以 .sample
结尾,如果想启用它们,得先移除这个后缀。
常用的客户端钩子:
pre-commit
钩子:在提交信息前运行。 它用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。 如果该钩子以非零值退出,Git 将放弃此次提交,不过你可以用git commit --no-verify
来绕过这个环节。 你可以利用该钩子,来检查代码风格是否一致(运行类似lint
的程序)、尾随空白字符是否存在(自带的钩子就是这么做的),或新方法的文档是否适当。prepare-commit-msg
钩子:在启动提交信息编辑器之前,默认信息被创建之后运行。 它允许你编辑提交者所看到的默认信息。 该钩子接收一些选项:存有当前提交信息的文件的路径、提交类型和修补提交的提交的 SHA-1 校验。 它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如提交信息模板、合并提交、压缩提交和修订提交等非常实用。 你可以结合提交模板来使用它,动态地插入信息。commit-msg
钩子:接收一个参数,此参数即上文提到的,存有当前提交信息的临时文件的路径。 如果该钩子脚本以非零值退出,Git 将放弃提交,因此,可以用来在提交通过前验证项目状态或提交信息。 在本章的最后一节,我们将展示如何使用该钩子来核对提交信息是否遵循指定的模板。post-commit
钩子:在整个提交过程完成后运行。它不接收任何参数,但你可以很容易地通过运行git log -1 HEAD
来获得最后一次的提交信息。 该钩子一般用于通知之类的事情。pre-push
钩子:会在git push
运行期间, 更新了远程引用但尚未传送对象时被调用。 它接受远程分支的名字和位置作为参数,同时从标准输入中读取一系列待更新的引用。 你可以在推送开始之前,用它验证对引用的更新操作(一个非零的退出码将终止推送过程)。
Javascript 应用 Git Hook
想在 JavaScript 应用中使用 Git Hook,推荐使用 husky ,可以很方便的编写钩子处理命令。
使用方法很简单,先安装 husky
npm i -D husky
然后,在 package.json 中添加配置:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
},
以上配置的作用是,当提交代码前( pre-commit
),先执行 lint-staged
;
lint-staged
中执行的动作是,对 src 目录的所有 js、vue 文件进行 eslint 检查,并尝试修复。如果修复后没有问题,就 git add 添加修改后的文件;如果修复失败,则拒绝提交代码。
参考资料
- 官方资源
- 模板
- gitignore 模板 - .gitignore 文件模板
- gitattributes 模板 - .gitattributes 文件模板
- github-cheat-sheet - git 命令简略图表
- Git 教程
- Learn Git branching - 交互式教程
- Git 官方推荐教程 - Scott Chacon 的 Git 书。
- git-tips
- Git 中文教程
- 廖雪峰的 Git 教程
- 有关 git 的学习资源
- 文章
- Git 工具
- guis - Git 官网展示的客户端工具列表。
- gogs - 极易搭建的自助 Git 服务。
- gitflow - 应用 fit-flow 模型的工具。
- firstaidgit.io 一个可搜索的最常被问到的 Git 的问题
- git-extra-commands - 一堆有用的额外的 Git 脚本
- git-extras - GIT 工具集 -- repo summary, repl, changelog population, author commit percentages and more
- git-fire - git-fire 是一个 Git 插件,用于帮助在紧急情况下添加所有当前文件, 做提交(committing), 和推(push)到一个新分支(阻止合并冲突)。
- git-tips - Git 小提示
- git-town - 通用,高级 Git 工作流支持!
- GUI 客户端
- git cheat sheet