课程结构
01.课程概览与 shell
02.Shell 工具和脚本
03.编辑器 (Vim)
04.数据整理
05.命令行环境
06.版本控制(Git)
07.调试及性能分析
08.元编程
09.安全和密码学
10.大杂烩
11.提问&回答
本文档修改自这里,补充了一些视频中展示但配套文档中未提供的代码,以及一些注释。
版本控制Git--目录
版本控制(Git)
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:
- 当前模块是谁编写的?
- 这个文件的这一行是什么时候被编辑的?是谁作出的修改?修改原因是什么呢?
- 最近的1000个版本中,何时/为什么导致了单元测试失败?
因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。我们将通过一种自底向上的方式向您介绍 Git,从数据模型开始,最后再学习它的接口。
Git 的数据模型
Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在Git的术语里,文件被称作Blob对象(数据对象),也就是一组数据。目录则被称之为树,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树(下例中的目录root及其包含的子目录、文件)。
例如,一个树看起来可能是这样的:
<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 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来(如下例中的 Z1
有两个父辈 X1
和 Y2
)。
在 Git 中,这些快照被称为“提交(commit)”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
N1 <-- N2 <-- N3 <-- X1 <---- Z1
^ /
\ v
--- Y1 <-- Y2
- 上面是一个 ASCII 码构成的简图,每个
字母+数字
表示一次提交(快照)
箭头指向了当前提交的父辈。在第三次提交 N3
之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交 Z1
会同时包含这些特性。
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
数据模型及其伪代码表示
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
// 文件就是一组数据
type blob = array<byte>
// 一个包含文件和目录的目录
type tree = map<id_string, tree | blob>
// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct {
parent: array<commit> // 可能有多个父辈
author: string
message: string // 对本次提交的说明信息
snapshot: tree
}
这是一种简洁的历史模型。
对象和内存寻址
Git 中的对象可以是 blob、树或提交:
type object = blob | tree | commit
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
objects = map<id_string, object>
// 每次提交时,会为每个对象使用sha1函数产生一个 40 位的十六进制的字符串,即下面的id
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
例如,上面例子中的树root,看上去是这样的:
$ git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
树本身会包含一些指向其他内容的指针,例如 baz.txt
(blob) 和 foo
(树)。通过哈希值查看 baz.txt 的内容:
$ git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
git is wonderful
引用
现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master
引用(git初始化时,默认创建的引用,指向新建git仓库的第一次提交,之后通常会指向主分支的最新一次提交)
// name_string为引用,id_string为SHA-1哈希值
references = map<name_string, id_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
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e
时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e
)(git允许只用 40 位的哈希值的前几位来表示该哈希值)
暂存区
Git 中还包括一个和数据模型完全不相关的概念,但它却是创建提交的接口的一部分。
Git 提供了**暂存区(staging area)**机制,它允许您指定下次快照中要包括那些改动。
简单来说,就是:上一次快照 + 保存到暂存区的改动 (提交之后)= 下一次快照
- 如果之前还没有提交过,即没有快照,那么,只有保存到暂存区的改动,会作为新的快照
例如,一份快照(对应目录root)中只包含文件a,你在root目录中新增了文件b,但是误删了文件a。如果,你仅仅把新增的文件b添加到了暂存区,并提交,那么,新产生的快照中包含文件a和b(因为删除a的操作没有在暂存区中)
Git 的命令行接口
基础
git help <command>
: 获取 git 命令的帮助信息git init
: 创建一个新的 git 仓库,其数据会存放在一个名为.git
的目录下git status
: 显示当前的仓库状态git add <filename>
: 添加文件到暂存区git commit
: 创建一个新的提交git log
: 显示历史日志git log --all --graph --decorate
: 可视化历史记录(有向无环图)git diff <filename>
: 显示与暂存区文件的差异git diff <revision> <filename>
: 显示某个文件两个版本之间的差异git checkout <revision>
: 更新 HEAD 和目前的分支
分支和合并
git branch
: 显示分支git branch <name>
: 创建分支git checkout -b <name>
: 创建分支并切换到该分支- 相当于
git branch <name>; git checkout <name>
- 相当于
git merge <revision>
: 合并到当前分支git mergetool
: 使用工具来处理合并冲突git rebase
: 将一系列补丁变基(rebase)为新的基线
远端操作
git remote
: 列出远端git remote add <name> <url>
: 添加一个远端git push <remote> <local branch>:<remote branch>
: 将对象传送至远端并更新远端引用git branch --set-upstream-to=<remote>/<remote branch>
: 创建本地和远端分支的关联关系git fetch
: 从远端获取对象/索引git pull
: 相当于git fetch; git merge
git clone
: 从远端下载仓库
撤销
git commit --amend
: 编辑提交的内容或信息git reset HEAD <file>
: 恢复暂存的文件git checkout -- <file>
: 丢弃修改git restore
: git2.32版本后取代git reset 进行许多撤销操作
高级操作
git config
: Git 是一个 高度可定制的 工具git clone --depth=1
:git clone --shallow
浅克隆(shallow clone),不包括完整的版本历史信息git add -p
: 交互式暂存,会询问所有的更改,可以自行选择要上传的更改git rebase -i
: 交互式变基git blame
: 参数选择仓库的某个文件,查看最后修改某行的人git stash
: 暂时移除工作目录下的修改内容,git stash pop
会恢复修改的内容git bisect
: 通过二分查找搜索历史记录.gitignore
: 指定 故意不追踪的文件
代码展示
本处代码结合了pro Git与视频示例的代码。
基础操作
- 创建git仓库并提交
- 初始化,即创建新仓库
~/gits $ mkdir demo ~/gits $ cd demo ~/gits/demo $ git init Initialized empty Git repository in /home/laihj/gits/demo/.git/ ~/gits/demo (main) $ ls .git branches config description HEAD hooks info objects refs
- 将文件添加到暂存区
~/gits/demo (main) $ echo 'hello world' > hello.txt ~/gits/demo (main?) $ git add hello.txt
- git提交
# 使用git提交之前,必须设置你自己的user.name和user.email ~/gits $ git config --global user.name 'usr' ~/gits $ git config --global user.email 'usr@example.com' # 设置默认创建的分支名称为 main (原来是 master) ~/gits $ git config --global init.defaultBranch main # 提交,切换到vim编辑器的缓存页面 # 在首行输入提交信息:"Create demo for learning git commands",保存并退出 ~/gits/demo (main+) $ git commit [main (root-commit) 1547103] Create demo for learning git commands 1 file changed, 1 insertion(+) create mode 100644 hello.txt
- 查看提交记录中的对象
- 查看提交"1547103"的内容
~/gits/demo (main) $ git cat-file -p 1547103 tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 author usr <usr@example.com> 1683086262 +0800 committer usr <usr@example.com> 1683086262 +0800 Create demo for learning git commands
- 查看tree"68aba62"的内容
~/gits/demo (main) $ git cat-file -p 68aba62 # git允许使用前几位代替40位的完整哈希值,查看tree 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello.txt ~/gits/demo (main) $ git cat-file -p 3b18e512 # 查看hello.txt hello world ~/gits/demo (main) $ echo 'another line' >> hello.txt ~/gits/demo (main*) $ git status On branch main # 目前所处的分支 Changes not staged for commit: # 提示没有保存到暂存区的修改 (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: hello.txt no changes added to commit (use "git add" and/or "git commit -a") ~/gits/demo (main*) $ git add hello.txt ~/gits/demo (main+) $ git status On branch main Changes to be committed: