- 版本控制系统 (VCSs,Version Control System) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
★Lecture 6 Version Control(git)
Git - Book (git-scm.com) :Git 详细的学习资料
版本控制系统 (VCSs,Version Control System) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
为什么说版本控制系统非常有用?即使您只是一个人进行编程工作,它也可以帮您创建项目的快照,记录每个改动的目的、基于多分支并行开发等等。和别人协作开发时,它更是一个无价之宝,您可以看到别人对代码进行的修改,同时解决由于并行开发引起的冲突。
Git 把数据看作是对小型文件系统的一系列快照。 在 Git 中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流。
现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:
- 当前模块是谁编写的?
- 这个文件的这一行是什么时候被编辑的?是谁作出的修改?修改原因是什么呢?
- 最近的1000个版本中,何时/为什么导致了单元测试失败?
尽管版本控制系统有很多, 其事实上的标准则是 Git 。
因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。很多时候您只能死记硬背一些命令行,然后像使用魔法一样使用它们,一旦出现问题,就只能像上面那幅漫画里说的那样去处理了。
尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。丑陋的接口只能靠死记硬背,而优雅的底层设计则非常容易被人理解。因此,我们将通过一种自底向上的方式向您介绍 Git。我们会从数据模型开始,最后再学习它的接口。一旦您搞懂了 Git 的数据模型,再学习其接口并理解这些接口是如何操作数据模型的就非常容易了。
★Git 的数据模型
- 进行版本控制的方法很多。
Git
拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git
将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在Git
的术语里,文件(file)被称作 Blob 对象(数据对象),也就是一组数据。目录(folder)则被称之为“树”(tree),它将名字与 Blob 对象或树对象(使得目录中可以包含其他目录)进行映射。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:1
2
3
4
5
6
7<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个blob对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
历史记录建模:关联快照
版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过处于种种原因,
Git
并没有采用这样的模型。在
Git
中,历史记录是一个由快照组成的有向无环图。有向无环图,听上去似乎是什么高大上的数学名词。不过不要怕,您只需要知道这代表Git
中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。在
Git
中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:1
2
3
4o <-- o <-- o <-- o
^
\
--- o <-- o上面是一个 ASCII 码构成的简图,其中的
o
表示一次提交(快照)。箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样:
1
2
3
4o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- oGit
中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
数据模型及其伪代码表示
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
1
2
3
4
5
6
7
8
9
10
11
12
13// 文件就是一组数据
type blob = array<byte>
// 一个包含文件和目录的目录
type tree = map<string, tree | blob> //string 代表了文件夹名或文件名
// 每个提交都包含父辈,元数据和顶层树
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree //最顶层的树 -> root
}
对象和内存寻址
git cat-file -p SHA-1
Git
中的对象可以是 blob、树或提交:1
type object = blob | tree | commit
Git
在储存数据时,所有的对象都会基于它们的 SHA-1(Secure Hash Algorithm - 1) 哈希进行寻址。1
2
3
4
5
6
7
8objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
提交(
commit
)、树(tree
)和文件(blob
)的关系为:我们先通过
commit
寻找到tree
的信息,每个commit
都会存储对应的tree ID
;
通过
tree
存储的信息,获取到对应的目录树信息;
根据从
tree
中获得的blob
的ID
,通过blob ID
获取对应的文件内容。
1
2
3
4
5
6
7<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")例如,上面例子中的树(可以通过
git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
来进行可视化(-p
: print),698281bc680d1995c5f4caaf3359721a5a58d48d
是<root> (tree)
的 sha1 值),看上去是这样的:1
2100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo树本身会包含一些指向其他内容的指针,例如
baz.txt
(blob) 和foo
(树)。如果我们用git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,即通过哈希值查看baz.txt
的内容,会得到以下信息:1
git is wonderful
引用
master、HEAD
现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,
Git
的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,**master
引用通常会指向主分支的最新一次提交**。1
2
3
4
5
6
7
8
9
10
11
12
13references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)这样,
Git
就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要再使用一长串十六进制字符了。有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在
Git
中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
仓库
最后,我们可以粗略地给出
Git
仓库的定义了:**对象
和引用
**。在硬盘上,
Git
仅存储对象和引用:因为其数据模型仅包含这些东西。所有的git
命令都对应着对提交树的操作,例如增加对象(不存在说删除对象或修改对象),增加或删除引用。你执行的 Git 操作,几乎只往 Git 数据库中 添加 数据。 你很难使用 Git 从数据库中删除数据,也就是说 Git 几乎不会执行任何可能导致文件不可恢复的操作。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交
5d83f9e
时,有什么命令可以完成该操作(针对这个具体问题,您可以使用git checkout master; git reset --hard 5d83f9e
)
★暂存区
Git
中还包括一个和数据模型完全不相关的概念,但它却是创建提交的接口的一部分。就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但
Git
不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。Git
处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括哪些改动。Git 有三种状态,你的文件可能处于其中之一: 已提交(committed)、已修改(modified) 和 已暂存(staged)。
- 已修改表示修改了文件,但还没保存到数据库中。
- 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
- 已提交表示数据已经安全地保存在本地数据库中。
这会让我们的 Git 项目拥有三个阶段:工作区、暂存区以及 Git 目录。

