【Git】版本切换、远程仓库与撤销操作

上一讲主要是讲了一些 Git 的最基本操作,这一讲来讲几个比较高级,但是也很常用的操作。

1. 版本切换

1.1 准备工作

所谓版本切换在实际应用当中十分常见。现在假设我们正在做一个简单的工程,这是我们的工作目录:

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- README.txt
`-- hello.cpp

0 directories, 2 files

两个文件的内容如下。

1
2
3
4
5
6
7
8
9
//hello.cpp
#include <iostream>
using namespace std;

int main()
{
cout << "Hello World!" << endl;
return 0;
}
1
This is just a empty README file!

当前目录的仓库是刚刚被建立的,还没有跟踪文件,所以我们首先做了一些初始化的工作。

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
26
27
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git status
On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)

README.txt
hello.cpp

nothing added to commit but untracked files present (use "git add" to track)

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git add -A
warning: LF will be replaced by CRLF in README.txt.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in hello.cpp.
The file will have its original line endings in your working directory

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git commit -m "Init the project!"
[master (root-commit) 5f74def] Init the project!
2 files changed, 10 insertions(+)
create mode 100644 README.txt
create mode 100644 hello.cpp

然后我们在原先的工程上做了一些修改:

  1. 修改 cpp 文件
  2. 删除了 README 文件
  3. 新加了一个文本文件 Hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- Hello.txt
`-- hello.cpp

0 directories, 2 files

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ cat Hello.cpp
#include <iostream>
using namespace std;

int main()
{
int i;
while (cin >> i)
cout << "Input: " << i << endl;
return 0;
}

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ cat Hello.txt
Hello World!

然后提交更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git add -A
warning: LF will be replaced by CRLF in hello.cpp.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in Hello.txt.
The file will have its original line endings in your working directory

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git commit -m "Modified!"
[master 2a54dcb] Modified!
3 files changed, 4 insertions(+), 2 deletions(-)
create mode 100644 Hello.txt
delete mode 100644 README.txt

现在我们就有两个版本的工程:

1
2
3
4
5
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git log --pretty=oneline
error: cannot spawn more: No such file or directory
2a54dcb33dd018d055878f1e403da24a7c4447dd (HEAD -> master) Modified!
5f74defa90fd658144458187ffeb91d22b2a2621 Init the project!

在多做几个更改,现在就成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git log --pretty=oneline
error: cannot spawn more: No such file or directory
aa57e7c63f772911648b7458eb945690cb9e0e40 (HEAD -> master) Rename hello.cpp to world.cpp
0d09a55e514606174b979f7d8d399594549d6d13 Add two files: 1.txt and 2.txt
2a54dcb33dd018d055878f1e403da24a7c4447dd Modified!
5f74defa90fd658144458187ffeb91d22b2a2621 Init the project!

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- 1.txt
|-- 2.txt
|-- Hello.txt
`-- world.cpp

0 directories, 4 files

有了多个版本,我们现在就可以讲怎么在各个版本之间随意切换了。

1.2 方法1:使用 HEAD 指针

要想切换版本,Git 必须知道当前版本是哪个版本。在Git中,用 HEAD 表示当前版本,也就是最新的提交 aa57e...,上一个版本就是 HEAD^,上上一个版本就是 HEAD^^,当然往上 100 个版本写 100 个 ^ 比较容易数不过来,所以写成 HEAD~100

使用 reset 命令来修改版本:

1
git reset --hard <version>

现在我们正处在第 4 个版本,我们想把它改回到第 2 个版本上,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git reset --hard HEAD^^
HEAD is now at 2a54dcb Modified!

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- Hello.txt
`-- hello.cpp

0 directories, 2 files

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ cat hello.cpp
#include <iostream>
using namespace std;

int main()
{
int i;
while (cin >> i)
cout << "Input: " << i << endl;
return 0;
}

显然版本已经被成功的修改为了原先的第 2 个版本,如果再次查看 log:

1
2
3
4
5
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git log --pretty=oneline
error: cannot spawn more: No such file or directory
2a54dcb33dd018d055878f1e403da24a7c4447dd (HEAD -> master) Modified!
5f74defa90fd658144458187ffeb91d22b2a2621 Init the project!

