虽然用了很久的 Git 了,但基本上只是 add、commit、push、pull,这次系统性的学习了一遍。
整体是按照 Github 官方教程,由牛客网翻译的视频 的顺序排下来的。
内容上外加网上的各种博文总结和补充了一些。
Setup
$ brew install git
Config
Git 希望知道每次提交的作者信息,来说明是谁进行的本次提交,这会随着版本迭代永久纳入历史记录中。
$ git config --global user.name "my name"
$ git config --global user.email xxx@example.com
user.email 最好与 github 中的邮件地址相同,否则 push 后在 github 上并不会准确显示出是自己。
配置文件:
在 ~/.gitconfig
中同样可以修改和查看上述配置,或修改 Git 全局忽略的位置信息等。
SSH
配合 SSH Key 在 Github 中可以免去输入密码的环节。
生成密钥:
$ ssh-keygen -t rsa -C "youremail@example.com"
在 ~/.ssh/id_rsa.pub
中取得公钥并上传 GitHub。
Init
创建本地仓库:
$ git init
Github 上创建仓库,可遵循 Github 的提示:
- create a new repository on the command line:
echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/xxx/test.git
git push -u origin master
git remote add origin https://github.com/xxx/test.git
git push -u origin master
status
查看状态:
$ git status
可以查看目前所有文件的状态,改动文件较多时推荐使用更简洁易读的:
$ git status -s
??
新文件,未跟踪A
新文件,已跟踪M
修改D
删除
接下来是 Git 的提交三部曲:
- 新建或修改文件
- 加入到暂存区
- 提交到仓库
Add
Add:将改动登记到暂存区:
单一文件的新建、修改、删除:
$ git add filename
也可以一次性 add 所有变更:
$ git add .
在简单操作中,add 被理解为把文件添加到暂存区没太大问题,但实际是它暂存的是修改而不是文件。
$ echo aaa > file
$ git add file
$ echo bbb >> file
$ git commit -m "bug fix"
上例中,只有 aaa
这一行的修改被暂存,所以版本库中只有 aaa
,现在使用 diff
命令仍然可以看到 bbb
并没有被提交。
关于 git add .
在谷歌前几页几乎所有的博客中都强调了这一点:
git add -A
= git add -u
+ git add .
(错误的,见下文)
git add .
将文件的修改、新建,登记到暂存区(错误的,见下文)git add -u
将文件的修改、删除,登记到暂存区git add -A
将文件的修改、新建、删除,登记到暂存区
macOS 自带的 git 默认都是 2.20 版本了,在 stackoverflow 中的评论中看到,2.0 版本后 git add .
已经和 git add -A
效果相同。(https://stackoverflow.com/questions/15011311/whats-the-difference-between-git-add-u-and-git-add-a/27854048)
所以在大部分情况,只需要记住 git add .
这一个命令就足够了。
Commit
commit: 将暂存区提交到版本库:
$ git commit -m "message..."
如果不使用 -m 参数填写提交信息,git 会打开一个文本编辑器让你输入
工作区、暂存区、仓库区
diff 在 GUI 中观察更为舒适,但下一章 diff 的三种比较模式可以让新手深入了解三大区的变更状态。
工作区(Workspace)
即你肉眼可见的项目目录。
暂存区(Index/Stage)
一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。它保存了下次将提交的文件列表信息,Git 对暂存区在命名和命令上有多个名字,包括Index,Stage,Cache。
本地仓库(Repository)
版本库即 .git
这个隐藏目录,真正存储各版本代码库的地方。
远程仓库(Remote)
协作开发时或个人备份用,存储在 GitHub 等平台或自建 git 的远端仓库,可用于多方共同开发。
日常操作与几大区的关系:
-> 修改代码、新建文件等操作,工作区发生改动
-> add:将改动内容"登记"到暂存区
-> commit:将暂存区的所有内容提交到本地仓库。
只有 commit 了,我们的代码才真正的进入到 git 仓库了。
-> push: 将本地仓库推送到远程仓库
Diff
三种比较模式
$ git diff
比较对象:工作区 与 暂存区(此时暂存区的内容是上一次提交的内容)
使用情景:日常命令,看看这次改动了什么。
$ git diff --staged
$ git diff --cached # 效果同上
比较对象:暂存区 与 版本库(上一次提交的内容)
使用情景: add
所有改动后,git diff
将不会显示任何东西了,在 commit
之前,仍可以使用 --staged
进行对比。
$ git diff HEAD
比较对象:工作区 与 版本库(上一次提交的内容)
使用情景:比如 add
后,又对这个文件进行了修改,在再次 add
之前,想看看在上一次提交之前都做了哪些改动。
经过自己动手测试,通过 Diff 章节应该能基本掌握工作区、暂存区、版本库之间的关系了。
diff 修饰命令
diff 默认是按行显示区别的,这两个命令会获得一种对长行小改动而言更易读的报告:
$ git diff --color-words
$ git diff --word-diff
不输出代码改动,仅仅告诉你哪些文件遭遇了改动:
$ git diff --stat
Log
了解仓库的提交历史:
$ git log
关于 40 位的 16 进制字符:(Git 保证完整性)
Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
Git 中使用这种哈希值的情况很多,你将经常看到这种哈希值。 实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
$ git log --oneline
每行一个简短的概要:标识符+提交信息
$ git log --patch
在原始 log 显示内容的基础上,增加每次提交的代码改动情况。
$ git log --stat
在原始 log 显示内容的基础上,增加每次提交的代码改动情况。(不输出代码改动,仅仅告诉你哪些文件遭遇了改动,类似 diff 中的 --stat
效果)
$ git log -- directory/filename.ext
后接 --
再接文件名,可以查看仅针对此文件的 log 记录。
常用组合命令
$ git log --graph --all --decorate --oneline
支持分支、标签的查看,快速了解整个项目的发展历程。
Remove
$ git rm
可以不用学习的命令,增删改都可以用 add 命令登记。
$ git rm xxx
该命令完成两件事:
- 删除文件
- 将删除文件这个操作’‘登记’‘到暂存区
如果你在本次变更中只进行了此次操作,可以直接 commit
,不用再 add
登记了。
如果你通过终端执行了 rm
操作 / 在编辑器或 IDE 中执行了删除操作 / 在电脑上直接删除文件:
则只完成了第一步的内容,仍然可以通过 git rm xxx
命令将删除文件这个改动登记到暂存区。
$ git rm --cached
这个命令将删除一个文件,但并不是从系统中真正的删除。只是告诉 git 请不要再跟踪这个文件,这个文件将仍然保留在我的工作区中。
情景说明:当我使用 JetBrains 全家桶时,项目下面会有一个 .idea/
的目录,里面也许会包含一些个人信息或无用信息,这个目录已经被纳入 git 管理了。
目的:并不在本地删除 .idea/
目录及其中文件,只是取消被 git 追踪。
需配合 .gitignore
文件使用。
- 创建
.gitignore
文件,写入一行内容:.idea/
即可。 $ git rm -r --cached .idea/
(-r 是递归目录,如果只是单个文件可取消这个参数)- add 与 commit 完成本次修改
以后即使 .idea/
目录里面发生了文件的增删改,git 也不会理会。
Move
$ git mv file.ext xxx/file.ext
与 rm
类似,同时完成移动与 add。
基本很少用到,一般都用系统自带的文件管理器或 IDE、编辑器来移动文件,然后 add
即可。
建议使用情景:在同一次提交前,既修改了超过 50% 的代码,又想修改文件名。(参考忒修斯之船)
$ git log --stat -M --follow directory/file.ext
加上 -M —follow
参数,可以 log 出文件的移动。
Ship of Theseus (忒修斯之船)
用忒修斯悖论来比喻代码修改:
- 某些 CVS:当移动文件后,它认为只是一个完全不同的东西,无法追踪文件的移动过程。
- 某些企业版 CVS:每个文件都有标识,有个数据库来记录,可以轻易的追踪文件的移动。
- Git:当一个文件至少有 50% 相似度时,这是同一个文件,小于 50% 时,这是一个新文件。
在移动并修改文件后 commit,Git 会反馈出一个百分比:
[master 48f02f2] 本次提交信息
1 file changed, 0 insertions(+), 0 deletions(-)
rename xxx => xxx/xxx (100%)
100% 即代表是纯移动,未做修改。
默认为 50%,低于这个阈值时,Git 会直接显示创建与删除:
[master 9a28f41] 本次提交信息
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 xxx
delete mode 100644 xxx/xxx
50% 这个默认数值是可以修改的,视频只是简单打了个示例代码并未详细说明,我觉得这可能是个几乎永远不会用到的功能。
Ignore
创建 .gitignore
文件并写入规则,来使 Git 去忽略哪些你不想纳入版本控制的文件与文件夹。
在被 gitignore 之前已经被 Git 追踪的文件不受 gitignore 规则的影响。
比如 Python 生成的 __pycache__
文件夹,JB 全家桶的 .idea
文件夹,macOS 的 .DS_Store
文件,都属于无用文件。或者存储了数据库密码的配置文件,是绝对不能纳入版本控制的。
.gitignore 书写规则
# 注释行以 # 开头
# 文件名中有 # 怎么办 -> 加一个反斜杠转义
# 文件名中有 ! 怎么办 -> 加一个反斜杠转义
# 文件名最后有空格怎么办 -> 加一个反斜杠转义
# 忽略文件,直接书写文件名
.DS_Store
# 忽略目录,以 / 结尾
# foo/ 会忽略 foo 目录及此目录下所有内容(但不忽略同名的 foo 文件)
foo/
# 支持通配符 *,忽略所有以 .log 结尾的文件
*.log
# 此文件除外
!a.log
# 如果 a.log 父级目录被忽略,Git 并不会递归这个目录来寻找 a.log
# 即如果 a.log 在 foo/ 中(foo/ 目录已被忽略),写这条规则也没用
# 忽略 bar/ 目录内除了 abc.txt 的文件
bar/*
!abc.txt
# 递归忽略所有 foo 文件和目录
**/foo
# 递归忽略所有 foo 目录及其子目录下的 bar.txt 文件
foo/**/bar.txt
全局忽略
在 ~/.gitconfig
中查看全局忽略文件的配置(默认即可):
...
[core]
excludesfile = /Users/xxx/.gitignore_global
...
如果没有配置,可以复制粘贴上面的,或者:
$ git config --global core.excludesfile ~/.gitignore_global
编辑 ~/.gitignore_global
文件,规则相同。
在 macOS 中安装完 Git,默认的 .gitignore_global
应该是这样的:
*~
.DS_Store
忽略了所有以 ~
结尾的文件,忽略了所有 .DS_Store
文件。
全局忽略适用于那些无论使用任何语言或 IDE 都需要忽略的文件,但是此文件只对自己有效,如果想让忽略规则对协作开发的所有成员有效,需要在项目目录正常建立 .gitignore
文件并纳入版本控制。
查看当前已被忽略的所有内容
$ git ls-files --others --ignored --exclude-standard
强制 add 被 gitignore 忽略的文件
$ git add -f filename
检测 gitignore 规则
在日常 git add .
时发现某一文件并不想忽略,但是已经被忽略了,检测命令:
$ git check-ignore -v filename
Branch
查看分支
$ git branch
显示分支一览表,当前所在分支前面有 * 表示。
创建分支
$ git branch 新分支名
删除分支
$ git branch -d 分支名
如果分支没有完全合并,Git 会抛出一个 error,提示你需要将 -d
改成 -D
。
切换分支
$ git checkout 分支名
Checkout
切换分支
$ git checkout 分支名
创建并切换分支
$ git checkout -b 分支名
相当于同时执行了 git branch 分支名
和 git checkout 分支名
。
Detached head (头指针分离)
先通过 log 查找到某一时刻的 commit 16 进制标识码。
这个命令会让你的工作区会改变为这次提交时的状态:
$ git checkout 12563de6382baaaaf78c58085b4b9abbe5b26608
Git 会提示:
注意:正在检出 '12563de6382baaaaf78c58085b4b9abbe5b26608'。
您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以通过另外
的检出分支操作丢弃在这个状态下所做的任何提交。
如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在检出命令添加
参数 -b 来实现(现在或稍后)。例如:
git checkout -b <新分支名>
HEAD 目前位于 12563de 本次的commit message
这时 checkout 命令相当于一个时光穿梭机,你可以检出任意一次提交时的状态。
注意这不是版本回退。如果你强行修改 + add + commit,Git 会自动帮你创建一个分支。
退出时光穿梭机:
$ git checkout 分支名
Discarding edits (撤销修改)
Merge
合并分支
$ git merge dev
将 dev 分支合并到当前分支,dev 分支会仍然存在。
Fast-forward
- 假设当前只有 master 分支
- 创建并切换到 dev 分支
- 进行了一次或 N 次的修改,但是 master 分支根本没人动
- 切换到 master 分支,合并 dev 分支
即便会导致冲突的地方,也会被 dev 分支的内容覆盖。
这就是 Fast-forward (快进)合并模式,关键点在于 master 分支无人推进。
缺点:历史记录是一条冗长的直线,删除分支后会丢失分支信息。
解决冲突
当 master 和 dev 两条分支都并行推进的时候,在 master 分支合并 dev 分支时,如果同一行都被修改了,就会产生冲突。
自动合并 xxx
冲突(内容):合并冲突于 xxx
自动合并失败,修正冲突然后提交修正的结果。
冲突以 <<<<<<<
=======
>>>>>>>
隔开,分别展示两个分支的不同内容。
<<<<<<< HEAD
master!!!
=======
dev!!!
>>>>>>> dev
取其一或将这一整体变成你想要的内容即可。
--abort
$ git merge dev
... (发现冲突)
$ git merge --abort
--abort
选项会尝试恢复到你运行合并前的状态,相当于你没有进行本次合并。
--suqash
$ git merge --squash dev
$ git commit -m "提交信息"
与直接合并结果相同,但是不提交、不移动 HEAD
,因此需要一条额外的 commit
命令。
合并后 master 分支很干净,只保留一条记录。
删除 dev 分支后,dev 分支信息消失。
--no-ff
$ git merge --no-ff dev -m "提交信息"
禁用 Fast-forward 模式,这样在删除分支后也会保留分支信息。
我觉得这个命令记录的提交内容比较全面且容易理解,但分支多的时候会让历史记录看起来比较混乱。
删除 dev 分支后,dev 分支信息仍然保留。
Network
Remotes
$ git remote add 远端名称 远端地址
# 例如:
$ git remote add origin https://github.com/aaa/bbb
# 修改 URL
$ git remote set-url origin https://github.com/aaa/ccc
# 删除远端链接
$ git remote rm 远端名称
# 例如:
$ git remote rm origin
# 查看所有远端链接
$ git remote -v
# 查看所有远端分支名
$ git branch -r
# 所有远端的分支前都加上了远端名称,例如 origin/master、origin/dev
终于知道 origin 到底是什么意思了。
其实没什么意思,就是约定俗成的,如果你只有一个远端,比如这份代码只放在 Github 上,或只放在 码云 或 Coding 上,那就把远端起名叫 origin,这样大家看到了都懂。
如果你这份代码在本地修改后,既要 push 到 Github,又要 push 到其它某平台,那两个远端名称就可以自己定制。
Fetch, Pull, Push
$ git fetch origin master # 先拉取更新,fetch 不会更改本地工作区内容
$ git merge origin/master master # 将远程内容与本地内容合并
$ git pull origin master # 拉取并合并
$ git push origin master # 将本地内容推送到远程仓库
只有单个远程主机时,一般都命名为 origin。
在任何分支第一次 push 时,Git 会提示:
fatal: 当前分支 xxx 没有对应的上游分支。
为推送当前分支并建立与远程上游的跟踪,使用
git push --set-upstream origin xxx
你可以输入这个命令让本地分支与远程分支建立连接。
也可以在每个分支的第一次 push 时使用 -u
参数:
$ git push -u origin xxx
以后就可以使用简化命令 git push
了。
Push 所有分支到远端:
$ git push --all
有多个远程主机时,比如 Hexo、Hugo 博客可能同时会部署到 GitHub 和 Coding,也会起不同的远程主机名,这时 -u
参数可以指定一个默认主机,不带参数时就默认 push 到这个远程主机。
GitHub 相关章节
GUI:主要是 GitHub Desktop 的介绍,我个人日常操作更喜欢用命令行,但是 GUI 在查看历史进度和 Diff 时更方便快捷。
GitHub:GitHub 的相关介绍
Forking:GitHub 的 Fork 功能相关介绍。
Pull Requests:GitHub 的 Pull Requests 功能相关介绍。
Reset
撤销修改与版本回退
必须理解的概念:
三个区:工作区、暂存区、版本库,或三棵树:Working Directory、Index、HEAD。
→ 改动代码,工作区改动
→ add,将改动登记到暂存区
→ commit,将暂存区提交到版本库,HEAD 指向当前分支的最后一次提交
→ …
→ 又一次 commit,将暂存区提交到版本库,HEAD 依然指向当前分支的最后一次提交
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。
撤销修改
当你对修改不太满意,想恢复初始状态:
# 单个文件撤销
$ git checkout -- 文件名
# 所有文件撤销
$ git checkout .
如果 add
进暂存区了,先撤销暂存区,再撤销工作区:
# 单个文件:
$ git reset HEAD 文件名
$ git checkout -- 文件名
# 多个文件:
$ git reset HEAD
$ git checkout .
如果 commit
进版本库了,那就用 reset
进行版本回退。
Reset
reset 分为三种模式:
--soft
保持本地所有改动(工作区、暂存区不变)--mixed
默认,保持工作副本但重置索引(工作区不变)--hard
丢弃所有改动
它们的主要区别在于作用覆盖的区域(工作区、暂存区)不一样,HEAD 的指向是必变的,在敲完命令后可以用 diff 的三种比较模式去验证。
以一个例子来讲,这是目前的状况。
$ git log --oneline
4a9dabc (HEAD -> master) 3
e285589 2
116eee9 1
$ git reset --soft e285589
- 工作区:无变化
- 暂存区:无变化
- 版本库:HEAD 指向此版本
$ git reset e285589
$ git reset --mixed e285589 # 同上
- 工作区:无变化
- 暂存区:变成此版本
- 版本库:HEAD 指向此版本
$ git reset --hard e285589
- 工作区:变成此版本
- 暂存区:变成此版本
- 版本库:HEAD 指向此版本
压缩提交 / 合并提交
例子:
$ git log --oneline
900db85 (HEAD -> master) 修复刚才修复刚才修复错误导致的错误导致的错误
656915e 修复刚才修复错误导致的错误
2d7e668 修复错误
57335f5 5
ed32739 4
6a15af1 3
e285589 2
116eee9 1
我认为最后三次这种 commit message 应该合并成一个。
$ git reset --soft 57335f5
HEAD 指向了"版本5",但是工作区和暂存区都是目前的状态,直接 commit 即可。
$ git commit -m "fixed a big bug"
再 log 查看:
$ git log --oneline
5827469 (HEAD -> master) fixed a big bug
57335f5 5
ed32739 4
6a15af1 3
e285589 2
116eee9 1
除了用 reset
,在图形化工具 GitHub Desktop 中,选中多个 commit 后点击“Squash n(选中的个数) Commit”,就可以将选中的 n 个提交合并成一个。
Tag
比起混乱的 commit id,标签可以更方便的标记出重要的版本。
狗书《Flask Web 开发》作者配合使用 Tag 功能做了一个相当方便的时光穿梭机,比如第一章第一部分代码使用 1a 作为标签,第二章第三部分代码使用 2c 作为标签,读者只需执行 git checkout 2c
即可查看此次代码。
查看所有标签:
$ git tag
打标签:
# 给最新的 commit 打标签:
$ git tag 标签内容
# 给指定 commit ID 打标签,如:
$ git tag 标签内容 116eee9
# 创建带有说明的标签,用-a指定标签名,-m指定说明文字,可用`git show 标签内容`查看:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb
删除标签:
$ git tag -d 标签内容
删除远端标签:
如果已经 push 到了 GitHub 上,想删除 GitHub 上的标签:
$ git push --delete <远端名成> <标签名称>
例如:
$ git push --delete origin v1.0
默认 push 时不推送标签,推送标签命令:
# 推送单个标签
$ git push 标签内容
# 推送所有标签
$ git push --tags
Rebase (变基)
https://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/rebase
推荐看一些文章后用 https://learngitbranching.js.org/ 这个教程试一试。
一个简洁的工作流教程:https://www.bilibili.com/video/BV19e4y1q7JJ
补充
命令行显示 utf-8 字符
在 git status
等命令下使 Git 使用 utf-8 编码显示中文等字符,而不是\123\123\123\123
这样的格式。
$ git config core.quotepath off
全局:
$ git config --global core.quotepath off
还原:off
改成 on
。
bisect
看到一个 git bisect
命令挺好玩的,用类似二分法去快速定位是哪次提交引起了当前的错误:
提交时关闭 issue
参考:https://stackoverflow.com/questions/60027222/github-how-can-i-close-the-two-issues-with-commit-message
可以使用 close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
等单词,如:
$ git commit -m "巴啦巴啦; close #233"
可以同时关闭多个 issue:
$ git commit -m "closes #1, closes #2, closes #3; YOUR COMMIT MESSAGE"