- 工作区是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
- 暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 按照 Git 的术语叫做“索引”,不过一般说法还是叫“暂存区”。
- Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。
基本的 Git 工作流程如下:
- 在工作区中修改文件。
- 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。
- 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。
★Git 的命令行接口
基础
git init、git status、git add、git rm、git commit、git log、git diff、git checkout、git mv
git help <command>
: 获取git
命令的帮助信息,如git help init
git init
: 创建一个新的git
仓库,其数据会存放在一个名为.git
的目录下git status
: 显示当前的仓库状态git add <filename>
: 添加文件到暂存区git add
命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。git add
是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。 将这个命令理解为 “精确地将内容添加到下一次提交中” 而不是 “将一个文件添加到项目中” 要更加合适。
git rm <filename>
:从 Git 中移除某个文件,并连带从工作目录中删除指定的文件,下一次提交时,该文件就不再纳入版本管理了。git rm
命令后面可以列出文件或者目录的名字,也可以使用glob
模式。比如:1
git rm log/\*.log
注意到星号
*
之前的反斜杠\
, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除log/
目录下扩展名为.log
的所有文件。-f
:如果要删除之前修改过或已经放到暂存区的文件,则必须使用强制删除选项-f
(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删尚未添加到快照的数据,这样的数据不能被 Git 恢复。-cached
:另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。
git commit
:创建一个新的提交,并启动你选择的文本编辑器来输入提交说明。git log
: 显示历史日志git log -p
:显示每次提交所引入的差异(按 补丁 的格式输出(p -> patch))。 你也可以限制显示的日志条目数量,例如使用-2
选项来只显示最近的两次提交:git log -p -2
git log --all --graph --decorate
: 可视化历史记录(有向无环图)git log --all --graph --decorate --oneline
: 以简洁的方式可视化历史记录(有向无环图)
git diff <filename>
: 显示当前文件与暂存区文件的差异,也就是修改之后还没有暂存起来的变化内容。注意:**
git diff
本身只显示尚未暂存的改动,而不是自上次提交以来所做的所有改动**。 所以有时候你一下子暂存了所有更新过的文件,运行git diff
后却什么也没有,就是这个原因。git diff --staged
:这条命令将比对已暂存文件与最后一次提交的文件差异。(git diff --staged
==git diff --cached
)git diff <v1> <v2> <filename>
: 显示某个文件两个版本之间的差异(v2 相对于 v1 来说,有哪些差异)
git checkout <revision>
: 更新 HEAD 和目前的分支1
2
3
4
5
6
7
8
9
10
11
12
13running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate
* commit 5704a02ff48a8c6ad19f6d753c4868dae2f7ab13 (HEAD -> master)
| Author: Running-Noob <fang_zhuoyue@163.com>
| Date: Wed Feb 22 20:17:20 2023 +0800
|
| add new directory "new dir"
|
* commit 831daf5a811b23398317fa2de9a079abe6f1bc2a
Author: Running-Noob <fang_zhuoyue@163.com>
Date: Wed Feb 22 20:06:21 2023 +0800
this is my hello.txt file上面的例子中,有两次
commit
,对应于Git的数据模型
中的commit
对象,此时,HEAD
和master
都指向最新的commit
。注意:最新的一次
commit
在最上面。但是如果执行下面的命令,然后再次
git log --all --graph --decorate
,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
28
29
30running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git checkout 831daf5a811b23398317fa2de9a079abe6f1bc2a
注意:正在切换到 '831daf5a811b23398317fa2de9a079abe6f1bc2a'。
您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以在切换
回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响。
如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在 switch 命令
中添加参数 -c 来实现(现在或稍后)。例如:
git switch -c <新分支名>
或者撤销此操作:
git switch -
通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议
HEAD 目前位于 831daf5 this is my hello.txt file
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate
* commit 5704a02ff48a8c6ad19f6d753c4868dae2f7ab13 (master)
| Author: Running-Noob <fang_zhuoyue@163.com>
| Date: Wed Feb 22 20:17:20 2023 +0800
|
| add new directory "new dir"
|
* commit 831daf5a811b23398317fa2de9a079abe6f1bc2a (HEAD)
Author: Running-Noob <fang_zhuoyue@163.com>
Date: Wed Feb 22 20:06:21 2023 +0800
this is my hello.txt file发现由于使用
git checkout 831daf5a811b23398317fa2de9a079abe6f1bc2a
,使得当前位置 “HEAD” 指向了第一次commit
的地方,此时进行的操作就都是基于第一次commit
时的状态了。1
2running_noob@RNoob-VM:~/桌面/the missing semester/git_$ ls
hello.txt此时没有了 “
new dir
“ 文件夹,当再次回到最新的commit
时,”new dir
“ 文件夹又出现了。1
2
3
4
5running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git checkout 5704a02ff48a8c6a
之前的 HEAD 位置是 831daf5 this is my hello.txt file
HEAD 目前位于 5704a02 add new directory "new dir"
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ ls
hello.txt 'new dir'git mv file_from file_to
:Git 并不显式跟踪文件移动操作,但会推断出究竟发生了什么,如文件重命名或移动文件。看下面例子:
1
2
3
4
5
6
7
8git mv README.md README
git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README其实,运行
git mv
就相当于运行了下面三条命令:1
2
3mv README.md README
git rm README.md
git add README如此分开操作,Git 也会意识到这是一次重命名,所以不管何种方式结果都一样。 两者唯一的区别在于,
git mv
是一条命令而非三条命令,直接使用git mv
方便得多。 不过在使用其他工具重命名文件时,记得在提交前git rm
删除旧文件名,再git add
添加新文件名。
分支和合并
git branch、git checkout -b、git merge
git branch
: 显示分支git branch <name>
: 创建分支git checkout -b <name>
:创建分支并切换到该分支- 相当于
git branch <name>; git checkout <name>
- 相当于
git merge <revision>
: 合并到当前分支git merge --abort
:会抛弃合并过程并且尝试重建合并前的状态git merge --continue
:该命令在首先验证是否有合并要完成后,只运行git commit
。
git mergetool
: 使用工具来处理合并冲突git rebase
: 将一系列补丁变基(rebase)为新的基线为了方便演示
git
的分支和合并命令,我们用.py
程序而不是.txt
文件来进行演示过程:分支过程:
首先创建
hello.py
文件,其内容如下:1
2
3
4
5
6
7
8
9
10
11#! usr/bin/env python
import sys
def default():
print("hello")
def main():
default()
if __name__ == '__main__':
main()然后
git checkout -b cat
创建 ”cat“ 分支并进入这个分支,对hello.py
文件进行修改:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#! usr/bin/env python
import sys
def default():
print("hello")
def cat():
print("cat!")
def main():
if sys.argv[1] == 'cat':
cat()
else:
default()
if __name__ == '__main__':
main()然后提交,再
git log --all --graph --decorate
,显示目前git
结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16* commit da87499922490632ccb62bf64b138722c4908ec5 (HEAD -> cat)
| Author: Running-Noob <fang_zhuoyue@163.com>
| Date: Thu Feb 23 14:33:23 2023 +0800
|
| add branch cat
|
* commit 9b3194f792b0bad3f837209842fdcf2bdb6d0494 (master)
| Author: Running-Noob <fang_zhuoyue@163.com>
| Date: Thu Feb 23 14:25:43 2023 +0800
|
| add hello.py
|
* commit c006e500746f9956b1e47a60d95867d0ee03d9aa
| Author: Running-Noob <fang_zhuoyue@163.com>
| Date: Wed Feb 22 21:43:41 2023 +0800
......回到
master
分支:git checkout master
,并创建新的分支 ”dog“ 并进入:git checkout -b dog
,同样对hello.py
文件进行修改:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#! usr/bin/env python
import sys
def default():
print("hello")
def dog():
print("dog!")
def main():
if(sys.argv[1] == 'dog'):
dog()
else:
default()
if __name__ == '__main__':
main()然后提交,再
git log --all --graph --decorate --oneline
,显示目前git
结构如下:1
2
3
4
5
6
7* aa43e4b (HEAD -> dog) add dog func
| * da87499 (cat) add branch cat
|/
* 9b3194f (master) add hello.py
* c006e50 add new thing to hello.txt
* 5704a02 add new directory "new dir"
* 831daf5 this is my hello.txt file现在我们有两个分支:”cat“ 和 ”dog“,”cat“ 分支实现了
cat
函数,”dog“ 分支实现了dog
函数,接下来我们要把这两个分支中的函数合并起来。合并过程:
首先回到
master
:git checkout master
,git
结构如下:1
2
3
4
5
6
7* aa43e4b (dog) add dog func
| * da87499 (cat) add branch cat
|/
* 9b3194f (HEAD -> master) add hello.py
* c006e50 add new thing to hello.txt
* 5704a02 add new directory "new dir"
* 831daf5 this is my hello.txt file我们先将 “cat” 分支合并:
git merge cat
,然后git
此时结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge cat
更新 9b3194f..da87499
Fast-forward
hello.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
* aa43e4b (dog) add dog func
| * da87499 (HEAD -> master, cat) add branch cat
|/
* 9b3194f add hello.py
* c006e50 add new thing to hello.txt
* 5704a02 add new directory "new dir"
* 831daf5 this is my hello.txt file然后再将 ”dog“ 分支合并:
git merge dog
,会发现出现合并冲突:1
2
3
4running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge dog
自动合并 hello.py
冲突(内容):合并冲突于 hello.py
自动合并失败,修正冲突然后提交修正的结果。浏览此时的
hello.py
文件,发现文件内容如下: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#! usr/bin/env python
import sys
def default():
print("hello")
<<<<<<< HEAD
def cat():
print("cat!")
def main():
if sys.argv[1] == 'cat':
cat()
=======
def dog():
print("dog!")
def main():
if(sys.argv[1] == 'dog'):
dog()
>>>>>>> dog
else:
default()
if __name__ == '__main__':
main()”=======“ 上面的部分表示 ”HEAD“ 分支中
hello.py
的内容,下面的部分表示 ”dog“ 分支中hello.py
的内容,正是这里起了合并冲突。针对合并冲突,我们可以选择放弃此次合并:
git merge --abort
,回到合并前的状态:1
2
3
4
5
6
7
8
9running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge --abort
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
* aa43e4b (dog) add dog func
| * da87499 (HEAD -> master, cat) add branch cat
|/
* 9b3194f add hello.py
* c006e50 add new thing to hello.txt
* 5704a02 add new directory "new dir"
* 831daf5 this is my hello.txt file也可以对冲突的内容进行人工修改,然后完成合并:
1
2
3
4
5
6
7
8
9
10
11
12
13
14running_noob@RNoob-VM:~/桌面/the missing semester/git_$ vim hello.py
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git add hello.py
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge --continue
[master 0536c80] Merge branch 'dog'
running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
* 0536c80 (HEAD -> master) Merge branch 'dog'
|\
| * aa43e4b (dog) add dog func
* | da87499 (cat) add branch cat
|/
* 9b3194f add hello.py
* c006e50 add new thing to hello.txt
* 5704a02 add new directory "new dir"
* 831daf5 this is my hello.txt file人工修改的
hello.py
的内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#! usr/bin/env python
import sys
def default():
print("hello")
def cat():
print("cat!")
def dog():
print("dog!")
def main():
if sys.argv[1] == 'cat':
cat()
elif sys.argv[1] == 'dog':
dog()
else:
default()
if __name__ == '__main__':
main()
远端操作
- 远程仓库是指托管在因特网或其他网络中的你的项目的版本库。
git remote、git push、git fetch、git pull、git remote rename、git remote remove、git clone
git remote
: 列出远端,查看你已经配置的远程仓库服务器git remote -v
:你也可以指定选项-v
,该命令会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。1
2
3running_noob@RNoob-VM:~/桌面/the missing semester/test$ git remote -v
origin https://github.com/Running-Noob/test.git (fetch)
origin https://github.com/Running-Noob/test.git (push)git remote add <shortName> <url>
:添加一个新的远程 Git 仓库,同时指定一个方便使用的简写,由此就可以在命令行中使用简写来代替整个 URL。git remote show <remote>
:查看某一个远程仓库的更多信息,例如:1
2
3
4
5
6
7
8
9
10
11
121@Running-Noob MINGW64 ~/Desktop/test (main)
git remote show origin
* remote origin
Fetch URL: https://github.com/Running-Noob/test.git/
Push URL: https://github.com/Running-Noob/test.git/
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)它同样会列出远程仓库的 URL 与跟踪分支的信息。 这些信息非常有用,它告诉你正处于
main
分支,并且如果运行git pull
, 就会抓取所有的远程引用,然后将远程main
分支合并到本地main
分支。 它也会列出拉取到的所有远程引用。
git push <remote> <local branch>:<remote branch>
: 将对象传送至远端(例如GitHub
)并更新远端引用git branch --set-upstream-to=<remote>/<remote branch>
: 创建本地和远端分支的关联关系git fetch
: 从远端获取对象/索引,若要从远程仓库中获得数据,可以执行该命令这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
如果你使用
clone
命令克隆了一个仓库,命令会自动将其添加为远程仓库并默认以 “origin” 为简写。 所以,git fetch origin
会抓取克隆(或上一次抓取)后新推送的所有工作。 必须注意git fetch
命令只会将数据下载到你的本地仓库——它并不会自动合并或修改你当前的工作。 当准备好时你必须手动将其合并入你的工作。git pull
: 相当于git fetch; git merge
该命令可以自动抓取该远程分支并合并到当前分支,默认情况下,
git clone
命令会自动设置本地master
分支跟踪克隆的远程仓库的master
分支(或其它名字的默认分支)。 运行git pull
通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支。git pull <远程仓库> <远程分支名>:<本地分支名>
git remote rename <oldRemoteName> <newRemoteName>
:用来修改一个远程仓库的简写名。git remote remove <remote>
:用来移除一个远程仓库,一旦你使用这种方式删除了一个远程仓库,那么所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除。git clone
: 从远端下载仓库
撤销
git commit –amend、git checkout <file>、git restore
git commit --amend
: 有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有--amend
选项的提交命令来重新提交,这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,而你所修改的只是提交信息。最终你只会有一个提交——第二次提交将代替第一次提交的结果。
git reset HEAD <file>
: 恢复暂存的文件git checkout <file>
: 丢弃修改(在未将文件git add
到暂存区中时有效)git restore <filename>
: git2.32 版本后取代git reset
进行许多撤销操作git restore --staged <filename>
:在对文件使用git add
后(将文件保存到暂存区后),可以使用这个命令以取消暂存。git restore <filename>
:在文件还未保存到暂存区时,将文件撤销到当前版本库版本;在文件保存到暂存区后,将文件撤销到到暂存区版本。
Git 高级操作
git config、git add -p、git blame、git show、git stash、.gitignore、pull request、git rebase
git config
:Git
是一个 高度可定制的 工具git clone --depth=1
: 浅克隆(shallow clone),不包括完整的版本历史信息git add -p
: 交互式暂存git rebase -i
: 交互式变基git blame
配合git show
使用:- **
git blame <filename>
: ** ,用来追溯一个指定文件的历史修改记录。它能显示任何文件中每行最后一次修改的提交记录。可以用来查看最后修改文件中某行(配合着 grep 使用)的人 git show <commit-sha1id>
:查看某次commit
的内容
例如:最后一次修改
_config.yml
文件中collections:
行时的提交信息是什么?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18先用 git blame 得到修改 _config.yml 文件的信息
再用 grep 搜索到修改 collections: 的提交的 sha-1 id
1@Running-Noob MINGW64 ~/Desktop/missing-semester (master)
git blame _config.yml | grep collection
a88b4eac (Anish Athalye 2020-01-17 15:26:30 -0500 18) collections:
得到提交的 sha-1 id 为 a88b4eac,再用 git show 查看具体提交内容
1@Running-Noob MINGW64 ~/Desktop/missing-semester (master)
git show a88b4eac
commit a88b4eac326483e29bdac5ee0a39b180948ae7fc
Author: Anish Athalye <me@anishathalye.com>
Date: Fri Jan 17 15:26:30 2020 -0500
Redo lectures as a collection
diff --git a/2020/index.html b/2020/index.html
deleted file mode 100644
index 153ddc8..0000000
.........- **
git stash
: 暂时移除工作目录下的修改内容git stash pop
:恢复暂时移除工作目录下的修改内容
git bisect
: 通过二分查找搜索历史记录