Git 这个版本控制工具,大家都经常使用,也非常好用。

本文主要记载一些我自己日常使用中偶尔愣掉的情景,以便自己回头看。

Git Pro

《Git Pro》是非常棒的一本 Git 相关的书,以前读过一点,但是现在模模糊糊也就那样了,如果有空可以多读读。不过根据自己的经验来说,还是带着问题去阅读更靠谱。而且现在多语言很详细,可以中文英文对照着看。

推荐先阅读此书前面两章1、Git Start_起步2、Git Basics_基础

一般使用,其实用到的命令不是很多。

Git基础

Git 保存方式

Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流(stream of snapshots)。

git_fIle_system

存储项目随时间改变的快照。

The Three States 三种状态

Git has three main states that your files can reside in.

文件一般处于以下三种状态中的一种。

States 状态Meaning 含义
Committed 已提交表示数据已经安全的保存在本地数据库中。
Modified 已修改表示修改了文件,但还没保存到数据库中
Staged 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

This leads us to the three main sections of a Git project:

git_three_sections

因此引入三个工作区域的概念:

Sections 工作区域Meaning 含义
Git directory
Git仓库
The Git directory is where Git stores the metadata and object database for your project
Git 仓库是 Git 用来保存项目的元数据和对象数据库的地方。
The Working Tree
工作目录
The working tree is a single checkout of one version of the project
工作目录是对项目的某个版本独立提取出来的内容。
Staging area
暂存区域
The staging area is a file, generally contained in your Git directory, that stores information about what will go into your next commit. Its technical name in Git parlance is the “index”.
暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作`‘索引’’,不过一般说法还是叫暂存区域。

The basic Git workflow goes something like this:

  1. You modify files in your working tree.

  2. You selectively stage just those changes you want to be part of your next commit, which adds only those changes to the staging area. 暂存文件,将文件的快照放入暂存区域。

1
   git add
  1. You do a commit, which takes the files as they are in the staging area and stores that snapshot permanently to your Git directory. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
1
   git commit

换种理解

  • Git 仓库就是Commit History:存储着所有提交的版本快照,并由当前分支引用的指针HEAD指向该分支最新一条提交。
  • 工作目录就是一个沙盒,随便你乱七八糟的搞。
  • 索引=暂存区域就是一张照片,所有信息都包含在里面了,但是还没有放到仓库里去。

Git 日常使用

本地仓库上传并连接到 Github

初始化本地仓库

1
2
3
4
touch README.MD			# 创建说明文档
git init			# 初始化本地仓库
git add.			# 添加全部已经修改的文件,准备 commit 提交,等价于 git add -A
git commit -m'提交说明'		# 将修改后的文件提交到本地仓库

连接到远程仓库,并同步

1
2
3
git remote add origin XXX远程仓库地址 	# 连接到 github 上XXX远程仓库,并创建别名为 origin
git push -u origin master 		# 创建一个 upStream(上传流),并将本地代码通过这个 upStream 推送到别名为 origin 的仓库中的 master 分支上
# -u 就是创建upStream上传,并进行绑定,之后 push 和 pull 可以简单化,同时,这个upStream只需要初次推送的时候创建。

-U 参数可以参考Stackoverflow知乎

修改本地代码,推送到 Github

1
2
3
4
5
6
git add.
git commit -m'提交说明'
git pull 
git push		# 如果存在多个远程仓库及多个分支,pull 和 push 要注意
git pull 仓库别名  仓库分支
git push 仓库别名  仓库分支

1、Git如何回滚一次错误的合并

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。

索引

索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的 “暂存区域”,这就是当你运行 git commit 时 Git 看起来的样子。

工作目录

最后,你就有了自己的工作目录。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git文件夹中。 工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。

回滚示例

我们先创建了下面这些文件:

1
2
3
4
├── README.md
├── v1.js
├── v2.js
└── v3.js

对应的 Commit History

1
2
3
4
5
6
7
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

Checkout、reset or revert?

Checket

当我们有了 Commit History之后,我们就可以把对应的照片(快照),从仓库中拿出放到沙盒(Work Directory)里去。

git checkout 可以检出提交、也可以检出单个文件甚至还可以检出分支。

1
git checkout 5aab391

检出v2,此时当前工作目录和5aab391完全一致,你可以查看这个版本的文件编辑、运行、测试都不会被保存到git仓库里面。你可以git checkout master 或者 git checkout -回到原来的工作状态上来。

1
git checkout 5aab391 v1.js

以检出v2版本对于v1.js的改动,只针对v1.js这个文件检出到5aab391版本。所以 它会影响你当前的工作状态,它会把当前状态的v1.js文件内容覆盖为5aab391版本。所以除非你清楚你在做什么,最好不要轻易的做这个操作。但这个操作对于舍弃我当前的所有改动很有用:比如当前我在v1.js上面做了一些改动,但我又不想要这些改动了,而我又不想一个个去还原,那么我可以git checkout HEAD v1.js 或者 git checkout -- v1.js

Reset 重置

1
git reset <file>

从暂存区移除特定文件,但不改变工作目录。它会取消这个文件的缓存,而不覆盖任何更改。

1
git reset

重置暂存区,匹配最近的一次提交,但工作目录不变。它会取消所有文件的暂存,而不会覆盖任何修改,给你了一个重设暂存快照的机会。

1
git reset --hard

加上--hard标记后会告诉git要重置缓存区和工作目录的更改,就是说:先将你的暂存区清除掉,然后将你所有未暂存的更改都清除掉,所以在使用前确定你想扔掉所有的本地工作。

1
git reset <commit>

将当前分支的指针HEAD移到 ,将缓存区重设到这个提交,但不改变工作目录。所有 之后的更改会保留在工作目录中,这允许你用更干净、原子性的快照重新提交项目历史。

1
git reset --hard <commit>

这个要慎重!!!

将当前分支的指针HEAD移到 ,将缓存区和工作目录都重设到这个提交。它不仅清除了未提交的更改,同时还清除了 之后的所有提交。

可以看出,git reset 通过取消缓存或者取消一系列提交的操作会摒弃一些你当前工作目录上的更改,这样的操作带有一定的危险性。

revert 撤销

git revert被用来撤销一个已经提交的快照。但实现上和reset是完全不同的。初始是A提交,通过撤销这个提交B引入的更改,然后加上该撤销了更改的操作的**新提交C **,而不是从项目历史中移除这个提交。

也就是说A –> B –> C

此时工作目录中的文件状态,C 等价于 A,因为 B 的更改,被 C 撤销掉了,C = -B

1
git revert <commit>

生成一个撤消了 引入的修改的新提交,然后应用到当前分支。

git_revert

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

我们在3a395af 引入了一个bug,我们明确是由于3a395af造成的bug的时候,以其我们通过新的提交来fix这个bug,不如git revert, 让他来帮你剔除这个bug。

1
git revert 3a395af

得到结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
cfb71fc Revert "bug"
        |
81f734d commit after bug
        |
3a395af bug
        |
3aa5dfb v3  (<- HEAD)
        |
5aab391 v2
        |
ff7b88e v1
        |
95d7816 init commit

这个时候bug的改动被撤销了,产生了一个新的commit,但是commit after bug没有被清除。

所以相较于resetrevert不会改变项目历史,对那些已经发布到共享仓库的提交来说这是一个安全的操作。其次git revert可以将提交历史中的任何一个提交撤销、而reset会把历史上某个提交及之后所有的提交都移除掉,这太野蛮了。

另外revert的设计,还有一个考量,那就是撤销一个公共仓库的提交。

回滚一个错误的合并

合并操作

相对于常规的commit,当使用git merge <branch>合并两个分支的时候,你会得到一个新的merge commit. 当我们git show <commit>的时候会出现类似信息:

1
2
commit 6dd0e2b9398ca8cd12bfd1faa1531d86dc41021a
Merge: d24d3b4 11a7112

Merge: d24d3b4 11a7112 这行表明了两个分支在合并时,所处的parent的版本线索。

比如在上述项目中我们开出了一个dev分支并做了一些操作,现在分支的样子变成了这样:

1
2
3
init -> v1 -> v2 -> v3  (master)
           \      
            d1 -> d2  (dev)

当我们在dev开发的差不多了

1
2
3
4
#git:(dev)
git checkout master 
#git:(master)
git merge dev

这个时候形成了一个Merge Commit faulty merge

1
2
3
init -> v1 -> v2 -> v3 -- faulty merge  (master)
           \            /
            d1  -->  d2  (dev)

此时faulty merge有两个parent 分别是v3 和 d2。

这个merge之后还继续在dev开发,另一波人也在从别的分支往master合并代码。变成这样:

1
2
3
4
5
init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 (master)
        \  \            /                     /
         \  d1  -->  d2  --> d3 --> d4  (dev)/
          \                                 / 
           c1  -->  c2 -------------------c3 (other)

这个时候你发现, 上次那个merge 好像给共享分支master引入了一个bug。这个bug导致团队其他同学跑不通测试,或者这是一个线上的bug,如果不及时修复老板要骂街了。

这个时候第一想到的肯定是回滚代码,但怎么回滚呢。用reset?不现实,因为太流氓不说,还会把别人的代码也干掉,所以只能用revert。而revert它最初被设计出来就是干这个活的。

怎么操作呢?首先想到的是上面所说的 git revert <commit> ,但是貌似不太行。

1
2
3
git revert faulty merge
error: Commit faulty merge is a merge but no -m option was given.
fatal: revert failed

这是因为试图撤销两个分支的合并的时候Git不知道要保留哪一个分支上的修改。所以我们需要告诉git我们保留那个分支m 或者mainline.

1
git revert -m 1 faulty merge

-m后面带的参数值 可以是1或者2,对应着parent的顺序.上面列子:1代表v3,2代表d2 所以该操作会保留master分支的修改,而撤销dev分支合并过来的修改。

提交历史变为

1
2
3
init -> v1 -> v2 -> v3 -- faulty merge -> v4 -> vc3 -> rev3 (master)
          \            /                     
           d1  -->  d2  --> d3 --> d4  (dev)

此处rev3是一个常规commit,其内容包含了之前在faulty merge撤销掉的dev合并过来的commit的【反操作】的合集。

参考链接:

Git如何回滚一次错误的合并

2、删除仍在VS Code中显示的GitHub上删除的分支?

显然,这个功能是有意的。我发现删除从Github删除的所有远程分支的正确方法是运行以下命令。

1
git fetch --prune

然后重新启动visual studio以从命令选项板中删除分支。

git remote prune origin

https://git-scm.com/docs/git-remote

Deletes all stale remote-tracking branches under . 会清理掉状态2中的远程库已被删除的远程分支,本地库仍存在的 stale remote-tracking branches

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
song@test MINGW64 /d/Git/Temp (master)
$ git branch

* master

song@test MINGW64 /d/Git/Temp (master)
$ git checkout -b dev
Switched to a new branch 'dev'

song@test MINGW64 /d/Git/Temp (dev)
$ git push origin dev
Total 0 (delta 0), reused 0 (delta 0)
To github.com:Song2017/Temp.git

* [new branch]      dev -> dev

song@test MINGW64 /d/Git/Temp (dev)
$ git branch -a -v

* dev                   3902953  add readme.md
  master                3902953  add readme.md
  remotes/origin/HEAD   -> origin/master
  remotes/origin/dev    3902953  add readme.md
  remotes/origin/master 3902953  add readme.md

接下来,在github中删掉dev分支,此时本地版本库中的数据快照仍然有dev分支 git remote prune 会与远程库进行一次同步,最终清理掉版本库中的dev分支,但本地工作区中的dev分支并不会删除。。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
song@test MINGW64 /d/Git/Temp (master)
$ git remote prune origin
Pruning origin
URL: git@github.com:Song2017/Temp.git

* [pruned] origin/dev

song@test MINGW64 /d/Git/Temp (master)
$ git branch -a -v
  dev                   3902953  add readme.md

* master                3902953  add readme.md
  remotes/origin/HEAD   -> origin/master
  remotes/origin/master 3902953  add readme.md

git fetch –prune

https://git-scm.com/docs/git-fetch

Before fetching, remove any remote-tracking references that no longer exist on the remote 同git remote prune

参考链接:

git prune, git remote prune, git fetch –prune 三者异同

总参考链接

  1. Pro Git
  2. Git如何回滚一次错误的合并
  3. git prune, git remote prune, git fetch –prune 三者异同