显然我们发现当前的版本已经指向到了第 2 个版本,但同时我们也发现原先的第 3 个和第 4 个版本都不见了。

1.2 方法2:使用 commit ID

上面那种方法仅适合于从当前版本回退到以前的某个版本。但是如果我们想要重新返回到第 4 个版本该怎么办呢?

还记得这个命令吗。

1
git reset --hard <version>

这里 <version> 不仅仅可以是 HEAD,还可以是 commit ID,也就是我们在查看 log 是前面那一大串字符:

1
2
3
4
5
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git log --pretty=oneline
error: cannot spawn more: No such file or directory
2a54dcb33dd018d055878f1e403da24a7c4447dd (HEAD -> master) Modified!
5f74defa90fd658144458187ffeb91d22b2a2621 Init the project!

在填写的时候不需要把整个字符串全部填进去,只要填写前面几位,能保证可以找到目标就行了。

1
2
3
4
5
6
7
8
9
10
11
12
# 回退到初始版本
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git reset --hard 5f74d
HEAD is now at 5f74def Init the project!

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- README.txt
`-- hello.cpp

0 directories, 2 files

不过我们注意到 git log 只能查看在当前版本之前的记录。我们如何才能查看该版本之后的版本记录号呢?答案是使用命令:

1
git reflog

这个命令可以查看我们所有提交过的记录。

1
2
3
4
5
6
7
8
9
10
# 从下往上依次是版本库的版本记录
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git reflog
error: cannot spawn more: No such file or directory
5f74def (HEAD -> master) HEAD@{0}: reset: moving to 5f74d
2a54dcb HEAD@{1}: reset: moving to HEAD^^
aa57e7c HEAD@{2}: commit: Rename hello.cpp to world.cpp
0d09a55 HEAD@{3}: commit: Add two files: 1.txt and 2.txt
2a54dcb HEAD@{4}: commit: Modified!
5f74def (HEAD -> master) HEAD@{5}: commit (initial): Init the project!

根据第 1 行,我们可以看出当前的 HEAD 指针移动到了 5f74def。而我们的第四个版本是 aa57e7c。所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git reset --hard aa57e7c
HEAD is now at aa57e7c Rename hello.cpp to world.cpp

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- 1.txt
|-- 2.txt
|-- Hello.txt
`-- world.cpp

0 directories, 4 files

搞定!

2. 远程仓库的使用

远程仓库是指托管在因特网或其他网络中的你的项目的版本库。你可以有好几个远程仓库,通常有些仓库对你只读,有些则可以读写。与他人协作涉及管理远程仓库以及根据需要推送或拉取数据。

管理远程仓库包括了解如何添加远程仓库、移除无效的远程仓库、管理不同的远程分支并定义它们是否被跟踪等等。

2.1 查看远程仓库

在一个工作区目录中,我们可以可以运行 git remote 命令来查看你已经配置的远程仓库服务器。

比如对于我们之前克隆出来的项目 mylibgit 而言,如果运行那么至少应该能看到 origin——这是 Git 给你克隆的仓库服务器的默认名字:

1
2
3
E:\MyGithub\mylibgit (master -> origin) (libgit2@0.27.0)
$ git remote
origin

你也可以指定选项 -v,会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。

1
2
3
$ git remote -v
origin https://github.com/libgit2/libgit2 (fetch)
origin https://github.com/libgit2/libgit2 (push)

而对于一个纯粹的本地仓库,那么就不会有显示了。

1
2
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git remote

2.2 添加远程仓库

现在如果我建立了一个远程仓库,并且想把我本地的仓库放到远端去。

我需要在工作区目录里运行 git remote add <shortname> <url> 给当前的仓库添加一个新的远程 Git 仓库,同时指定一个可以轻松引用的简写。

现在我要把仓库加进来了,首先复制链接。

1
2
3
4
5
6
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git remote add Temp git@gitee.com:frostime/Temp.git

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git remote
Temp

2.3 关联到远程仓库

