git 是一个版本控制工具, 具有: 开销小, 速度快, 分布式, 高安全性, 自由免费的特点.
什么是版本控制
版本: 对工程文件的每一次保存都可以算作一个版本的记录, 例如vscode自带的 timeline, 对psd文件的每一次备份等. 小到每一次修改, 大到每一次发布的版本, 都是版本的一种.
- 哪些版本需要管理?
- 虽然每次保存都会产生一个新的版本, 但是如果所有版本都被纳入管理, git记录就会变的非常臃肿. 因此, 一般来说, git的版本管理遵循最小事务原则, 即一次最小的有意义的完整改动, 被视为一次记录修改.
作用
I. 协作
假设 a, b 两人现在手上都有一个 v0.0.1
的项目代码, 此时 a 在 v0.0.1
之上修复了一个 bug
, b 在 v0.0.1
之上新增了一个功能, 此时
flowchart TD;
raw(["v0.1.1"])
a["v0.1.1" + A]
b["v0.1.1" + B]
raw-->a
raw-->b
此时为两个版本取一个有意义的版本号显然并无太大意义, 一般要达到一定规模的修改, 才会发布一个新的版本号, 因此, git
会自动产生一个用于标识的 hash
值 (如: 663a4b235fe3df41c406346a80660ad4b5beb610 或简写为 663a4b2
).
flowchart TD;
raw(["a34bfe3 (v0.1.1)"])
a["663a4b2"]
b["c904188"]
raw-->a
raw-->b
之后如果我们需要将 a, b 两人的代码合并, 可以选择搭建一个仓库服务器, 服务器上记录了项目的所有历史版本: 即 git
记录. a, b 将自己的修改添加到服务器的 git
记录上. 此时其他项目参与者便可以看到 a, b 两人的提交. git
内置了一个差异合并算法, 如果两人的代码没有 冲突(conflict)
则可以完成自动 合并(merge)
, 否则需要手动处理 冲突(conflict)
的代码. merge
时会默认创建一个 merge
节点.
flowchart TD;
raw(["a34bfe3 (v0.1.1)"])
a["663a4b2"]
b["c904188"]
merge(["29a4b12 (merge)"])
raw-->a
raw-->b
a --> merge
b --> merge
此时就完成了一次分布式的代码协作.
II. 方便开源托管
追溯历史 git
诞生于 linux
项目, 为了方便大家都能获取到最新的代码, linus
花了 3个月 开发了 git
, 如前所述, git
具有分布式的特点, 每个人都可以方便的将自己本地的项目和服务器上最新的代码进行同步.
III. 版本回退
git
会保留历史所有的提交记录(除非手动删除), 万一某天发现了一个bug, 可以方便的通过 git 进行版本回退, 减少损失. 相当于一个备份系统.
IV. 分支管理
每个版本在 git
中都是一个节点, 因此我们可以通过一个指针指向一个节点, 这便是分支:
flowchart TD;
raw(["a34bfe3 (v0.1.1)"])
a["663a4b2"]
b["c904188"]
merge(["29a4b12 (merge)"])
master1([master commit1])
master2([master commit2])
master3([master commit3])
master_branch{{master}}
dev1([dev commit1])
dev2([dev commit2])
dev3([dev commit3])
dev_branch{{dev}}
commit_node([commit])
branch_lable{{branch}}
raw-->a
raw-->b
a --> merge
b --> merge
merge --> master1
master1 --> master2
master2 --> master3
master_branch --> master3
merge --> dev1
dev1 --> dev2
dev2 --> dev3
dev_branch --> dev3
master
分支的维护和 dev
分支的新特性开发可以同步进行, 都通过同一个 git
仓库进行管理, 可以随时通过 git
切换当前的头结点(当前的工作区版本).
How to Use
I. 暂存区
在本地工作区和 git
版本树之间还有一个暂存区, 将代码添加到版本树之前, 要将工作区的版本先添加到暂存区, 这样做可以:
- 避免将一些不应提交的数据提交到版本树(上传之后非管理员无法撤销).
- 同时修改了多个文件, 可以分批添加到暂存区, 分批添加到版本树, 使得提交记录更清晰.
II. 常用指令
git add ./path_or_file # 将工作区的文件(夹)添加到暂存区
git commit -m "commit message" # 将暂存区的文件(夹)提交到版本树
git commit --amend # 覆盖 HEAD 分支提交
git branch [--option] # 创建/删除/查看 分支
git checkout branch_name # 签出分支(将)工作区和暂存区的版本同步为版本树中的指定分支
# 重置 [工作区和暂存区/仅暂存区] 回当前 HEAD, commit_lable 可以为 HEAD^^^, 表示 HEAD 的前驱
git reset [--hard/mixed] commit_lable
git merge another_branch # 将 another_branch 合并到当前分支
git rebase commit_lable # 将当前分支的 HEAD 和当前暂存区的版本设置为 commit_lable
git push # 将当前分支(例如: dev)提交到远程(origin/dev)分支
git pull # 拉取远程仓库合并到本地
# 克隆远程仓库(可选只克隆最新的n个版本, 可选指定分支, --recursive拉取子模块)
git clone [--depth=n] [-b branch] [--recursive] url
git submodule update # 更新子模块
III. Advanced Usage
STFW and RTFM
Github
全球最大的同性交友网站
之前提到 git
是一个分布式的项目版本管理器, 可以托管在远程仓库里, github
就是其中最大的托管平台, 它提供了许多高效的协作工具(issue, fork, pull requirest…), 吸引了众多开源工作者.
- 注册
- 在github上创建一个test仓库
- 本地拉取 test 仓库
- 往里添加一个 readme
- 两个人相互给对方仓库(仓库所有者称为a, b想给a的仓库添加代码):
b
提一个issueb
fork and clone 对方的仓库b
在本地创建新的分支b
在本地新分支上修改代码b
push 到自己 fork 的仓库b
Create pull request
a
合并new brancha
给b
仓库管理权限b
不通过 fork, pr 直接添加代码
- enjoy opensource world ❤️
How Does Git
Work
如何判断文件是否修改
- 这个问题在计算机各类工具软件中非常常见: 如 makefile 等构建工具, 文件服务器等.
git
会对项目目录下的所有纳入管理的文件(夹)计算一个 hash 值, 其中文件夹的 hash 值通过包含成员的 hash 值二次计算而来, 同时还会保存一个最近一次更改的时间. 如果工作区 a.c 文件最近更改的时间与 git 记录的(暂存区/版本树)时间不同, 则会对 a.c 重新计算一个 hash 值, 如果计算得到的 hash 值与 git 记录的 hash 值不同, 则改文件被认为修改过.
flowchart TD;
entry([entry])
match_time[/比较修改时间/]
diff([被修改])
same([未修改])
cal[计算hash值]
match_hash[/比较hash值/]
entry --> match_time
match_time -->|"=="| same
match_time -->|"!="| cal
cal --> match_hash
match_hash -->|"=="|same
match_hash -->|"!="|diff
如何将新文件加入到暂存区/版本树
首先 git
会将工作区的文件和暂存区的文件比较差异, 有修改的文件会被标识. 被标识的文件的 hash值, 修改时间 和 zstd
压缩后的文件会被加密存放到 .git
文件夹下.
以上过程不难看出, git
每次提交只记录了差异文件, 但是 git
需要知道每个文件的最新版本. 方法是, 将文件单独作为元数据保存, 每个 commit 节点, 记录每个文件最新版本的元数据指针.
flowchart TD;
HEAD([INDEX NEAD])
file_ptrs3(["Arr[指向版本3的文件指针]"])
file_ptrs1(["Arr[指向版本1的文件指针]"])
FILE_A_C([file a.c trace tree])
file_a_c_4{{file edition 4}}
file_a_c_3{{file edition 3}}
file_a_c_2{{"file edition 2 (merge)"}}
file_a_c_1{{file edition 1}}
file_a_c_0{{file edition 0}}
file_a_c_init{{file a.c init}}
HEAD ==> file_ptrs3
file_ptrs3 ==> file_ptrs1
FILE_A_C --> file_a_c_4
file_a_c_4 --> file_a_c_3
file_a_c_3 --> file_a_c_2
file_a_c_2 --> file_a_c_1 --> file_a_c_init
file_a_c_2 --> file_a_c_0 --> file_a_c_init
file_ptrs3 -.-> file_a_c_3
file_ptrs1 -.-> file_a_c_1