git 基础知识与使用

Git

本文内容基于 The Missing Semester of Your CS Education - 版本控制(Git) 改编

Git 相关学习资料看这里

一、理论知识

版本控制系统 (VCSs)

  • Version Control Systems
  • 用于追踪源代码(或其他文件、文件夹)改动的工具
  • 管理代码的修改历史
  • 协作编码变得更方便
  • VCS
    • 通过一系列的快照将某个文件夹及其内容保存了起来
    • 每个快照都包含了文件或文件夹的完整状态
    • 同时它还维护了快照创建者的信息以及每个快照的相关信息

关于版本控制: https://git-scm.com/book/zh/v2/起步-关于版本控制

本地版本控制系统
集中化版本控制系统
分布式版本控制系统

Git

Git 是什么?

直接记录快照,而非差异比较
WikipediaGit (/ɡɪt/) is free and open source software for distributed version control

  • 很多时候只能死记硬背一些命令行,然后像使用魔法一样使用它们。
    • 一旦出现问题,就只能像这幅漫画里说的那样去处理了😅
      • 毕竟只知道硬背命令,不知道所处的情况,遇到报错最简单的处理方式莫过于保存现在的工作,然后删掉混乱的仓库,重新克隆仓库,再粘回去

Git 的数据模型

快照

  • Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。
  • 文件被称作Blob对象(数据对象),也就是一组数据。
  • 目录则被称之为 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")

历史记录建模:关联快照

  • 有向无环图
  • Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。
  • 注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。
    • 例如,经过合并后的两条分支。

可视化展示一下:

1
2
3
4
o <-- o <-- o <-- o
^
\
--- o <-- o
  • o 表示一次提交(快照)
  • 箭头指向当前提交的父辈
  • 第三次提交之后,历史记录分岔成了两条独立的分支
    • 可能因为此时需要同时开发两个不同的特性,他们之间是相互独立的
1
2
3
4
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
  • 开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。

数据模型的伪代码表示

1
2
3
4
5
6
7
8
9
10
11
12
13
// 文件就是一组数据
type blob = array<byte>

// 一个包含文件和目录的目录
type tree = map<string, tree | blob>

// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}

对象和内存寻址

1
2
3
4
5
6
7
8
9
10
11
12
13
// Git 中的对象可以是 blob、tree、commit
type object = blob | tree | commit

// Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址
// 换句话说,当它们引用其他对象时,硬盘上保存的上这些对象的哈希值
objects = map<string, object>

def store(object):
id = sha1(object)
objects[id] = object

def load(id):
return objects[id]

例如上面例子中的树:

1
2
3
4
5
6
7
# 698281bc680d1995c5f4caaf3359721a5a58d48d 是顶层树的哈希值
❯ git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
# 可以通过哈希读取baz.txt文件的内容
❯ git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
git is wonderful

引用

  • 所有的快照可通过哈希值标记,但是40位的16进制字符串难以记忆
  • 给这些哈希值赋予人类可读的名字,也就是引用(references)
    • 引用是指向提交的指针
    • 引用可以被更新,指向新的提交
    • master 引用通常会指向主分支的最新一次提交
    • 当前的位置有一个特殊的索引: “HEAD”
1
2
3
4
5
6
7
8
9
10
11
12
13
references = 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仓库定义:对象 和 引用
  • 在硬盘上,Git 仅存储对象和引用

版本库、工作区、暂存区

版本库、工作区与暂存区

Pasted image 20220920114455

我们把文件往Git版本库里添加的时候,是分两步执行的:

  1. 第一步是用 git add 把文件添加进去,实际上就是把文件修改添加到暂存区
  2. 第二步是用 git commit 提交更改,实际上就是把暂存区的所有内容提交到当前分支。

协议

服务器上的Git - 协议

  • 本地协议(Local protocol): 其中的远程版本库就是同一主机上的另一个目录。
    • git clone /srv/git/project.git
  • HTTP 协议:
    • git clone https://github.com/git/git.git
  • SSH 协议:
    • git clone ssh://[user@]server/project.git
  • Git 协议:
    • git clone git@github.com:git/git.git

📌 Remove my password from list so hackers won't be able to hack me

Pasted image 20220920115625

查看讨论: https://github.com/danielmiessler/SecLists/pull/155

二、基本操作

基础命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ git --version
$ git help <command>
$ git config --global user.name ex7l0it && git config --global user.email abc@abc.com
$ git config --global core.editor vim
$ git config --global core.pager ''

$ git init
$ git add .
$ git status
$ git commit -m "first commit"
$ git log --all --graph --decorate --oneline
$ git diff
$ git restore baz.txt
$ git reset --hard/--soft/--mixed HEAD^
$ git branch dev
$ git switch dev
$ git merge dev

