Git
Git基础
Git&GitHub
Git基础
版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统
有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态。
就算你乱来一气把整个项目中的文件改的改删的删,你也照样可以轻松恢复到原先的样子。但额外增加的工作量却微乎其微。
你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。
集中式(svn)
svn因为每次存的都是差异,需要的硬盘空间会相对的小一点,可是回滚的速度会很慢
- 优点: 代码存放在单一的服务器上,便于项目的管理
- 缺点: 服务器宕机(员工写的代码得不到保障) 服务器炸了(整个项目的历史记录都会丢失)
分布式(git)
git每次存的都是项目的完整快照,需要的硬盘空间会相对大一点
(Git团队对代码做了极致的压缩,最终需要的实际空间比svn多不了太多,可是Git的回滚速度极快)
- 优点: 完全的分布式
- 缺点: 学习起来比SVN陡峭
托管中心(维护远程库)
- 局域网:可以自己搭建一个GitLab服务器
- 外网:可以使用码云、Github
Git 安装
Git是目前世界上最先进的分布式版本控制系统。
Git地址 : https://git-scm.com/download/win 下载完安装包之后,傻瓜式安装
安装后,桌面右键git bash Here
菜单之后,可以看到一个终端窗口,在终端里面输入命令 git --version
,如果可以看到 git 的版本信息,则说明安装成功。
Git初始化配置
一般在新的系统上,我们都需要先配置下自己的 Git 工作环境。
配置工作只需一次,以后升级时还会沿用现在的配置。当然,如果需要,你随时可以用相同的命令修改已有的配置。
Git 提供了一个叫做git config
的命令来配置或读取相应的工作环境变量而正是由这些环境变量,决定了 Git 在各个环节的具体工作方式和行为。这些变量可以存放在以下三个不同的地方:
/etc/gitconfig
文件:系统中对所有用户都普遍适用的配置。若使用git config
时用system
选项,读写的就是这个文件~/.gitconfig
文件:用户目录下的配置文件只适用于该用户。若使用git config
时用global
选项,读写的就是这个文件。.git/config
文件:当前项目的Git目录中的配置文件(也就是工作目录中的 .git/config 文件)这里的配置仅仅针对当前项目有效。
第一个要配置的是你个人的用户名称和电子邮件地址。
这两条配置很重要,每次Git提交时都会引用这两条信息,说明是谁提交了更新,所以会随更新内容一起被永久纳入历史记录:
1 | git config --global user.name damu |
要检查已有的配置信息,可以使用 git config --list
命令
Git基本操作
Git 的工作就是创建和保存你项目的快照及与之后的快照进行对比。
Git 常用的是以下 6 个命令:git clone、git push、git add 、git commit、git checkout、git pull,后面我们会详细介绍。
说明:
- workspace:工作区
- Index:暂存区/缓存区
- Repository:版本库或本地仓库
- Remote:远程仓库
创建仓库命令
命令 | 说明 |
---|---|
git init | 初始化仓库 |
git clone | 拷贝一份远程仓库,也就是下载一个项目。 |
提交与修改
命令 | 说明 |
---|---|
git add | 添加文件到仓库 git add [file name] |
git status | 查看仓库当前的状态,显示有变更的文件。 |
git diff | 比较文件的不同,即暂存区和工作区的差异。 |
git commit | 提交暂存区到本地仓库。git commit -m “commit message” [file name] |
git reset | 回退版本。 |
git rm | 删除工作区文件。 |
git mv | 移动或重命名工作区文件。 |
前进后退
推荐:git reset --hard [局部索引值]
只能后退:
git reset --hard HEAD^
一个^表示后退一步,n 个表示后退n 步
git reset --hard HEAD~n
表示后退n 步
reset命令的三个参数对比:
soft
参数- 仅仅在本地库移动HEAD 指针
mixed
参数- 在本地库移动HEAD 指针
- 重置暂存区
hard
参数- 在本地库移动HEAD 指针
- 重置暂存区
- 重置工作区
删除文件并找回
用 git rm 来删除文件,同时还会将这个删除操作记录下来;
用 rm 来删除文件,仅仅是删除了物理文件,没有将其从 git 的记录中剔除。
前提:删除前,文件存在时的状态提交到了本地库。
操作:git reset --hard [指针位置]
删除操作已经提交到本地库:指针位置指向历史记录
删除操作尚未提交到本地库:指针位置使用HEAD
比较文件差异
git diff [文件名]
将工作区中的文件和暂存区进行比较git diff [本地库中历史版本] [文件名]
将工作区中的文件和本地库历史记录比较- 不带文件名比较多个文件
提交日志
命令 | 说明 |
---|---|
git log | 查看历史提交记录(多屏显示控制方式:空格向下翻页 b向上翻页 q退出) |
git blame <file> | 以列表形式查看指定文件的历史修改记录 |
git reflog | 可以查看自己的所有分支的所有操作记录(HEAD@{移动到当前版本需要多少步}) |
远程操作
命令 | 说明 |
---|---|
git remote | 远程仓库操作 |
git fetch | 从远程获取代码库 |
git pull | 下载远程代码并合并 |
git push | 上传远程代码并合并 |
1 |
|
分支管理
在版本控制过程中,使用多条线同时推进多个任务。
- 同时并行推进多个功能开发,提高开发效率
- 各个分支在开发过程中,如果某一个分支开发失败,不会对其他分支有任何影响。失败的分支删除重新开始即可。
分支操作
创建分支
git branch [分支名]
查看分支
git branch -v
切换分支
git checkout [分支名]
合并分支
第一步:切换到接受修改的分支(被合并,增加新内容)上 git checkout [被合并分支名]
第二步:执行merge 命令git merge [有新内容分支名]
解决冲突
- 第一步:编辑文件,删除特殊符号
- 第二步:把文件修改到满意的程度,保存退出
- 第三步:
git add [文件名]
- 第四步:
git commit -m "日志信息"
注意:此时commit 一定不能带具体文件名
1 | $ git branch -v |
Git底层概念(底层命令)
哈希
哈希是一个系列的加密算法,各个不同的哈希算法虽然加密强度不同,但是有以下几个共同点:
- 不管输入数据的数据量有多大,输入同一个哈希算法,得到的加密结果长度固定。
- 哈希算法确定,输入数据确定,输出数据能够保证不变
- 哈希算法确定,输入数据有变化,输出数据一定有变化,而且通常变化很大
- 哈希算法不可逆
Git 底层采用的是SHA-1 算法。
Git 就是靠这种机制来从根本上保证数据完整性的。
基础的linux命令
1 | clear:清除屏幕 |
区域
工作区:就是你在电脑里能看到的目录。
暂存区:英文叫 stage 或 index。一般存放在 .git 目录下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。
版本库:工作区有一个隐藏目录 .git,这个不算工作区,而是 Git 的版本库。
新建一个workspace文件夹,在里面右键git bash Here
,输入git init
,查看中勾选隐藏项目
1 | hooks 目录包含客户端或服务端的钩子脚本; |
对象
git对象
Git 的核心部分是一个简单的键值对数据库 。 你可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索该内容。
key:val 组成的键值对(key是val对应的hash)
键值对在git内部是一个blob类型
1 | 向数据库写入内容 并返回对应键值 |
对一个文件进行简单的版本控制
1 | 创建一个新文件并将其内容存入数据库 |
问题:
- 记住文件的每一个版本所对应的 SHA-1 值并不现实
- 在Git中,文件名并没有被保存——我们仅保存了文件的内容
解决方案:树对象
注意:当前的操作都是在对本地数据库进行操作 不涉及暂存区
树对象
树对象( tree object ),它能解决文件名保存的问题,也允许我们将多个文件组织到一起 。 Git 以一种类似于UNIX文件系统的方式存储内容 。所有内容均以树对象和数据对象 (git对象)的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象 (git对象)则大致上对应文件内容。 一个树对象包含了一条或多条记录(每条记录含有一个指向git对象或者子树对象的SHA 1指针以及相应的模式、类型、文件名信息)。一个树对象也可以包含另一个树对象。
我们可以通过update-index;write-tree;read-tree
等命令来构建树对像并塞入到暂存区。
1 | 操作 |
问题:
现在有三个树对象(执行了 三次 write tree),分别代表了我们想要跟踪的不同项目快照。然而问题依旧:若想重用这些快照,你必须记住所有三个SHA-1哈希值。 并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。 而以上这些,正是提交对象( commit object)能为你保存的基本信息。
1 | 查看暂存区当前的样子 |
1 | 解析树对象 |
我们可以认为树对象就是我们项目的快照
提交对象
我们可以通过调用commit-tree 命令创建一个提交对象,为此需要指定一个树对象的SHA-1值,以及该提交的父提交对象(如果有的话, 第一次将暂存区做快照就没有父对象)
1 | 创建提交对象 |
git commit-tree 不但生成提交对象 而且会将对应的快照(树对象)提交到本地库中
GitHub
注册账号
创建远程库
创建远程库地址别名
git remote -v 查看当前所有远程地址别名
git remote add [别名] [远程地址]
1 | $ git remote add origin https://github.com/Lesliewaong/test.git |
推送
git push [别名] [分支名]
1 | git add . |
连接错误解决方法
1 | error: RPC failed; curl 28 OpenSSL SSL_read: Connection was reset, errno 10054 |
1 | git config http.sslVerify "false" |
出错:
1 | ! [rejected] master -> master (fetch first) error: failed to push some refs to ' ...' |
出现这个问题是因为github中的README.md文件不在本地代码目录中,可以通过如下命令进行代码合并
1 | git pull --rebase origin master |
克隆
git clone [远程地址]
效果
- 完整的把远程库下载到本地
- 创建origin 远程地址别名
- 初始化本地库
1 | Lesliewaong@LAPTOP-NT1AH9KS MINGW64 /d/学习Java |
团队成员邀请
拉取
pull=fetch+merge
git fetch [远程库地址别名] [远程分支名]
git merge [远程库地址别名/远程分支名]
git pull [远程库地址别名] [远程分支名]
1 | git checkout master |
1 | $ cat good.txt |
解决冲突
要点
- 如果不是基于GitHub 远程库的最新版所做的修改,不能推送,必须先拉取。
- 拉取下来后如果进入冲突状态,则按照“分支冲突解决”操作解决即可。
跨团队协作
Fork,本地修改,然后推送到远程,Pull Request,对话,审核代码,合并代码,将远程库修改拉取到本地
SSH登录
- 进入当前用户的家目录
cd ~
- 删除.ssh 目录
rm -rvf .ssh
- 运行命令生成.ssh 密钥目录
ssh-keygen -t rsa -C email
[注意:这里-C 这个参数是大写的C] - 进入.ssh 目录查看文件列表
cd .ssh
ls -lF
- 查看id_rsa.pub 文件内容
cat id_rsa.pub
- 复制id_rsa.pub 文件内容,登录GitHub,点击用户头像→Settings→SSH and GPG keys
- New SSH Key
- 输入复制的密钥信息
- 回到Git bash 创建远程地址别名
git remote add origin_ssh ssh地址
- 推送文件进行测试
1 | $ git remote add origin_ssh git@github.com:Lesliewaong/test.git |
Git学习总结
基本概念
- 版本库👉.git
- 当我们使用git管理文件时,比如git init时,这个时候,会多一个**.git**文件,我们把这个文件称之为版本库。
- .git文件另外一个作用就是它在创建的时候,会自动创建master分支,并且将HEAD指针指向master分支。
- 工作区
- 本地项目存放文件的位置。
- 可以理解成图上的workspace。
- 暂存区 (
Index/Stage
)- 顾名思义就是暂时存放文件的地方,通过是通过add命令将工作区的文件添加到缓冲区。
- 本地仓库(
Repository
)- 通常情况下,我们使用commit命令可以将暂存区的文件添加到本地仓库。
- 通常而言,HEAD指针指向的就是master分支。
- 远程仓库(
Remote
)- 举个例子,当我们使用GitHub托管我们项目时,它就是一个远程仓库。
- 通常我们使用clone命令将远程仓库代码拷贝下来,本地代码更新后,通过push托送给远程仓库。
Git文件状态
通常我们需要查看一个文件的状态
1 | git status |
Changes not staged for commit
大概就是工作区有该内容,但是缓存区没有,需要我们git add
Changes to be committed
这个时候,文件放在缓存区了,我们需要git commit
nothing to commit, working tree clean
这个时候,我们将本地的代码推送到远端即可
常见命令
git配置命令
列出当前配置
1
git config --list
git日志过多最后显示end无法输入命令。直接按 q (或+Enter) , 即可退出。
列出Repository配置
1
git config --local --list
列出全局配置
1
git config --global --lis
列出系统配置
1
git config --system --list
通过上述的命令,如果发现你并没有配置用户信息的话,接下来配置一下👇
配置用户名
1
git config --global user.name "your name"
配置用户邮箱
1
git config --global user.email "youremail@github.com"
分支管理
查看本地分支
1
git branch
查看远程分支
1
git branch -r
查看本地和远程分支
1
git branch -a
从当前分支,切换到其他分支
1
git checkout <branch-name> #举个例子 git checkout feature/wangjichenTest
创建并切换到新建分支
1
git checkout -b <branch-name> # 举个例子 git checkout -b feature/tiantian
删除分支
1
git branch -d <branch-name>
当前分支与指定分支合并
1
git merge <branch-name>
查看哪些分支已经合并到当前分支
1
git branch --merged
查看哪些分支没有合并到当前分支
1
git branch --no-merged
查看各个分支最后一个提交对象的信息
1
git branch -v
删除远程分支
1
git push origin -d <branch-name>
重命名分支
1
git branch -m <oldbranch-name> <newbranch-name>
拉取远程分支并创建本地分支
1
2
3git checkout -b 本地分支名x origin/远程分支名x
另外一种方式,也可以完成这个操作。
git fetch origin <branch-name>:<local-branch-name>
fetch指令
将远程仓库内容更新到本地
fetch推荐写法
1 | git fetch origin <branch-name>:<local-branch-name> |
- 一般而言,这个
origin
是远程主机名,一般默认就是origin。 branch-name
你要拉取的分支local-branch-name
通常而言,就是你本地新建一个新分支,将origin下的某个分支代码下载到本地分支。
举个例子👇
1 | git fetch origin feature/template_excellent:feature/template_layout |
fetch其他写法
将某个远程主机的更新,全部取回本地。
1
git fetch <远程主机名>
这样子的话,取回的是所有的分支更新,如果想取回特定分支,可以指定分支名👇
1
git fetch <远程主机名> <分支名>
当你想将 master分支的内容取回到本地下某个分支的话,如下👇
1
2
3git fetch origin :<local-branch-name>
// 等价于👇
git fetch origin master:<local-branch-name>
花式撤销
撤销工作区修改 撤销上一次对文件的操作
1
git checkout -- <file>
暂存区文件撤销 (不覆盖工作区)
1
git reset HEAD
版本回退
1
git reset --(soft | mixed | hard ) < HEAD ~(num) > |
指令 作用范围 –hard 回退全部,包括HEAD,index,working tree –mixed 回退部分,包括HEAD,index –soft 只回退HEAD
状态查询
查看状态
1
git status
查看历史操作记录
1
git reflog
查看日志
1
git log
文档查询
展示Git命令大纲
1
git help / --help
展示Git命令大纲全部列表
- git help -a
展示具体命令说明手册
- git help
文件暂存
添加改动到stash
1
git stash save -a “message”
删除暂存
1
git stash drop <stash@{ID}>
查看stash列表
1
git stash list
删除全部缓存
1
git stash clear
恢复改动
1
git stash pop <stash@{ID}>
差异比较
比较工作区与缓存区
1
git diff
比较缓存区与本地库最近一次commit内容
1
git diff -- cached
比较工作区与本地最近一次commit内容
1
git diff HEAD
比较两个commit之间差异
1
git diff
git add
Git 版本 1.x
命令 | 新文件 | 修改文件 | 已删除的文件 | 描述 |
---|---|---|---|---|
git add -A | ✔️ | ✔️ | ✔️ | 暂存所有(新的、修改的、删除的)文件 |
git add . | ✔️ | ✔️ | ❌ | 仅在当前文件夹中暂存新文件和修改文件 |
git add -u | ❌ | ✔️ | ✔️ | 仅暂存修改和删除的文件 |
Git 版本 2.x
命令 | 新文件 | 修改文件 | 已删除的文件 | 描述 |
---|---|---|---|---|
git add -A | ✔️ | ✔️ | ✔️ | 暂存所有(新的、修改的、删除的)文件 |
git add . | ✔️ | ✔️ | ✔️ | 在当前文件夹中暂存所有(新的、修改的、删除的)文件 |
git add --ignore-removal . | ✔️ | ✔️ | ❌ | 仅暂存新文件和修改后的文件 |
git add -u | ❌ | ✔️ | ✔️ | 仅暂存修改和删除的文件 |
长格式标志:
git add -A
相当于git add --all
git add -u
相当于git add --update
分支命名
master分支
- 主分支,用于部署生产环境的分支,确保稳定性。
- master分支一般由develop以及hotfix分支合并,任何情况下都不能直接修改代码。
develop 分支
- develop为开发分支,通常情况下,保存最新完成以及bug修复后的代码。
- 开发新功能时,feature分支都是基于develop分支下创建的。
feature分支
- 开发新功能,基本上以develop为基础创建feature分支。
- 分支命名:feature/ 开头的为特性分支, 命名规则: feature/user_module、 feature/cart_module。
release分支
- release 为预上线分支,发布提测阶段,会release分支代码为基准提测。
hotfix分支
- hotfix/ 开头的为修复分支,它的命名规则与 feature 分支类似。
- 线上出现紧急问题时,需要及时修复,以master分支为基线,创建hotfix分支,修复完成后,需要合并到master分支和develop分支。
基本操作
有了上述的基本了解后,那么我们就来看看整体的一个流程吧。
创建本地仓库
1
git diff
链接本地仓库与远端仓库
1
2git remote add origin url
origin默认是远端仓库别名 url 可以是可以使用https或者ssh的方式新建检查配置信息
1
2
3git config --list
git config --global user.name "yourname"
git config --global user.email "your_email"生成SSH密钥
1
2
3ssh-keygen -t rsa -C "这里换上你的邮箱"
cd ~/.ssh 里面有一个文件名为id_rsa.pub,把里面的内容复制到git库的我的SSHKEYs中
cat ~/.ssh/id_rsa.pub常看远端仓库信息
1
git remote -v
远端仓库重新命名
1
git remote rename old new
提交到缓存区
1
2git add . 全部上传到缓存区
git add 指定文件提交到本地仓库
1
git commit -m 'some message'
提交远程仓库
1
git push <远程主机名> <本地分支名>:<远程分支名>
查看分支
1
git branch
创建新分支
1
git branch <branch-name>
切换分支
1
git checkout <branch-name>
创建分支并切换
1
git checkout -b <branch-name>
删除分支
1
git branch -d <branch-name>
克隆仓库后切换分支
1
2git clone url //将远程仓库克隆下载到本地
git checkout -b dev origin/dev // 克隆仓库后切换到dev 分支
忽略文件 .gitignore
这个文件的作用,会去忽略一些不需要纳入Git管理这种,我们也不希望出现在未跟踪文件列表。
那么我们来看看如何配置该文件信息。
1 | # 此行为注释 会被Git忽略 |
VSCode中git使用
vscode 拉取git仓库
直接选择克隆存储库,输入你的仓库地址,然后回车选择要存储的位置即可!
使用vscode提交代码
打开下面视图,添加一行文字
## 测试提交
点击对号;等于
git commit -m "备注信息"
;右边的箭头输入需要备注的信息。然后按 Enter 确定。
回车之后,然后我们可以看到。所有的修改的文件,均已经提交到缓存区。1变成了 0;提交到远程仓库;
git push origin master
到git仓库里面;查看。已经成功提交。
Pull Request
什么是Pull Request?
首先需要知道,pull request不是Git核心特性。
相反,是由使用的Git托管平台提供的,GitHub、GitLab、Bitbucket、AzureDevops以及其他平台都提供类似的内置功能。
为什么需要创建pull request?
在我们讨论如何创建完美的pull request的细节之前,先来讨论一下为什么需要这个特性。
假设我们刚刚完成软件的一个新特性,也许之前一直在特性分支中工作,因此下一步将是将其合并到主线分支(master
分支或main
分支)。
在某些情况下,比方说你是项目中唯一的开发人员,或者有足够的经验并确定团队成员不会提出异议,那么直接合并一点问题都没有。
不过如果代码变更稍微复杂一点,并且希望其他人能够检查这部分工作,该怎么办呢?这就是pull request的目的。有了pull request,可以邀请其他人来评论所作的工作并给出反馈。
一旦创建了pull request,就可以和其他开发人员讨论相关代码。
大多数Git托管平台允许其他用户在此过程中添加评论以及提出建议,当评审人员批准后,就可以将其合并到另一个分支中。
评审工作流并不是创建pull request的唯一原因。
如果想对其他没有写访问权限的代码库做出贡献,用pull request就会很方便。想想所有的开源项目,如果你有一个新特性的想法,或者如果想提交一个补丁,pull request是一个很好的方式来展示想法,而不必加入这个项目并成为主要贡献者。
这就引出了一个与pull request紧密相关的话题: fork。
用fork工作
fork是现有Git代码库的个人副本。回到之前关于开源的示例,第一步是创建原始代码库的副本(fork),之后就可以在自己的个人副本中更改代码。
一旦完成,就可以创建一个pull request,要求原始代码库的维护者采用你的更改。维护者或其他主要贡献者可以检查相关代码,然后决定是否采用。
重要提示: Pull request总是基于分支,而不是单个提交!创建pull request时,需要基于一个特定的分支并请求采用。
让审阅者的生活更轻松: 如何创建一个优秀的pull request
一般的工作流程都差不多,包括以下步骤:
- 如果你没有对代码库的写权限,第一步是创建一个
fork
,也就是个人版本的代码库。 - 在fork的代码库中创建新的本地分支。(提示: pull request是基于分支的,而不是提交!)
- 在本地分支中进行变更并提交。
- 将变更推送到自己的远程代码库。
- 创建一个包含相关变更的
pull request
,开始与他人讨论。
我们看看pull request本身,以及如何创建让其他开发人员的生活更轻松的请求。
首先应该简短,以便快速审阅,当面对3000行代码而不是30行代码时,就很难理解代码了。
其次,确保添加良好的、不言自明的标题和有意义的描述。
试着描述做了哪些更改,为什么创建pull request,以及这些更改对项目的影响。
大多数平台都允许添加屏幕截图来帮助展示这些变化。
批准、合并还是拒绝?
一旦变更被批准,你(或具有写访问权的人)就可以将分支合并到主分支中。
但是,如果审阅者不想在当前状态下合并pull request,该怎么办?嗯,你可以等会儿,也可以将新的提交推送到那个分支上,这样现有的pull request也会更新。
此外,维护者或其他具有写访问权限的人可以在不想合并更改时拒绝pull request。
Git 中的撤销
撤销可能是使用过程中最需要的操作,你可能在任何时候都需要撤销。根据情况不同,撤销的命令也是不同的。
撤销最近几次 commit
要撤销最近几次的提交,可以使用 git reset
,下面介绍它的三种模式:soft
、mixed
和 hard
。
假设目前分支情况如下,我们需要撤销到 98c27
commit 上去。
1)soft 模式
执行 git reset --soft 98c27
,git 会首先修改 HEAD 的指向,它会连带修改 HEAD 所在分支的指向:
如上图所示,现在的暂存区和 HEAD 是不同的,这个操作本质上撤销了 2c9be
这个 commit,如同回到了上次准备 commit 的时候。(git 中的时光机!)
此时你可以进行后悔操作,继续修改文件再 add,然后重新 commit,这时会提交一个新的 commit。
2)mixed 模式
回到一开始的时候,假如我们执行的是 git reset --mixed 98c27
,它也会首先修改 HEAD 的指向,使得 HEAD 上的 commit 为 98c27
。
但还不够,git 还会接着更新你的暂存区,如同回到了你准备 add 的时候。(时光机再向前!)
对你来说可能更方便了,继续改就行,然后重新 add、commit。这实际上是 reset 的默认模式,等同于 git reset 98c27
。
3)hard 模式
不用我多说你可能已经意识到 hard 是干什么用的了。这一次 git 摧枯拉朽,把你的 HEAD、暂存区、工作区全给干掉了:
一下子回到了你开始写需求的时候。所以这个命令是 危险 的,除非你真的打算不要这些修改了,否则最好不要用。
不过即使你真的用了又后悔,那也是有办法的,在 git 里面,既然能回到过去,也能在过去穿越到未来。
使用 git reflog
可以查看你最近的修改,找到最前面的 commit id,可以继续使用 reset 穿回去。
合并 commit
有时候你可能发现自己刚才提交的好几个 commit 其实都是中间状态,还不如把它们合并成一个。
根据上面的 reset,实际上就能完成这件事情。
比如下面的场景,我们多提交了一个 File V1.1 的中间版本,希望将其从 commit 历史中去掉:
那其实可以直接 reset 到 v1 版本:
然后重新进行 commit,这样就会将 v1 之后的修改都提交到了新的版本,如同移除了中间的版本。
当然,这个场景也能用 rebase 解决,之后会提到。
挪动 commit
在多个分支上切换开发的时候,有时候会忘记切换分支就开始开发。
当发现自己提交的 commit 放错分支怎么办呢?在 git 中,这也不算个事,通过 git cherry-pick
就能解决。
cherry-pick 可以将指定的 commit “摘到”当前的分支上面,git 会为你重新生成一个 commit,但内容与 pick 的 commit 一致。
如果你要 pick 好几个 commit,它们之间有依赖关系,那需要根据先后顺序依次进行 cherry pick。
当发生冲突时,此时需要修改文件解决冲突,可以使用 git cherry-pick --abort
放弃此次 pick,或者解决完 add 进暂存区,然后使用 git cherry-pick --continue
。注意这里并不是使用 git commit
,如果你需要改变 commit 的信息,可以使用 commit,否则 git 会默认使用 pick 的 commit 的信息。
约定式提交 1.0.0
概述
约定式提交规范是一种基于提交信息的轻量级约定。 它提供了一组简单规则来创建清晰的提交历史; 这更有利于编写自动化工具。 通过在提交信息中描述功能、修复和破坏性变更, 使这种惯例与 SemVer 相互对应。
提交说明的结构如下所示:
原文:
1 | <type>[optional scope]: <description> |
译文:
1 | <类型>[可选 范围]: <描述> |
提交说明包含了下面的结构化元素,以向类库使用者表明其意图:
- fix: 类型 为
fix
的提交表示在代码库中修复了一个 bug(这和语义化版本中的PATCH
相对应)。 - feat: 类型 为
feat
的提交表示在代码库中新增了一个功能(这和语义化版本中的MINOR
相对应)。 - BREAKING CHANGE: 在脚注中包含
BREAKING CHANGE:
或<类型>(范围)
后面有一个!
的提交,表示引入了破坏性 API 变更(这和语义化版本中的MAJOR
相对应)。 破坏性变更可以是任意 类型 提交的一部分。 - 除
fix:
和feat:
之外,也可以使用其它提交 类型 ,例如 @commitlint/config-conventional(基于 Angular 约定)中推荐的build:
、chore:
、ci:
、docs:
、style:
、refactor:
、perf:
、test:
,等等。 - 脚注中除了
BREAKING CHANGE: <description>
,其它条目应该采用类似 git trailer format 这样的惯例。
其它提交类型在约定式提交规范中并没有强制限制,并且在语义化版本中没有隐式影响(除非它们包含 BREAKING CHANGE)。 可以为提交类型添加一个围在圆括号内的范围,以为其提供额外的上下文信息。例如 feat(parser): adds ability to parse arrays.
。
示例
包含了描述并且脚注中有破坏性变更的提交说明
1 | feat: allow provided config object to extend other configs |
包含了 !
字符以提醒注意破坏性变更的提交说明
1 | feat!: send an email to the customer when a product is shipped |
包含了范围和破坏性变更 !
的提交說明
1 | feat(api)!: send an email to the customer when a product is shipped |
包含了 !
和 BREAKING CHANGE 脚注的提交说明
1 | chore!: drop support for Node 6 |
不包含正文的提交说明
1 | docs: correct spelling of CHANGELOG |
包含范围的提交说明
1 | feat(lang): add polish language |
包含多行正文和多行脚注的提交说明
1 | fix: prevent racing of requests |
约定式提交规范
本文中的关键词 “必须(MUST)”、“禁止(MUST NOT)”、“必要(REQUIRED)”、“应当(SHALL)”、“不应当(SHALL NOT)”、“应该(SHOULD)”、“不应该(SHOULD NOT)”、“推荐(RECOMMENDED)”、“可以(MAY)” 和 “可选(OPTIONAL)” ,其相关解释参考 RFC 2119 。
- 每个提交都必须使用类型字段前缀,它由一个名词构成,诸如
feat
或fix
, 其后接可选的范围字段,可选的!
,以及必要的冒号(英文半角)和空格。 - 当一个提交为应用或类库实现了新功能时,必须使用
feat
类型。 - 当一个提交为应用修复了 bug 时,必须使用
fix
类型。 - 范围字段可以跟随在类型字段后面。范围必须是一个描述某部分代码的名词,并用圆括号包围,例如:
fix(parser):
- 描述字段必须直接跟在 <类型>(范围) 前缀的冒号和空格之后。 描述指的是对代码变更的简短总结,例如: fix: array parsing issue when multiple spaces were contained in string 。
- 在简短描述之后,可以编写较长的提交正文,为代码变更提供额外的上下文信息。正文必须起始于描述字段结束的一个空行后。
- 提交的正文内容自由编写,并可以使用空行分隔不同段落。
- 在正文结束的一个空行之后,可以编写一行或多行脚注。每行脚注都必须包含 一个令牌(token),后面紧跟
:<space>
或<space>#
作为分隔符,后面再紧跟令牌的值(受 git trailer convention 启发)。 - 脚注的令牌必须使用
-
作为连字符,比如Acked-by
(这样有助于 区分脚注和多行正文)。有一种例外情况就是BREAKING CHANGE
,它可以被认为是一个令牌。 - 脚注的值可以包含空格和换行,值的解析过程必须直到下一个脚注的令牌/分隔符出现为止。
- 破坏性变更必须在提交信息中标记出来,要么在 <类型>(范围) 前缀中标记,要么作为脚注的一项。
- 包含在脚注中时,破坏性变更必须包含大写的文本
BREAKING CHANGE
,后面紧跟着冒号、空格,然后是描述,例如: BREAKING CHANGE: environment variables now take precedence over config files 。 - 包含在 <类型>(范围) 前缀时,破坏性变更必须通过把
!
直接放在:
前面标记出来。 如果使用了!
,那么脚注中可以不写BREAKING CHANGE:
, 同时提交信息的描述中应该用来描述破坏性变更。 - 在提交说明中,可以使用
feat
和fix
之外的类型,比如:docs: updated ref docs. 。 - 工具的实现必须不区分大小写地解析构成约定式提交的信息单元,只有
BREAKING CHANGE
必须是大写的。 - BREAKING-CHANGE 作为脚注的令牌时必须是 BREAKING CHANGE 的同义词。
为什么使用约定式提交
- 自动化生成 CHANGELOG。
- 基于提交的类型,自动决定语义化的版本变更。
- 向同事、公众与其他利益关系者传达变化的性质。
- 触发构建和部署流程。
- 让人们探索一个更加结构化的提交历史,以便降低对你的项目做出贡献的难度。
FAQ
在初始开发阶段我该如何处理提交说明?
我们建议你按照假设你已发布了产品那样来处理。因为通常总 有人 使用你的软件,即便那是你软件开发的同事们。他们会希望知道诸如修复了什么、哪里不兼容等信息。
提交标题中的类型是大写还是小写?
大小写都可以,但最好是一致的。
如果提交符合多种类型我该如何操作?
回退并尽可能创建多次提交。约定式提交的好处之一是能够促使我们做出更有组织的提交和 PR。
这不会阻碍快速开发和迭代吗?
它阻碍的是以杂乱无章的方式快速前进。它助你能在横跨多个项目以及和多个贡献者协作时长期地快速演进。
约定式提交会让开发者受限于提交的类型吗(因为他们会想着已提供的类型)?
约定式提交鼓励我们更多地使用某些类型的提交,比如 fixes
。除此之外,约定式提交的灵活性也允许你的团队使用自己的类型,并随着时间的推移更改这些类型。
这和 SemVer 有什么关联呢?
fix
类型提交应当对应到 PATCH
版本。feat
类型提交应该对应到 MINOR
版本。带有 BREAKING CHANGE
的提交不管类型如何,都应该对应到 MAJOR
版本。
我对约定式提交做了形如 @jameswomack/conventional-commit-spec
的扩展,该如何版本化管理这些扩展呢?
我们推荐使用 SemVer 来发布你对于这个规范的扩展(并鼓励你创建这些扩展!)
如果我不小心使用了错误的提交类型,该怎么办呢?
当你使用了在规范中但错误的类型时,例如将 feat
写成了 fix
在合并或发布这个错误之前,我们建议使用 git rebase -i
来编辑提交历史。而在发布之后,根据你使用的工具和流程不同,会有不同的清理方案。
当使用了 不在 规范中的类型时,例如将 feat
写成了 feet
在最坏的场景下,即便提交没有满足约定式提交的规范,也不会是世界末日。这只意味着这个提交会被基于规范的工具错过而已。
所有的贡献者都需要使用约定式提交规范吗?
并不!如果你使用基于 squash 的 Git 工作流,主管维护者可以在合并时清理提交信息——这不会对普通提交者产生额外的负担。 有种常见的工作流是让 git 系统自动从 pull request 中 squash 出提交,并向主管维护者提供一份表单,用以在合并时输入合适的 git 提交信息。
约定式提交规范中如何处理还原(revert)提交?
还原提交(Reverting)会比较复杂:你还原的是多个提交吗?如果你还原了一个功能模块,下次发布的应该是补丁吗?
约定式提交不能明确的定义还原行为。所以我们把这个问题留给工具开发者, 基于 类型 和 脚注 的灵活性来开发他们自己的还原处理逻辑。
一种建议是使用 revert
类型,和一个指向被还原提交摘要的脚注:
1 | revert: let us never again speak of the noodle incident |