# Git
:一个流行的版本控制工具。
- 官方文档 (opens new window)
- 采用 C 语言开发。2005 年由 Linus Torvalds 发布。
- 特点:
- 分布式管理。每个服务器、客户端都存储一份独立的代码仓库,可以相互同步。
- 支持将每次修改后的文件提交为一个版本,允许用户将文件回滚到任意历史版本。
- 支持创建多个分支,进行分支切换、合并,便于多人合作开发同一个项目。
# 安装
在 CentOS 上安装 git :
yum install git
在 Windows 上推荐再安装 git 的 GUI 工具,比如 Tortoisegit 。
配置用户名、邮箱:
git config --global user.name "name" git config --global user.email "[email protected]"
每次执行
git commit
时,默认会记录该用户名、邮箱,表示提交者。
# 基本用法
用户进入项目根目录,执行
git init
命令进行初始化。- 这默认会在当前目录下创建一个 .git 子目录,存储 Git 仓库的所有文件。
用户修改一些文件,然后执行
git commit
命令,将当前时刻的所有文件提交为一个新版本,保存到 Git 仓库。- Git 会将这些文件拷贝一份到 Git 仓库中。并记录当前时刻所有文件的哈希值,组成一个版本。
- 如果文件的哈希值发生变化,Git 就认为文件的内容已经改变,会将改变之后的文件拷贝一份到 Git 仓库中。不改变的文件则不会拷贝。
用户执行
git checkout xxx
命令,切换到历史版本。- Git 会找到该版本对应的所有文件的哈希值,根据哈希值将这些文件从 Git 仓库拷贝到项目目录下,从而将项目目录还原到历史时刻。
# 版本
# 修改
git status # 显示当前 Git 仓库的状态(包括当前的分支名、缓存区内容)
git add <path>... # 将指定路径下的所有文件加入缓存区
-u # 如果有文件不匹配 path ,但已被 Git 管理,则也加入缓存区。与 git add . 相比,git add -u 能发现已删除的文件,但不能发现新增的文件
-A # 相当于执行 git add . 和 git add -u
git rm <file> # 删除某个文件,并将该改动加入缓存区
--cached # 从缓存区删除
git mv <src_file> <dst_file> # 移动文件,并将该改动加入缓存区
- 修改了文件时,建议先加入 Git 缓存区(称为 stage、index)。等准备好了再执行 git commit ,将缓存区中的所有文件提交成一个新版本,永久保存到 Git 仓库中。
- 也可以不加入缓存区就直接执行 git commit 。
- 如果一个文件相对上一版本未被改动,或者被 .gitignore 文件忽略,则不会添加到缓存区。
- 如果在 Windows 上修改了一个文件名的大小写,则 Git 默认不会发现该改动,此时建议通过 git mv 重命名文件。
- 如果一个目录为空,或者只包含空的子目录,则会被 Git 忽略,不会提交。
# 提交
git commit # 将当前缓存区的所有文件提交成一个新版本
-m "initial version" # 加上备注信息(该选项为强制要求)
-a # 提交从上一个版本以来被改动的所有文件
--amend # 将当前缓存区合并到上一个版本
-S # 添加 GPG 签名
- 每次 commit ,会自动生成一个 SHA-1 哈希值,作为该版本的唯一标识符,称为 commit ID、version name 。如下:
commit 86e696bd125aa895e067c2216ae8298289ab94d6 Author: Leo <[email protected]> Date: Thu Dec 10 09:15:19 2020 +0800
- 该哈希值的长度为 40 位,不过用户只使用前几位也能定位到该版本,比如
git checkout 86e696
。
- 该哈希值的长度为 40 位,不过用户只使用前几位也能定位到该版本,比如
- 每次 commit 需要声明一个 Author 和一个 Committer ,分别表示作者、提交者,两者的含义通常相同。
- 如果一个人修改文件,委托另一个人提交,则建议两人分别担任 Author 与 Committer 。
- 通过
git commit --amend
可以修改上一个版本,包括提交内容、commit ID、Committer ,而 Author 不变,指向第一次提交者。
# 撤销
git clean [path]... # 删除指定目录(默认为当前目录)下,所有未被 Git 版本控制、未被 .gitignore 忽略的文件
-d # 递归子目录
-f # 强制删除
-x # 将 .gitignore 中记录的文件也删除
-e <pattern> # --exclude ,排除一些文件,不删除
git reset [refs] # 将当前分支指向目标版本(默认是上一个版本 HEAD~1 )
--soft # 不改变工作目录的文件(即依然处于原版本),将与目标版本不同的所有文件添加到缓存区
--mixed # 不改变工作目录的文件,清空缓存区。默认采用该方式
--hard # 改变工作目录的文件(即变为目标版本),并清空缓存区
git revert <refs>... # 自动提交一个新版本来抵消某个历史版本的变化(这样不会删除历史版本)
-n # --no-commit ,只是修改文件并加入缓存区,不自动提交
撤销文件的常用命令:
git checkout . # 将文件复原到当前版本 git clean -dfx # 清理未被版本控制的文件 git reset --hard # 复原项目文件,清空缓存区 git revert HEAD # 撤销上一个版本 git revert HEAD~5..HEAD # 撤销一连串版本,即还原到 HEAD~5 版本
假设一个 C 语言项目,在某个历史版本新增了一个函数,但后来发现代码写错了,有几种修改方案:
- 如果该历史版本只是上一个版本,可执行
git commit --amend
直接修改。- 优点:amend 的修改痕迹很少,保持了 Git 版本树的简洁。
- 缺点:如果该历史版本已经推送到 Git 服务器上,则修改之后本地 Git 仓库与服务器不一致,只能执行
git push -f
。
- 如果该历史版本比较远,比如 HEAD~5 ,则可以:
- 在当前分支,提交一个新版本,并在 comment 中说明这是为了修正上述历史版本。
- 在当前分支,通过 git revert 自动提交一个新版本,撤销该历史版本的提交内容。然后重新编写函数代码,手动提交成一个版本。这样总共增加了两个版本,不怎么美观,但保持了历史版本的固定不变。
- 通过 git checkout 切换到 HEAD~5 的上一个版本 HEAD~6 ,然后新建一个分支,在该版本的基础上重新提交函数代码。这样分叉了一个新分支。
- 如果该历史版本只是上一个版本,可执行
Git 仓库中不应该存储大体积文件(否则 git 会占用比该文件多几倍的磁盘空间),也不应该存储私密文件(因为 git 仓库通常会被分享给多人查看)。如果存在这些文件,则可执行以下命令,从 Git 仓库的所有版本中删除指定文件:
git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch *.log' --prune-empty --tag-name-filter cat -- --all git push origin --force --all --tags # 强制推送,覆盖远程仓库
# .gitignore
- 不受 Git 版本控制的文件主要有两种:
- 新增的文件,尚未被 git add 加入版本控制。
- 被 .gitignore 忽略的文件。
- 可以在项目根目录下创建一个 .gitignore 文件,声明一些文件,不被 Git 版本控制。如下:
/test.py # 忽略项目根目录下的指定文件 /log/*.log # 忽略 log 目录下的一些文件 __pycache__/ # 忽略所有目录下的指定目录
- 这里的文件路径,是指相对于项目根目录的路径,可以使用通配符 * 、? 。
- 以 / 开头的路径,是从项目根目录开始,匹配方向是明确的。不以 / 开头的路径,可能匹配到多个目录下的文件。
- 以 / 结尾的路径,是强调匹配目录,不匹配文件。
# 版本
想指定一个 commit 版本时,有多种引用(Reference ,refs)方式:
- commit ID :是不方便记忆的哈希值。
- branch :分支,指向某个版本,且可以改为指向其它版本,相当于指针。
- tag :标签,指向某个版本,且创建之后不能改为指向其它版本,相当于某个版本的别名。
常见分支:
- master :Git 仓库初始化时,默认创建的一个分支,通常用作主分支。
- HEAD :Git 仓库内置的一个特殊分支,指向用户当前所处的版本。
- 用户可以一会切换到 branch1 分支,一会切换到 branch2 分支,甚至切换到一个不存在分支的版本,而 HEAD 分支总是指向用户当前所处的版本。
- 可通过 HEAD~n 的格式指向 n 次 commit 之前的版本。比如 HEAD~0 指向当前版本,HEAD~1 指向上一个版本。
假设在 Git 仓库创建一个分支 test ,提交几个 commit ,然后删除分支 test 。
- 此时,这些 commit 不在任何 branch 或 tag 的版本树上,称为孤立提交(orphan commit)。可执行
git gc
来清理。
- 此时,这些 commit 不在任何 branch 或 tag 的版本树上,称为孤立提交(orphan commit)。可执行
# branch
git branch # 显示所有本地分支
-a # 增加显示本地仓库中的 remote-tracking branch ,默认不会显示它们
-v # 显示每个分支所在的版本
<branch> [src_refs] # 新建一个分支,源版本默认为 HEAD 分支
-d # 删除分支,需要该分支已合并到其它分支
-D # 强制删除分支
# checkout
git checkout
[refs] # 将当前分支切换到某个 refs 指向的版本,如果不指定则选中当前版本
<path>... # 不切换,而是将指定路径下的所有文件改为目标版本的状态
-b <branch> # 先创建指定的分支,再切换过去。如果该分支已存在,则报错
<refs> # 创建新分支时,使用指定的 refs 版本
-B <branch> # 先创建指定的分支,再切换过去。如果该分支已存在,则不会报错
- 例:
# 将文件回滚到上一个版本的状态。如果上一个版本没有修改该文件,则不会回滚 git checkout HEAD~1 README.md # 将文件回滚到指定时刻的状态 git checkout master@{1hourago} README.md git checkout master@{2022-01-01T12:00:00} # 在本地创建一个分支 dev ,切换过去,并跟踪远程分支 dev git checkout -b dev origin/dev # checkout 时,如果本地不存在该名称的 refs ,而远程仓库存在,则会自动新建一个本地 refs ,跟踪远程 refs git fetch git checkout dev
- 如果用
git checkout
切换到一个 commit 或 tag ,则不会绑定分支,会提示:You are in 'detached HEAD' state.
。此时可以执行git fetch
,但不能执行git pull
,否则会报错:You are not currently on a branch
# tag
git tag # 显示已有的所有标签
-a v1.0 9fceb02 # 给版本 9fceb02 加上标签 v1.0
-d <tagName> # 删除一个标签
# merge
git merge <branch> # 将指定分支的所有版本合并到当前分支
-m "Merge branch" # 加上备注信息
示例图:
用户提交的历史版本会按先后顺序排列成一条线:
如果用户想重新修改某个历史版本,可创建一个 dev 分支,指向该分支:
用户在 dev 分支提交另一个版本,此时版本树从一条线分叉成多条线:
用户可以将 dev 分支合并到 master 分支:
- 合并时,如果 dev 分支不包含 master 分支没有的版本,则合并后 master 分支不会变化。否则,合并后会产生一个新版本,以解决两个分支的差异。
- 如果两个分支包含不同路径的文件,则会自动合并。如果包含相同路径的文件,但内容不同,就会产生冲突,必须解决冲突才能合并。
- 例:
- 如果 master 分支包含文件 /test/1.txt ,dev 分支不包含文件 /test/1.txt ,则 Git 会自动合并,保留该文件。
- 如果两个分支都包含文件 /test/1.txt ,但内容的大小写不同,则用户需要手动确定合并之后该文件的内容是什么。
# rebase
git rebase
<branch> # 将当前分支以变基方式合并到指定分支,这会产生一个新 commit
branch1 branch2 # 将 branch2 以变基方式合并到 branch1
branch1 branch2 --onto branch3 # 将 branch2 相对于 branch1 的变基应用到 branch3 上
如下图,假设在 Git 仓库的 master 分支上依次提交了 C0、C1、C2、C4 版本。此时有人 fork 了 C2 版本,做出修改,生成 C3 版本。
- 如果以 merge 方式将 C3 合并到 C4 ,则可能存在合并冲突,需要手动处理。
- 如果以变基(rebase)方式将 C3 合并到 master ,则会自动找到 C3 与 C4 的共同祖先 C2 。然后删除 C3 ,将从 C2 到 C3 之间的所有变化拷贝提交到 C4 ,生成一个新版本 C3' 。最后将 master 分支指向 C3' 版本。
merge 与 rebase 方式最终生成的版本都一样,但是 rebase 方式会删除分叉的分支,将版本图简化成一条线。
# cherry-pick
git cherry-pick <commit_hash>... # 提取多个 commit 的修改内容,拷贝提交到当前分支。支持提交到其它 Git 仓库
-n # 只更新文件,不提交
- merge 与 rebase 方式都是将一个分支的所有 commit ,合并到另一个分支。而 cherry-pick 只是拷贝部分 commit ,因此适合处理差异较大、不能合并的两个分支,甚至拷贝到其它 Git 仓库的分支。
- 假设修改了 master 分支的 .gitignore 文件,然后想同步到其它分支。如果采用 merge、rebase 方式,则需要将 master 分支的所有 commit 合并到其它分支,可能修改大量文件。而采用 cherry-pick 方式,则只会修改 .gitignore 文件。
# log
git log [refs] # 显示某个分支上的 commit 日志,不指定 refs 则采用 HEAD 分支
[path] # 可选指定 path ,然后向前追溯涉及 path 文件的 commit ,不显示其它 commit 。不过该 path 必须在当前版本存在
--full-history -- [path] # 从 commit 历史中查找涉及 path 文件的所有 commit 。该 path 不必在当前版本存在,因此可以发现 path 什么时候从 Git 仓库删除了
-n <int> # 最多显示多少个 commit
--reverse # 倒序显示各个 commit 。默认按时间从新到旧排序,该选项会从旧到新排序
--since=<date> # 只显示指定时刻之后的 commit 。date 可以是 2022-01-01T12:00:00 或 "1 hours ago" 的格式
--until=<date> # 只显示指定时刻之前的 commit
--author=<pattern> # 只显示提交者与 pattern 正则匹配的 commit
--format=<format> # 设置每个 commit 的显示格式,比如 oneline、reference、fuller ,还可以是 format:%h,%cI,%s 这样的格式字符串
--show-signature # 增加显示每个 commit 的 GPG 签名
git reflog # 显示 HEAD 分支的变化日志,包括在本机的 checkout、commit、pull 等事件
git show
[refs] --format=full # 显示指定版本的 commit 内容
<refs>:<path> # 显示指定版本的某个文件的内容
git diff <refs> <refs> # 显示从一个版本到另一个版本的差异,包括差异文件、文件内增删的每行
--stat # 只显示统计信息,包括差异文件列表、增减的行数
--name-status # 只显示差异文件列表、文件动作的缩写(比如 A 新增、D 删除、M 修改、R 重命名)
--name-only # 只显示差异文件列表
--no-renames # 不自动识别 rename 动作,直接显示 create、delete
git ls-remote # 列出远程仓库的所有 refs
--heads # 只列出所有分支,格式如 refs/heads/master
--tags # 只列出所有标签,格式如 refs/tags/v1.0
git for-each-ref
--points-at=<name> # 列出与一个名称相关的所有 refs
git rev-parse
--show-toplevel # 返回 Git 项目的顶级目录
git gc # 清理磁盘文件,比如删除 orphan commit 、删除重复文件
# 配置
Git 的配置文件有三种,局部的配置会覆盖全局的配置:
- 系统的配置文件:保存在
/etc/gitconfig
。 - 当前用户的配置文件:保存在
~/.gitconfig
。 - 当前目录的 Git 仓库的配置文件:保存在
.git/config
。
- 系统的配置文件:保存在
可以用 vim 命令修改 Git 的配置文件,也可以用以下命令修改:
git config --system # 使用系统的配置文件 --global # 使用当前用户的配置文件 --local # 使用当前 Git 仓库的配置文件 -l # --list ,显示配置文件的全部内容 -e # --edit ,在文本编辑器中打开配置文件 <key> # 显示配置文件中某项参数的值 <key> <value> # 设置配置文件中某项参数的值
Git 配置文件为 INI 格式,下方是一个 Git 仓库的配置示例:
[core] repositoryformatversion = 0 # 仓库格式的版本 filemode = true # 是否保留文件权限中的可执行位 bare = false # 该仓库是否为裸仓库 ignorecase = false # 是否忽略文件名的大小写 [remote "origin"] # 定义一个远程仓库,名为 origin url = https://github.com/LeoHsiao1/test.git fetch = +refs/heads/*:refs/remotes/origin/* # 格式为 [+]<src>:<dst> ,表示让本地分支 src 跟踪远程分支 dst [branch "master"] remote = origin merge = refs/heads/master
通过 git pull、git push 同步 Git 仓库时,会同步受版本控制的所有文件,但不会同步 Git 仓库的配置文件,比如
.git/config
、.git/hooks
。- .gitmodules 文件受版本控制,因此会被同步。
# hooks
用户可以给 Git 仓库添加一些钩子(hooks),用于在发生某些 Git 操作时,自动执行自定义的 shell 脚本。
git hooks 保存在
.git/hooks
目录下,例如:.git/hooks/ |-- pre-commit.sample |-- pre-push.sample |-- pre-rebase.sample `-- ...
默认有一些名为
<hook_name>.sample
的 shell 脚本,将脚本重命名为<hook_name>
即可生效。常见的钩子:
pre-commit # 每次执行 git commit 操作之前,触发该钩子 post-commit # 每次执行 git commit 操作之后,触发该钩子 pre-push post-checkout pre-receive # 用于 Git 服务器,每次执行 git-receive-pack 操作之前,触发该钩子 post-receive # 用于 Git 服务器,每次执行 git-receive-pack 操作之后,触发该钩子
# submodule
:子模块,用于在当前 Git 仓库中以子目录的形式引用其它 Git 仓库。
- 相关命令:
git submodule add <repository_url> [<path>] [--name <repo>] [-b <branch>] # 添加 submodule update # 从远程仓库拉取 submodule ,根据记录的 commit id --remote # 根据 .gitmodules 中配置的 branch 进行拉取 --recurse # 递归拉取所有嵌套的 submodule sync # 将 .gitmodules 文件中的配置同步到 .git/config 中(默认不会自动同步) status # 显示所有 submodule 的 commit、path、branch 信息
- 添加了 submodule 之后,会在项目根目录生成一个 .gitmodules 文件,用于保存其配置信息。如下:并根据 .gitmodules 文件,在 .git/config 文件中添加配置:
[submodule "python_utils"] # submodule 的名称 url = https://github.com/LeoHsiao1/python_utils.git # submodule 的仓库地址,会通过 git clone 命令下载 path = submodules/python_utils # 将该 submodule 下载到哪个目录 branch = master # 引用的分支
[submodule "python_utils"] active = true url = https://github.com/LeoHsiao1/python_utils.git
- 如果想移除一个 submodule ,需要在上述两个配置文件中删除它。
- 进入 submodule 的目录之后,就相当于处于其 Git 仓库下,可以执行 git checkout 等命令。
- 当前 Git 仓库会引用 submodule 的某个 commit 版本,不会自动更新,需要手动更新:
cd submodule_dir/ git pull cd .. # 以上命令可简化为 git submodule update --remote --merge git add . git commit -m 'Updated submodule' git push
- 当前 Git 仓库会引用 submodule 的某个 commit 版本,不会自动更新,需要手动更新:
# 远程仓库
可以将本机的 Git 仓库(称为本地仓库),推送到 Git 服务器存储(称为远程仓库)。也可以从 Git 服务器下载 Git 仓库到本机。
一个本地仓库可以绑定 0 个或任意个远程仓库。
- 配置之后,通过 repo name 或 URL 即可引用远程仓库。
- 多个用户同时修改一个 Git 仓库时,常见的方案:
- 部署一个 Git 服务器,创建一个远程仓库。
- 每个用户在自己电脑上,创建一个本地仓库,分别修改。
- 当一个用户在本地仓库新增 commit 之后,通过 git push 命令同步到远程仓库。然后其他用户通过 git pull 命令,将远程仓库新增的 commit 同步到各自的本地仓库。
本地仓库的每个分支通常会关联到远程仓库中的一个分支,称为跟踪(tracked)。
- 假设本地分支 master 跟踪了远程分支 master ,则执行 git push 命令时会将本地分支同步到远程分支,执行 git pull 命令时会将远程分支同步到本地分支。
- 建立 tracked 关系时,Git 会在本地仓库创建一个代表远程分支的分支,称为 remote-tracking branch ,命名格式为
remotes/<repo>/<branch>
。- 例:执行 git clone 时,默认将远程仓库命名为 origin ,让本地分支 master 跟踪 origin 仓库中的分支 master 。为此,会在本地仓库创建一个 remote-tracking branch ,名为 `remotes/origin/master`` 。
- remote-tracking branch 用于跟踪远程分支指向的 commit 。但本地仓库可能长时间未与远程仓库同步,因此 remote-tracking branch 不一定反映远程分支的最新位置。
- 最初,本地分支、远程分支、remote-tracking branch 三者指向相同的 commit 0 。
- 假设用户 A 在本地仓库新增 commit 1 ,则本地分支指向 commit 1 ,远程分支、remote-tracking branch 依然指向 commit 0 。
- 此时用户 B 新增 commit 2 到远程仓库,则用户 A 的本地分支指向 commit 1 ,remote-tracking branch 指向 commit 0 。
- 等到用户 A 执行 git fetch 命令,则本地分支指向 commit 1 ,远程分支、remote-tracking branch 指向 commit 2 。
- 用户执行 git fetch 命令时会将远程分支同步到 remote-tracking branch ,使得两者指向相同的 commit 。此时不会同步到本地分支,除非执行 git pull 命令。
本地仓库与远程仓库之间,有两种数据传输方式:
- 基于 SSH 协议:
- 先生成一对 SSH 密钥,将密钥保存在本机的
~/.ssh/id_rsa
文件中,将公钥保存到 Git 服务器上。 - 然后在本机连接到 Git 服务器,使用私钥文件进行认证。
- 先生成一对 SSH 密钥,将密钥保存在本机的
- 基于 HTTPS 协议:
- 先在 Git 服务器上创建账号。
- 然后在本机连接到 Git 服务器,输入账号、密码进行认证。
每次 pull、push 都需要输入账号、密码,比较麻烦,可以将输入的凭证缓存起来:
git config --global credential.helper cache # 将凭证在内存中缓存 15 分钟 git config --global credential.helper store # 将凭证持久保存,以明文形式保存到 ~/.git-credentials 文件中
- 基于 SSH 协议:
常见的 Git 服务器软件:
- GitLab :提供了代码托管、项目管理、Wiki、CI/CD 等丰富的功能。可使用公网版、私有部署版。
- GitHub :功能比 GitLab 少些。只可使用公网版。
- Gogs :只有代码托管功能,轻量级。可使用公网版、私有部署版。
- Gitee :从 Gogs 分叉而来,功能更多,页面像 GitHub 。
# 相关命令
git clone <URL> [dir] # 将一个远程仓库克隆到本地,默认是保存到一个与仓库同名的子目录中
-b <branch> # 切换到指定分支,默认是远程仓库的 HEAD 分支
--depth <n> # 浅克隆(shallow clone),只下载最近的 n 个版本的文件,默认会下载全部版本
--recursive # 递归克隆所有 submodule ,默认不会克隆 submodule
git remote # 显示已配置的所有远程仓库的名字
-v # 显示各个远程仓库的 URL
show <repo> # 显示某个远程仓库的地址、所有 tracked 的远程分支
add <repo> <URL> # 添加一个远程仓库,并设置其名字
rm <repo> # 删除一个远程仓库
rename <repo> <repo> # 重命名一个远程仓库
prune <repo> # 如果一个 tracked 的远程分支在远程仓库不存在,则删除本地仓库中的 remote-tracking branch ,但本地分支依然保留,只是没有 tracked 到远程分支
git fetch [repo 或 URL] # 拉取远程仓库的最新内容(包括分支、标签),但只是下载到本地仓库,不会修改本地分支
--all # 拉取所有远程仓库(默认只是 origin 仓库)
--tags # 拉取标签
--prune # 先执行 git remote prune ,然后执行 fetch
--prune-tags # 如果一个 tag 在远程仓库不存在,则删除本地仓库中的同名 tag 。然后执行 fetch
--dry-run
git pull [repo 或 URL] # 先 fetch 远程仓库,然后将所有 tracked 的远程分支合并到本地分支
origin master # 拉取远程仓库的 master 分支,合并到本地的 HEAD 分支
git push [repo 或 URL] # 推送本地仓库到远程仓库。默认会推送所有 tracked 分支,但不会推送 tag
--force # 强制推送,即清空远程仓库后再上传本地仓库
--all # 推送本地仓库的所有分支。如果不存在 tracked 的远程分支,则自动创建它
<tag> # 推送一个标签
--tags # 推送所有标签
--delete origin <refs> # 删除远程的分支或标签
- 执行 git fetch、pull、push 时,如果不指定远程仓库,则默认使用 origin 仓库。
- 执行 git push 时,会先把要传输的 commit 打包为一个 pack 文件,然后用 git-send-pack 命令发送到服务器。而服务器会用 git-receive-pack 命令接收 pack 文件。
- 有的服务器会限制单次 push 的 pack 文件的最大体积,比如 10M 。
- 例:推送一个本地分支到远程仓库
git push origin master : origin/master # 推送分支 master 到远程仓库 origin ,并与远程分支 master 合并 git push origin : origin/master # 推送一个空分支,这会删除指定的远程分支
- 例:在远程仓库创建一个 test 分支,然后在本地仓库查看
[root@CentOS ~]# git branch -a # 查看当前分支,此时没看到远程分支 test * master remotes/origin/HEAD -> origin/master remotes/origin/master [root@CentOS ~]# git fetch # 拉取远程仓库 From https://github.com/LeoHsiao1/Notes * [new branch] test -> origin/test [root@CentOS ~]# git branch -a # 此时可看到远程分支 test * master remotes/origin/HEAD -> origin/master remotes/origin/master remotes/origin/test [root@CentOS ~]# git checkout test # 切换到本地分支 test ,这会自动创建它,并跟踪到远程分支 test Switched to a new branch 'test' Branch 'test' set up to track remote branch 'test' from 'origin'. [root@CentOS ~]# git checkout test2 # 切换到本地分支 test2 ,这不会自动创建它,因为不存在对应的远程分支 error: pathspec 'test2' did not match any file(s) known to git [root@CentOS ~]# git branch -a # 查看此时的分支 master * test remotes/origin/HEAD -> origin/master remotes/origin/master remotes/origin/test
# 裸仓库
- Git 服务器上通常以裸仓库的形式,存储多个远程仓库。
- 执行
git init --bare
会创建一个裸仓库。- 它不会创建 .git 子目录,而是将 Git 仓库中的文件直接存储到项目根目录。并且通常将项目根目录加上扩展名 .git 。
- 它不支持 git commit ,只能通过 git push 的方式修改。
# LFS
- Git LFS(Large File Storage):Git 的一种插件,用于存储大文件。
- 原理:将一些大文件存储在 Git 仓库外部(位于
.git/lfs/
目录下),只在 Git 仓库内通过指针引用。在 pull 远程仓库时,默认只拉取当前版本的大文件。 - 相关命令:
yum install git-lfs # 安装 lfs git lfs track "*.jpg" # 将文件标记为大文件,被 lfs 跟踪
- 原理:将一些大文件存储在 Git 仓库外部(位于
# 相关概念
# SCM
Git 属于软件配置管理(Source Code Management ,SCM)工具,同类产品包括:
- Subversion :简称为 svn 。
- 集中式管理。代码仓库只能存储在服务器上,用户每次拉取、提交代码时,都需要通过客户端连接服务器。
- 以多个子目录的形式管理代码仓库,目录结构如下:
repository/ ├── branches # 存放各个分支的项目代码 ├── tags # 存放各个版本的项目代码 └── trunk # 存放主干分支的项目代码
- 用户每次拉取、提交代码时,可以处理指定路径的目录、文件,而不必处理整个代码仓库。
- Mercurial :采用 Python 语言开发。
# GPG 签名
GitLab、GitHub 等平台支持对每个 git commit 生成一个数字签名,保存在 comment 中。步骤如下:
- 用户生成一对 GPG 私钥、公钥,在其中记录自己的用户名、邮箱地址。
- 用户使用私钥签署每个 commit 。
git config --global user.signingkey ****** git commit -S -m "..." git push
- 用户在平台的设置页面登记 GPG 公钥,平台会自动验证各个 commit 的签名是否有效。
- 如果有效,则显示一个 Verified 标志,证明该 commit 是由该用户提交的,并且 commit 内容没有被篡改。
# git flow
:一种 Git 使用策略,适合管理复杂的项目。
- 在 Git 仓库中至少使用以下两个分支:
- master 分支:用于保存正式发布的版本。
- dev 分支:用于保存开发环境的版本。平时的代码都提交到 dev 分支,发布稳定版本时才合并到 master 分支。
- 可以视情况创建以下临时分支:
- feature 分支:从 dev 分支创建,用于开发一个新功能,完成之后就合并到 dev 分支。
- hotfix 分支:从 dev 分支创建,用于解决一个 bug ,完成之后就合并到 dev 分支。
- release 分支:从 dev 分支创建,用于发布一个新版本,测试通过之后就合并到 master 分支,并加上一个 tag ,声明版本号。
- 对 Git 仓库加上权限控制,比如:
- 禁止对 master 分支 push -f 。甚至禁止直接 push ,只允许先在其它分支提交代码,然后通过 PR 审批、review ,合并到 master 分支。
- 建议在 comment 的开头声明本次 git commit 的大致分类,便于查看。例如:
# 开头用一个动词来分类,说明修改了哪些内容 Add function test1() Delete ... Modify ... # 开头用一个动词来分类,说明本次修改的原因、动机 Update ... # 更新某个内容 Optimize ... # 优化 Rewrite ... # 重写 Refactor ... # 重构 Fix bug 20200101_001 # 修复某个 bug ,并说明 bug ID # 开头用一个名称加括号来分类 [CI] ... # 关于持续集成 [DOC] ... # 关于文档 [TEST] ... # 关于测试 [FETURE] ... # 关于新的功能、特性
# monorepo
- polyrepo :一种管理方案,将不同项目的代码分别用一个代码仓库中。
- monorepo :一种管理方案,将不同项目的代码放到同一个代码仓库中。
- 优点:
- 方便用户获取所有项目的代码。
- 方便统一管理所有项目,比如复用一些配置文件。
- 缺点:
- 不同项目的代码没有权限隔离,不安全。
- 单个仓库的体积很大,不方便存储。
- 优点: