Post

一文看懂Git

一文看懂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

最佳实践:

  1. 第一行应该是简短的摘要(不超过50个字符),例如,feat: 添加新功能,fix: 修复bug,refactor: 重构代码,style: 调整格式,test: 添加测试,chore: 其他更改
  2. 空一行
  3. 详细的说明(如果需要)
  • 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 实际上是两个命令的组合:

  1. git fetch: 从远程获取最新代码
  2. 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 的区别:

  1. merge方式(默认)
    • 创建一个新的合并提交
    • 保留完整的提交历史
    • 分支历史呈现分叉结构
  2. 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)。

根据变更类型,递增版本号的规则如下:

  1. 当进行不兼容的API更改时,递增主版本号(MAJOR)
  2. 当以向后兼容的方式添加功能时,递增次版本号(MINOR)
  3. 当进行向后兼容的错误修复时,递增修订号(PATCH)
  4. 预发布版本和Meta-data可以作为附加标签(Labels)添加到主版本号、次版本号和修订号的后面

MAJOR.MINOR.PATCH-Labels

Commit规范

Conventional Commits

1
2
3
4
5
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

每次提交的Commit都应该符合上面的格式:

  1. 类型(type):
    • feat: 新功能
    • fix: 修复bug
    • refactor: 重构代码
    • style: 调整格式,不影响代码逻辑的更改(例如空格、格式化、删除日志)
    • chore: 其他更改
    • perf: 性能优化
    • test: 添加测试
    • build: 构建过程或外部依赖项的更改
    • ci: 持续集成
    • docs: 文档更新
    • revert: 回滚到之前的提交
  2. 作用域(scope):
    • 可选,用于说明本次提交的影响范围,例如:文件名、文件夹名、模块名、组件名、类名
  3. 描述(description):
    • 简短描述本次提交的内容
    • 开头的第一个词尽量从下面选取:Create、Modify、Add、Fix、Refactor、Release、Document、Update、Remove、Delete
    • 首字母大写,语言精简,少于50个单词,使用动词原型
  4. 主体(body):
    • 可选,用于提供更详细的描述
    • 可以包含多个段落,每段落之间用空行分隔

为开源项目贡献代码

参考HowToContribute

首先请核对下本地git config配置的用户名和邮箱与你github上的注册用户和邮箱一致,否则即使pull request被接受,贡献者列表中也看不到自己的名字,设置命令:

1
2
$ git config --global user.email "you@example.com"
$ git config --global user.name "Your Name"

具体步骤:

  1. 登录 github,在本项目页面点击 fork 到自己仓库
  2. clone 自己的仓库到本地:git clone https://github.com/xxx/kubeasz.git
  3. 在 master 分支添加原始仓库为上游分支:git remote add upstream https://github.com/easzlab/kubeasz.git
  4. 在本地新建开发分支:git checkout -b dev
  5. 在开发分支修改代码并提交:git add ., git commit -am ‘xx变更说明’
  6. 切换至 master 分支,同步原始仓库:git checkout master, git pull upstream master
  7. 切换至 dev 分支,合并本地 master 分支(确保开发分支包含原始仓库的最新更改),可能需要解冲突:git checkout dev, git merge master
  8. 提交本地 dev 分支到自己的远程 dev 仓库:git push origin dev
  9. 在github自己仓库页面,点击Compare & pull request给原始仓库发 pull request 请求
  10. 等待原作者回复(接受/拒绝)
This post is licensed under CC BY 4.0 by the author.