有了远程的仓库,我们就会想将自己的项目推送到远程仓库,需要使用push命令:git push [remote-name][branch-name]

比如我们要把当前的 master 分支推送到 Temp 服务器:

1
2
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git push Temp master

只有当你有隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效


另外注意,当其他人先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先将他们的工作拉取下来并将其合并进你的工作后才能推送

比如我刚才运行的那一条命令就是失败的。

1
2
3
4
5
6
7
8
9
10
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git push Temp master
To gitee.com:frostime/Temp.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'git@gitee.com:frostime/Temp.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

为啥?因为刚才我新建的那个仓库里初始化的时候,它自动添加了两个 readme 文件。必须先拉取下来才行:

1
2
3
4
5
6
7
8
9
10
11
12
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git pull Temp master
warning: no common commits
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
From gitee.com:frostime/Temp
* branch master -> FETCH_HEAD
* [new branch] master -> Temp/master
fatal: refusing to merge unrelated histories

咦,怎么又失败了?我上网上查了一下资料,大体的意思是本地的仓库和远端的库没有任何吻合的地方,所以它不允许你拉下来合并。解决方法是加一个 --allow-unrelated-histories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git pull Temp master --allow-unrelated-histories
From gitee.com:frostime/Temp
* branch master -> FETCH_HEAD
Merge made by the 'recursive' strategy.
README.en.md | 36 ++++++++++++++++++++++++++++++++++++
README.md | 37 +++++++++++++++++++++++++++++++++++++
2 files changed, 73 insertions(+)
create mode 100644 README.en.md
create mode 100644 README.md

zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ tree
.
|-- 1.txt
|-- 2.txt
|-- Hello.txt
|-- README.en.md
|-- README.md
`-- world.cpp

0 directories, 6 files

现在我们再推送上去就不会遇到任何问题了。

1
2
3
4
5
6
7
8
9
10
11
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git push Temp master
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 4 threads
Compressing objects: 100% (12/12), done.
Writing objects: 100% (15/15), 1.46 KiB | 298.00 KiB/s, done.
Total 15 (delta 2), reused 0 (delta 0)
remote: Powered By Gitee.com
To gitee.com:frostime/Temp.git
a4f12d7..2c8cc6f master -> master

2.4 抓取与合并

一种常见的情况是当你和多个人进行合作时,远端仓库此刻的内容和你本地仓库的内容不再匹配。

为了模拟这种情况,我在我测试了远程仓库当中,分别删了一个文件,新增加了一个文件并修改了一个文件。

从远程仓库中获得数据,可以执行:

1
$ git fetch [remote-name]

这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。

必须注意 git fetch 命令会将数据拉取到你的本地仓库 —— 它并不会自动合并或修改你当前的工作。 当准备好时你必须手动将其合并入你的工作。

2.5 查看远程仓库信息

如果想要查看某一个远程仓库的更多信息,可以使用 git remote show [remote-name] 命令:

1
2
3
4
5
6
7
8
9
10
zuoyiping@ZuoYipingPC MINGW64 ~/Desktop/Temp (master)
$ git remote show Temp
* remote Temp
Fetch URL: git@gitee.com:frostime/Temp.git
Push URL: git@gitee.com:frostime/Temp.git
HEAD branch: master
Remote branch:
master tracked
Local ref configured for 'git push':
master pushes to master (local out of date)

它同样会列出远程仓库的 URL 与跟踪分支的信息。

这些信息非常有用,它告诉你正处于 master 分支,并且如果运行 git pull,就会抓取所有的远程引用,然后将远程 master 分支合并到本地 master 分支。 它也会列出拉取到的所有远程引用。

2.6 远程仓库的移除和重命名

如果想要重命名引用的名字可以运行 git remote rename <oldName> <newName> 去修改一个远程仓库的简写名。

1
2
3
4
$ git remote rename Test Demo

$ git remote
Demo

如果因为一些原因想要移除一个远程仓库 - 你已经从服务器上搬走了或不再想使用某一个特定的镜像了,又或者某一个贡献者不再贡献了 - 可以使用 git remote rm

1
$ git remote rm Demo