一文看懂Git
Git是由Linux之父Linus Torvalds于2005年开发的分布式版本控制系统。在Git出现之前,Linux内核开发团队一直使用BitKeeper作为版本控制系统。然而,在2005年,BitKeeper的所有者收回了Linux社区免费使用BitKeeper的权利。这促使Linus开发了一个全新的版本控制系统。
Linus开发Git的主要目标是:
- 快速
- 简单的设计
- 对非线性开发的强力支持(允许上千个并行开发的分支)
- 完全分布式
- 能够高效管理类似Linux内核一样的超大规模项目
Data Model of Git
Git项目由文件夹(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")
其在计算机内部的存储可以由下面的伪代码进行描述:
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
30
31
32
// a file is a bunch of bytes
type blob = array<byte>
// a directory contains named files and directories
type tree = map<string, tree | blob>
// a commit has parents, metadata, and the top-level tree
type commit = struct {
parents: array<commit>
author: string
message: string
snapshot: tree
}
// an object is a blob, tree, or commit
type object = blob | tree | commit
// all objects are content-addressed by their SHA-1 hash
objects = map<string, object>
def store(object):
id = sha-1(ogject)
objects[id] = object
def load(id):
return object[id]
// references are a human-readable name for SHA-1 hashes, like HEAD, main
references = map<string, string>
// check the above data model in real git
git cat-file -p <blob | tree | commit id>
安装和配置Git
MacOS可以通过homebrew安装Git:
1
$ brew install git
安装完成后,运行下面的指令配置Git,--global
参数表示这台机器上所有的Git仓库都会使用这个配置,也可以对某个仓库指定不同的用户名和Email地址。
1
2
3
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
git config --list # 查看配置
.gitignore 文件的使用
.gitignore文件用于指定哪些文件或目录不需要被Git跟踪。
最佳实践:
- 在项目开始时就创建.gitignore文件
- 按照项目类型使用合适的模板
- 定期维护和更新
- 避免忽略重要的配置文件
常见的忽略规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 编译生成的文件
*.class
*.o
*.pyc
# 包管理器目录
/node_modules/
/venv/
/.virtualenv/
# IDE配置文件
.idea/
.vscode/
*.sublime-workspace
# 系统文件
.DS_Store
Thumbs.db
# 日志和临时文件
*.log
*.tmp
Git常用命令
添加更改:git add
1
2
3
4
5
6
git add . # 添加当前目录下的所有文件到暂存区
git add -A # 添加所有变化到暂存区(包括新建、修改和删除的文件)
git add -u # 添加已被跟踪的文件的修改和删除,不包括新文件
git add -i # 交互式添加文件到暂存区
git add -p # 以补丁模式添加文件,可以选择部分更改进行暂存,在某些场合非常实用!
git add *.js # 使用通配符,添加所有JavaScript文件
提交更改:git commit
最佳实践:
- 第一行应该是简短的摘要(不超过50个字符),例如,feat: 添加新功能,fix: 修复bug,refactor: 重构代码,style: 调整格式,test: 添加测试,chore: 其他更改
- 空一行
- 详细的说明(如果需要)
- refactor: 涉及代码内部实现的改变,中等风险,需要充分测试
- style: 只涉及代码的外观格式,低风险,不需要测试
- chore: 其他更改,低风险,不需要测试
1
2
3
4
5
git commit -m "<提交说明>" # 提交暂存区的更改并添加说明
git commit -am "<提交说明>" # 将所有已跟踪的文件的更改提交(相当于git add + git commit)
git commit --amend # 修改最近一次提交的信息或内容
git commit -v # 在提交时显示详细的差异信息
git commit --date="<日期>" # 指定提交日期,例如:"2024-03-14 10:00:00"
临时保存:git stash
最佳实践:
- 使用有意义的备注
- 使用
-u
包含未跟踪的文件 - 使用
-a
包含所有文件(包括被忽略的文件) - 切换分支前使用stash保存修改
- 及时处理或清理stash列表
- 优先使用pop而不是apply
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 保存修改
git stash # 保存当前工作区的修改
git stash save "消息" # 保存时添加备注
git stash -u # 包含未跟踪的文件
git stash -a # 包含所有文件(包括被忽略的文件)
# 查看stash
git stash list # 查看所有stash
git stash show # 查看最新stash的内容
# 应用stash
git stash pop # 应用并删除最新的stash
git stash apply # 应用但不删除最新的stash
git stash apply stash@{n} # 应用指定的stash
# 删除stash
git stash drop # 删除最新的stash
git stash clear # 删除所有stash
撤销更改:git reset, git checkout
最佳实践:
- 在feature分支上自由使用reset,在main分支上谨慎使用reset
- 使用reset整理提交历史
- 不要在以下情况使用reset:
- 已推送到远程的提交
- 多人协作的分支
- 没有备份的情况下使用–hard
1
2
3
4
5
6
7
8
9
10
11
# 撤销更改
git reset --soft <commit id> # 保留工作目录和暂存区的更改
git reset --mixed <commit id> # 保留工作目录的更改,删除暂存区的更改(默认模式)
git reset --hard <commit id> # 删除工作目录和暂存区的更改
# 在使用`--hard`前先创建备份分支
git branch backup-branch
git reset --hard HEAD~3
# 撤销对某个文件的更改
git checkout <file>
远程管理:git remote, git fetch, git clone
1
2
3
4
5
6
7
8
9
10
11
# 查看本地仓库的远程地址
git remote -vv
# 为本地仓库添加一个远程地址,name一般为origin
git remote add <name> <url>
# 抓取远程仓库的status,抓取完之后可以通过git log查看,但不会对本地的项目进行更改,需要更改的话使用git pull
git fetch
# 当克隆一个非常大的远程仓库时(包含上万条commit),可以通过shallow参数,只克隆最新的版本来加快速度
git clone --shallow <url>
远程推送:git push
最佳实践:
- 经常推送小的改动,避免一次推送大量更改
- 推送前确保代码已经经过测试
- 使用有意义的提交信息
- 遵循团队的分支管理规范
1
2
3
4
# git push 命令的常用形式:
git push <远端别名> <分支名> # 推送本地分支到远端
git push -u <远端别名> <分支名> # 推送本地分支到远端并设置默认远端和分支
git push --delete <远端别名> <分支名> # 删除远程分支
远程拉取:git pull
最佳实践
- 经常进行pull操作,保持代码最新
- 使用
--rebase
保持提交历史整洁 - 在feature分支开发时定期同步主分支
- 使用
--ff-only
确保安全合并
1
2
3
4
5
6
7
8
9
# 基本用法
git pull <远程仓库> <远程分支> # 从指定远程分支拉取并合并
# 常用参数
git pull --rebase # 使用rebase方式合并
git pull --no-rebase # 使用merge方式合并
git pull --ff-only # 仅允许快进合并
git pull --no-ff # 禁用快进合并,总是创建合并提交
git pull --verbose # 显示详细信息
git pull 实际上是两个命令的组合:
git fetch
: 从远程获取最新代码git merge/rebase
: 将获取的代码合并到本地
分支管理:git branch, git switch
最佳实践:
- 保持主分支稳定,只合并已测试的代码
- 功能开发在特性分支进行
- 定期同步主分支到特性分支
- 及时删除已合并的分支
- 为每个分支设置上游分支,以便确认当前分支和上游分支的关系
分支命名建议:
- feature/* : 新功能分支
- bugfix/* : 问题修复分支
- hotfix/* : 紧急修复分支
- release/* : 发布分支
- dev : 开发分支
- main/master : 主分支
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 branch -vv # 列出本地所有分支,详细模式
git branch -a # 列出所有本地和远程分支
# 创建分支
git branch <分支名> # 创建新分支
git branch <分支名> <起点> # 从特定提交创建分支
git branch -b <新分支> <基础分支> # 创建并切换到新分支
git switch -c <新分支名> # 创建并切换到新分支
# 删除分支
git branch -d <分支名> # 删除已合并的分支
git branch -D <分支名> # 强制删除分支
# 重命名分支
git branch -m <旧名> <新名> # 重命名分支
# 切换分支
git switch <分支名> # 切换到指定分支
# 手动设置上游分支(远程分支)
git branch --set-upstream-to=origin/remote-branch-name
# 也可以在push的时候设置上游分支(推荐)
git push -u origin remote-branch-name
合并分支:git merge, git rebase
merge 和 rebase 的区别:
- merge方式(默认)
- 创建一个新的合并提交
- 保留完整的提交历史
- 分支历史呈现分叉结构
- rebase方式
- 将本地提交移动到远程分支的最新提交之后
- 产生线性的提交历史
- 更清晰的版本演进线图
无冲突合并、快进合并(fast-forward merge):当本地分支的提交历史是远程分支的直接延续时(即没有合并冲突),使用快进合并可以简化合并过程。
当出现合并冲突时,则需要手动查看冲突地方,解决冲突之后完成合并。
1
2
3
4
5
6
7
8
9
10
11
# 开始合并,将dog分支合并到当前分支上,当前分支一般为主分支
git merge dog
# 出现合并冲突时,Git会提示发生冲突的是哪个文件,打开文件,找到提示冲突的地方,删去冲突提示语,解决冲突,保存文件
vim animal.py
# 解决完冲突,将文件添加到暂存区
git add animal.py
# 提交解决完冲突后的文件
git merge --continue
历史管理:git log, git checkout
最佳实践:
- 定期使用git log检查项目历史
- 使用合适的format选项优化log输出
- 使用git checkout在不同的commit之间进行跳转,本质上是将HEAD指向特定的commit
1
2
3
4
5
6
7
8
9
10
11
12
13
git log # 查看提交历史
git log --stat # 显示简要统计信息
git log --pretty=format:"%h - %an, %ar : %s" # 自定义输出格式
git log -n <数量> # 显示最近n条提交
git log --author="用户名" # 查看特定作者的提交
git log --since="2 weeks ago" # 查看最近两周的提交
# 万金油语句,可以保存成alias快速调用!
git log --all --graph --decorate --oneline
git checkout <commit id> # 跳转到之前的commit
标签管理:git tag
最佳实践:
- 使用语义化版本号(Semantic Versioning)
- 为重要的发布创建标签
- 及时推送标签到远程
- 使用
-a
创建附注标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看标签
git tag # 列出所有标签
git tag -l "v1.8.5*" # 查看符合特定模式的标签
git show <标签名> # 查看特定标签的详细信息
# 创建标签
git tag <标签名> # 创建轻量标签
git tag -a <标签名> -m "<标签说明>" # 创建附注标签
git tag -a <标签名> <commit_id> # 给指定的提交创建标签
# 推送标签
git push origin <标签名> # 推送特定标签到远程
git push origin --tags # 推送所有标签到远程
# 删除标签
git tag -d <标签名> # 删除本地标签
git push origin :refs/tags/<标签名> # 删除远程标签
其他
1
2
3
4
5
6
7
# 查看文件中的每一行所对应的commit和author
git blame <filename>
# 找到某一行对应的commit之后,查看这次commit的详细信息
git show <commit id>
# 快速查找从哪次commit开始,unit test无法通过
git bisect
Git 开发规范
语义化版本号
语义化版本控制(Semantic Versioning)是一种版本命名规范,根据规范,版本号由三个部分组成:主版本号(MAJOR)、次版本号(MINOR)和修订号(PATCH)。
根据变更类型,递增版本号的规则如下:
- 当进行不兼容的API更改时,递增主版本号(MAJOR)
- 当以向后兼容的方式添加功能时,递增次版本号(MINOR)
- 当进行向后兼容的错误修复时,递增修订号(PATCH)
- 预发布版本和Meta-data可以作为附加标签(Labels)添加到主版本号、次版本号和修订号的后面
MAJOR.MINOR.PATCH-Labels
Commit规范
1
2
3
4
5
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
每次提交的Commit都应该符合上面的格式:
- 类型(type):
- feat: 新功能
- fix: 修复bug
- refactor: 重构代码
- style: 调整格式,不影响代码逻辑的更改(例如空格、格式化、删除日志)
- chore: 其他更改
- perf: 性能优化
- test: 添加测试
- build: 构建过程或外部依赖项的更改
- ci: 持续集成
- docs: 文档更新
- revert: 回滚到之前的提交
- 作用域(scope):
- 可选,用于说明本次提交的影响范围,例如:文件名、文件夹名、模块名、组件名、类名
- 描述(description):
- 简短描述本次提交的内容
- 开头的第一个词尽量从下面选取:Create、Modify、Add、Fix、Refactor、Release、Document、Update、Remove、Delete
- 首字母大写,语言精简,少于50个单词,使用动词原型
- 主体(body):
- 可选,用于提供更详细的描述
- 可以包含多个段落,每段落之间用空行分隔
为开源项目贡献代码
首先请核对下本地git config配置的用户名和邮箱与你github上的注册用户和邮箱一致,否则即使pull request被接受,贡献者列表中也看不到自己的名字,设置命令:
1
2
$ git config --global user.email "you@example.com"
$ git config --global user.name "Your Name"
具体步骤:
- 登录 github,在本项目页面点击 fork 到自己仓库
- clone 自己的仓库到本地:git clone https://github.com/xxx/kubeasz.git
- 在 master 分支添加原始仓库为上游分支:git remote add upstream https://github.com/easzlab/kubeasz.git
- 在本地新建开发分支:git checkout -b dev
- 在开发分支修改代码并提交:git add ., git commit -am ‘xx变更说明’
- 切换至 master 分支,同步原始仓库:git checkout master, git pull upstream master
- 切换至 dev 分支,合并本地 master 分支(确保开发分支包含原始仓库的最新更改),可能需要解冲突:git checkout dev, git merge master
- 提交本地 dev 分支到自己的远程 dev 仓库:git push origin dev
- 在github自己仓库页面,点击Compare & pull request给原始仓库发 pull request 请求
- 等待原作者回复(接受/拒绝)