虽然用了很久的 Git 了,但基本上只是 init、log、add、commit、push、pull。。。

前面是按照 Github 官方教程,由牛客网翻译的视频 的顺序,在各个部分都自行总结和补充了一些内容。

Setup

1
$ brew install git

Config

Git 希望知道每次提交的作者信息,来说明是谁进行的本次提交,这会随着版本迭代永久纳入历史记录中。

1
2
$ 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 来使用 Git 可以免去输入密码的环节。

1
$ ssh-keygen -t rsa -C "youremail@example.com"

~/.ssh/id_rsa.pub 中取得公钥并上传 GitHub。

Init

创建本地仓库:

1
$ git init

Github 上创建仓库,可遵循 Github 的提示:

  • create a new repository on the command line:
1
2
3
4
5
6
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
  • push an existing repository from the command line
1
2
git remote add origin https://github.com/xxx/test.git
git push -u origin master

Commit

Add: 将改动内容登记到暂存区:

1
$ git add 文件

commit: 将暂存区提交到版本库:

1
2
$ git commit -m "message..."
$ git commit # 如果不使用 -m 参数填写提交信息,git 会打开一个文本编辑器让你输入

工作区、暂存区、仓库区

要了解三种 diff 模式,要先大概了解一下这些内容

工作区(Workspace)

即你的项目目录,不包括 .git

暂存区(Index/Stage)

一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。它保存了下次将提交的文件列表信息。

本地仓库(Repository)

版本库即 .git 这个隐藏目录,暂存区也被包含在其中。

远程仓库(Remote)

协作开发时或个人备份用,存储在 GitHub 等同类型网站或自建 git 的远端仓库,可用于多方共同开发。

日常操作与几大区的关系:

-> 修改代码、新建文件等操作,工作区发生改动

-> add:将改动内容”登记”到暂存区

-> commit:将暂存区的所有内容提交到本地仓库。

只有 commit 了,我们的代码才真正的进入到 git 仓库了。

-> push: 将本地仓库推送到远程仓库

Diff

三种比较模式

1
$ git diff

比较对象:工作区 与 暂存区(此时暂存区的内容是上一次提交的内容)


1
2
$ git diff --staged
$ git diff --cached # 效果同上

比较对象:暂存区 与 版本库(上一次提交的内容)

使用情景: add 所有改动后,git diff 将不会显示任何东西了,在 commit 之前,仍可以使用 --staged 进行对比。


1
$ git diff HEAD

比较对象:工作区 与 版本库(上一次提交的内容)

使用情景:比如 add 后,又对这个文件进行了修改,在再次 add 之前,想看看在上一次提交之前都做了哪些改动。


经过自己动手测试,通过 Diff 章节应该能基本掌握工作区、暂存区、版本库之间的关系了。

diff 修饰命令

diff 默认是按行显示区别的,这两个命令会获得一种对长行小改动而言更易读的报告:

1
2
$ git diff --color-words
$ git diff --word-diff


不输出代码改动,仅仅告诉你哪些文件遭遇了改动:

1
$ git diff --stat

Log

了解仓库的提交历史:

1
$ git log

关于 40 位的 16 进制字符:(Git 保证完整性)

Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。

Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:

1
24b9da6552252987aa493b52f8696cd6d3b00373

Git 中使用这种哈希值的情况很多,你将经常看到这种哈希值。 实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。


1
$ git log --oneline

每行一个简短的概要:标识符+提交信息


1
$ git log --patch

在原始 log 显示内容的基础上,增加每次提交的代码改动情况。


1
$ git log --stat

在原始 log 显示内容的基础上,增加每次提交的代码改动情况。(不输出代码改动,仅仅告诉你哪些文件遭遇了改动,类似 diff 中的 --stat 效果)


1
$ git log -- directory/filename.ext

后接 -- 再接文件名,可以查看仅针对此文件的 log 记录。


常用组合命令

1
$ git log --graph --all --decorate --oneline

支持分支、标签的查看,快速了解整个项目的发展历程。