$ git clone <url>
$ git remote add origin <url>
$ git push origin master
$ git pull
$ git fetch
$ git merge

git config 相关

  • 通过 git config --local 进行的配置将保存在当前 git 仓库的 .git/config 文件中
  • 通过 git config --global 进行的配置将保存在当前用户家目录的 .gitconfig 文件中

第一次提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cd /tmp
mkdir git_learning
cd git_learning
git init # 初始化git仓库
ls -al
mkdir foo
echo "hello world" > foo/bar.txt
echo "git is wonderful" > baz.txt
echo "# README\n" > README.md
git status
git add . # 将当前目录下所有文件加入暂存区
git status
git commit -m "first commit"
git status
git log

创建一个分支

1
2
3
4
5
6
7
8
9
git switch -c dev
git branch
echo "#/usr/bin/python3\nprint('Hello world')" > hello.py
echo "add hello.py" >> README.md
git add hello.py
git commit -m "dev 0.1"
git status -s # -s 以简短的形式输出
git commit -am "dev 0.1.1" # 自动把所有已经跟踪过的文件暂存起来一并提交
git status

📌 关于 git checkoutgit switchgit restore

git checkout 这个命令既被用来切换分支,又被用来恢复工作区文件,对用户的认知造成了较大的困惑。Git自2.23版本开始引入两个新的命令: git switchgit restore,用来替代 git checkout

  • git switch: 负责分支管理
    • git switch <branch_name> 切换至指定分支
    • git switch -c <branch_name> 创建新的分支并切换至该分支
  • git restore: 负责文件恢复

合并分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 正常合并不出错的情况:
git switch master
echo test > foo/a.txt
git add foo/a.txt
git commit -m "add a.txt"
git log --all --graph
git merge dev
git log --graph

# 合并分支时出现冲突的情况:
echo "name=input('your name: ')\nprint('hello, {}'.format(name))" > hello.py
git add hello.py
git commit -m "update hello.py"
git switch dev
echo "name=input('your name: ')\nprint('hello, {}'.format(name))" >> hello.py
git commit -am "dev 0.2: update hello.py"
git switch master
git merge dev
git status
vim hello.py # 修复冲突
git commit -a / git merge --continue

查看差异

1
2
3
4
echo checkofdiff > baz.txt
git diff # 查看工作区/暂存区的差异
git add baz.txt
git diff --staged # 查看暂存区/上一次commit的差异

回退

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 尚未添加到暂存区时
git status
echo junkdata >> baz.txt
git diff
git restore baz.txt # 取回暂存区的版本到工作区
cat baz.txt

# 修改内容已添加到暂存区时
echo junkdata >> baz.txt
git add baz.txt
git reset HEAD baz.txt # 暂存区的修改撤销,移回到工作区
git diff
git restore baz.txt # 再取回暂存区版本到工作区,彻底放弃暂存区的修改

# 修改内容已经提交到了仓库时
echo junkdata >> baz.txt
git commit -am "add junkdata"
git reset --hard HEAD^ # 回退仓库和所有区,HEAD^表示第一个父级提交(也就是最近的一次提交)
--soft HEAD^ # 仅回退仓库
--mixed HEAD^ # 回退仓库和暂存区

git reset --hard <commit id> # 回退的指定的某个提交状态,之后的记录全部删除

image-20200218115929811

远程仓库

1
2
3
4
5
6
7
8
git remote add origin git@github.com:ex7l0it/git_learning.git
git push -u origin main

cd /tmp
git clone https://github.com/ex7l0it/git_learning.git remote_git_learning
cd remote_git_learning
git fetch # 将远程主机的最新内容拉到本
git pull # 将远程主机的最新内容拉下来后直接合并, 等于 git fetch + git merge

Github相关

三、更多命令及应用场景

1. 当你已经更改了很多内容,突然发现需要把修改的内容移动到新分支

  • 解决方案:使用 git stash 进行暂存工作
1
2
3
git stash # 保存当前工作区修改的内容
git switch -c dev # 创建并切换到新分支
git stash pop # 将暂存的修改内容还原到新分支上

git stash 相关命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 保存工作现场
git stash
git stash save # 加不加 save 都一样的效果

# 将最新的 stash 内容应用到当前工作区,并从列表中移除
git stash pop
# 查看所有保存的 stash 列表
git stash list
# 将最新的 stash 内容应用到当前工作区,但不从列表中移除
git stash apply
# 删除掉最新的 stash
git stash drop
# 删除掉列表中所有的 stash
git stash clear

TODO