Remove

$ git rm

1
$ git rm xxx

该命令完成两件事:

  1. 删除文件
  2. 将删除文件这个操作”登记”到暂存区

如果你在本次变更中只进行了此次操作,可以直接 commit,不用再 add 登记了。

如果你通过终端执行了 rm 操作 / 在编辑器或 IDE 中执行了删除操作 / 在电脑上直接删除文件:

则只完成了第一步的内容,仍然可以通过 git rm xxx 命令将删除文件这个改动登记到暂存区。


$ 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,并只记住 git add . 这一个命令就足够了。


$ git rm --cached

这个命令将删除一个文件,但并不是从系统中真正的删除。只是告诉 git 请不要再跟踪这个文件,这个文件将仍然保留在我的工作区中。

情景说明:当我使用 JetBrains 全家桶时,项目下面会有一个 .idea/ 的目录,里面也许会包含一些个人信息或无用信息,这个目录已经被纳入 git 管理了。

目的:并不在本地删除 .idea/ 目录及其中文件,只是取消被 git 追踪。

需配合 .gitignore 文件使用。

  1. 创建 .gitignore 文件,写入一行内容:.idea/ 即可。
  2. $ git rm -r --cached .idea/ (-r 是递归目录,如果只是单个文件可取消这个参数)
  3. add 与 commit 完成本次修改

以后即使 .idea/ 目录里面发生了文件的增删改,git 也不会理会。

Move

1
$ git mv file.ext xxx/file.ext

rm 类似,同时完成移动与 add。

基本很少用到,一般都用系统自带的文件管理器或 IDE、编辑器来移动文件,然后 add 即可。


1
$ git log --stat -M --follow directory/file.ext

加上 -M —follow 参数,可以 log 出文件的移动。

Ship of Theseus (忒修斯之船)

忒修斯悖论来比喻代码修改:

  • 某些 CVS:当移动文件后,它认为只是一个完全不同的东西,无法追踪文件的移动过程。
  • 某些企业版 CVS:每个文件都有标识,有个数据库来记录,可以轻易的追踪文件的移动。
  • Git:当一个文件至少有 50% 相似度时,这是同一个文件,小于 50% 时,这是一个新文件。

在移动并修改文件后 commit,Git 会反馈出一个百分比:

1
2
3
[master 48f02f2] 本次提交信息
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename xxx => xxx/xxx (100%)

100% 即代表是纯移动,未做修改。

默认为 50%,低于这个阈值时,Git 会直接显示创建与删除:

1
2
3
4
[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 书写规则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 注释行以 # 开头
# 文件名中有 # 怎么办 -> 加一个反斜杠转义
# 文件名中有 ! 怎么办 -> 加一个反斜杠转义
# 文件名最后有空格怎么办 -> 加一个反斜杠转义

# 忽略文件,直接书写文件名
.DS_Store

# 忽略目录,以 / 结尾
# foo/ 会忽略 foo 目录及此目录下所有内容,但不忽略同名的 foo 文件。
foo/

# 支持通配符 *,忽略所有以 .log 结尾的文件
*.log

# 此文件除外
!a.log
# 如果 a.log 父级目录被忽略,Git 并不会递归这个目录来寻找 a.log
# 即如果 a.log 在 foo/ 中(foo/ 目录已被忽略),写这条规则也没用

# 递归忽略所有 foo 文件和目录
**/foo

# 递归忽略所有 foo 目录及其子目录下的 bar.txt 文件
foo/**/bar.txt

全局忽略

~/.gitconfig 中查看全局忽略文件的配置(默认即可):

1
2
3
4
...
[core]
	excludesfile = /Users/xxx/.gitignore_global
...

编辑 ~/.gitignore_global 文件,规则相同。

在 macOS 中安装完 Git,默认的 .gitignore_global 应该是这样的:

1
2
*~
.DS_Store

忽略了所有以 ~ 结尾的文件,忽略了所有 .DS_Store 文件。

全局忽略适用于那些无论使用任何语言或 IDE 都需要忽略的文件,但是此文件只对自己有效,如果想让忽略规则对协作开发的所有成员有效,需要在项目目录正常建立 .gitignore 文件并纳入版本控制。

查看当前已被忽略的所有内容

1
$ git ls-files --others --ignored --exclude-standard

强制 add 被 gitignore 忽略的文件

1
$ git add -f filename

检测 gitignore 规则

在日常 git add . 时发现某一文件并不想忽略,但是已经被忽略了,检测命令:

1
$ git check-ignore -v filename

Branch

查看分支

1
$ git branch

显示分支一览表,当前所在分支前面有 * 表示。

创建分支

1
$ git branch 新分支名

删除分支

1
$ git branch -d 分支名

如果当前分支没有被完全合并,Git 会抛出一个 warning,提示你需要将 -d 改成 -D

切换分支

1
$ git checkout 分支名

Checkout

切换分支

1
$ git checkout 分支名

创建并切换分支

1
$ git checkout -b 分支名

相当于同时执行了 git branch 分支名git checkout 分支名

Detached head (头指针分离)

先通过 log 查找到某一时刻的 commit 16 进制标识码。

这个命令会让你的工作区会改变为这次提交时的状态:

1
$ git checkout 12563de6382baaaaf78c58085b4b9abbe5b26608

Git 会提示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
注意:正在检出 '12563de6382baaaaf78c58085b4b9abbe5b26608'。

您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以通过另外
的检出分支操作丢弃在这个状态下所做的任何提交。

如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在检出命令添加
参数 -b 来实现(现在或稍后)。例如:

  git checkout -b <新分支名>

HEAD 目前位于 12563de 本次的commit message

这时 checkout 命令相当于一个时光穿梭机,你可以检出任意一次提交时的状态。

注意这不是版本回退。如果你强行修改 + add + commit,Git 会自动帮你创建一个分支。

退出时光穿梭机:

1
$ git checkout 分支名

Discarding edits (撤销修改)

见下面章节

Merge

合并分支

1
$ git merge dev

将 dev 分支合并到当前分支,dev 分支会仍然存在。

Fast-forward

  1. 假设当前只有 master 分支
  2. 创建 dev 分支
  3. 进行了一次或 N 次的修改,但是 master 分支根本没人动
  4. 切换到 master 分支,合并 dev 分支

即便会导致冲突的地方,也会被 dev 分支的内容覆盖。

这就是 Fast-forward (快进)合并模式,关键点在于 master 分支无人推进。

解决冲突

当 master 和 dev 两条分支都并行推进的时候,在 master 分支合并 dev 分支时,如果同一行都被修改了,就会产生冲突。

1
2
3
自动合并 xxx
冲突(内容):合并冲突于 xxx
自动合并失败,修正冲突然后提交修正的结果。

冲突以 <<<<<<< ======= >>>>>>> 隔开,分别展示两个分支的不同内容。

1
2
3
4
5
<<<<<<< HEAD
master!!!
=======
dev!!!
>>>>>>> dev

取其一或将这一整体变成你想要的内容即可。

--abort

1
2
3
$ git merge dev
... (发现冲突)
$ git merge --abort

--abort 选项会尝试恢复到你运行合并前的状态,相当于你没有进行本次合并。

--suqash

1
$ git merge dev

这个命令是正常的合并,会将 dev 分支的所有 commit 都纳入版本控制,log 查看时会阅读到 dev 分支的 commit 记录。

1
$ git merge --squash dev

如果你在 dev 分支上的 commit message 都写成流水账了,为了 log 记录不那么丑陋,为了只保留简单的 release 信息,使用 --squash 参数进行合并,可以丢弃 dev 分支上的 commit 记录。

Network

Remotes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ 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

Fetch, Pull, Push

1
2
$ git fetch origin master # 先拉取更新,fetch 不会更改本地工作区内容
$ git merge origin/master master # 将远程内容与本地内容合并
1
$ git pull origin master # 拉取并合并
1
$ git push origin master # 将本地内容推送到远程仓库

只有单个远程主机时,一般都命名为 origin。

在任何分支第一次 push 时,Git 会提示:

1
2
3
4
fatal: 当前分支 xxx 没有对应的上游分支。
为推送当前分支并建立与远程上游的跟踪,使用

    git push --set-upstream origin xxx

你可以输入这个命令让本地分支与远程分支建立连接。

也可以在每个分支的第一次 push 时使用 -u 参数:

1
$ git push -u origin xxx

以后就可以使用简化命令 git push 了。

Push 所有分支到远端:

1
$ 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 的最简方式,就是将它看做 你的上一次提交 的快照。

撤销修改

当你对修改不太满意,想恢复初始状态:

1
2
3
4
# 单个文件撤销
$ git checkout -- 文件名
# 所有文件撤销
$ git checkout . 

如果 add 进暂存区了,先撤销暂存区,再撤销工作区:

1
2
3
4
5
6
# 单个文件:
$ git reset HEAD 文件名
$ git checkout -- 文件名
# 多个文件:
$ git reset HEAD
$ git checkout .

如果 commit 进版本库了,那就用 reset 进行版本回退。

Reset

reset 分为三种模式:

  • --soft
  • --mixed 默认
  • --hard

它们的主要区别在于作用覆盖的区域(工作区、暂存区、版本库)不一样,在敲完命令后可以用 diff 的三种比较模式去验证。

以一个例子来讲,这是目前的状况。

1
2
3
4
$ git log --oneline
4a9dabc (HEAD -> master) 3
e285589 2
116eee9 1
1
$ git reset --soft e285589
  • 工作区:无变化
  • 暂存区:无变化
  • 版本库:HEAD 指向此版本
1
2
$ git reset e285589
$ git reset --mixed e285589 # 同上
  • 工作区:无变化
  • 暂存区:变成此版本
  • 版本库:HEAD 指向此版本
1
$ git reset --hard e285589
  • 工作区:变成此版本
  • 暂存区:变成此版本
  • 版本库:HEAD 指向此版本

压缩提交

例子:

1
2
3
4
5
6
7
8
9
$ git log --oneline
900db85 (HEAD -> master) 修复刚才修复刚才修复错误导致的错误导致的错误
656915e 修复刚才修复错误导致的错误
2d7e668 修复错误
57335f5 5
ed32739 4
6a15af1 3
e285589 2
116eee9 1

我认为最后三次这种 commit message 应该合并成一个。

1
$ git reset --soft 57335f5

HEAD 指向了”版本5”,但是工作区和暂存区都是目前的状态,直接 commit 即可。

1
$ git commit -m "fixed a big bug"

再 log 查看:

1
2
3
4
5
6
7
$ git log --oneline
5827469 (HEAD -> master) fixed a big bug
57335f5 5
ed32739 4
6a15af1 3
e285589 2
116eee9 1

Tag

比起混乱的 commit id,标签可以更方便的标记出重要的版本。

狗书《Flask Web 开发》作者配合使用 Tag 功能做了一个相当方便的时光穿梭机,比如第一章第一部分代码使用 1a 作为标签,第二章第三部分代码使用 2c 作为标签,读者只需执行 git checkout 2c 即可查看此次代码。


查看所有标签:

1
$ git tag


打标签:

1
2
3
4
5
6
7
8
# 给最新的 commit 打标签:
$ git tag 标签内容

# 给指定 commit ID 打标签,如:
$ git tag 标签内容 116eee9

# 创建带有说明的标签,用-a指定标签名,-m指定说明文字,可用`git show 标签内容`查看:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb


删除标签:

1
$ git tag -d 标签内容


默认 push 时不推送标签,推送标签命令:

1
2
3
4
5
# 推送单个标签
$ git push 标签内容

# 推送所有标签
$ git push --tags

Rebase (变基)

https://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/rebase

推荐看一些文章后用 https://learngitbranching.js.org/ 这个教程试一试。