【Git 学习笔记_19】第九章 Git 仓库的运维

文章目录

  • [第九章 Git 仓库的运维](#第九章 Git 仓库的运维)
    • [9.1 整理远程仓库](#9.1 整理远程仓库)
    • [9.2 手动执行垃圾回收](#9.2 手动执行垃圾回收)
    • [9.3 关闭自动垃圾回收](#9.3 关闭自动垃圾回收)
    • [9.4 分隔仓库](#9.4 分隔仓库)
    • [9.5 重写提交历史 -- 变更单个文件](#9.5 重写提交历史 – 变更单个文件)
    • [9.6 以镜像的方式创建仓库备份](#9.6 以镜像的方式创建仓库备份)
    • [9.7 一个快捷子模块操作演示](#9.7 一个快捷子模块操作演示)
    • [9.8 子树合并](#9.8 子树合并)
    • [9.9 子模块与子树对比](#9.9 子模块与子树对比)
    • 后话

第九章 Git 仓库的运维

本章将介绍用于 Git 仓库运维工作的各类工具。例如在本地轻松删除远程库中已经删除的分支;垃圾回收的触发及关闭;利用 filter-branch 拆分 Git 仓库或修改仓库历史;最后简要介绍一下怎样以子项目的方式(应用子模块或子树策略),将 Git 库集成到另一个 Git 库。

本章相关主题

  • 整理远程仓库
  • 手动执行垃圾回收
  • 关闭自动垃圾回收
  • 分隔仓库
  • 重写提交历史 -- 变更单个文件
  • 以镜像的方式创建仓库备份
  • 一个快捷子模块操作演示
  • 子树合并
  • 子模块与子树对比

9.1 整理远程仓库

项目的开发往往是在 features 特性分支上进行的。开发完成后并入主分支,并将原特性分支删除。对于不是推送该分支的本地库,Git 不会自动将分支变更信息自动更新到之前克隆的其他本地仓库,对这些本地仓库而言,必须手动同步远程分支信息。

使用命令:git fetch --prune

功能演示:

bash 复制代码
# Prepare repos: a bare and a clone
$ git clone --bare https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git hello_world_flow_model_remote
$ git clone hello_world_flow_model_remote hello_world_flow_model
# delete branches from the remote repo
$ cd hello_world_flow_model_remote 
$ git branch -D feature/continents  
Deleted branch feature/continents (was 5bd5222).
$ git branch -D feature/printing  
Deleted branch feature/printing (was a5da07d).
$ git branch -D release/1.0  
Deleted branch release/1.0 (was 134fe4b).
# enter cloned repo
$ cd ../hello_world_flow_model 
$ git checkout develop 
$ git reset --hard origin/develop
# Check branches from remote
$ git branch -a 
* develop
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/develop
  remotes/origin/feature/cities
  remotes/origin/feature/continents
  remotes/origin/feature/printing
  remotes/origin/master
  remotes/origin/release/1.0
# fetch with default options
$ git fetch 
$ git pull 
Already up to date. 
$ git branch -a 
* develop
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/develop
  remotes/origin/feature/cities
  remotes/origin/feature/continents
  remotes/origin/feature/printing
  remotes/origin/master
  remotes/origin/release/1.0
# Sync with remote repo by using --prune
$ git fetch --prune
From C:/Users/ad/Desktop/hello_world_flow_model_remote
 - [deleted]         (none)     -> origin/feature/continents
 - [deleted]         (none)     -> origin/feature/printing
 - [deleted]         (none)     -> origin/release/1.0
# Check again
$ git branch -a
* develop
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/develop
  remotes/origin/feature/cities
  remotes/origin/master

此外,也可以用 git pull --prune 达到相同效果,或者运行命令 git remote prune origin。不过,后者虽然能够删除远程库中已经不存在的分支,但不会自动更新本地的远程跟踪分支信息。

9.2 手动执行垃圾回收

周期性运作 git 仓库时,有时可能会看到某些命令触发了 git 的垃圾回收,同时将松散对象变为致密文件(git 的对象式存储)。这也可以通过手动运行命令 git gc 实现。手动触发垃圾回收,可用于存在大量松散对象的场合。一个松散对象(loose object)可以是一个 blob、一个 tree、或者一个 commit。正如第一章中提到过的,这些对象是在添加到 Git 或创建版本时进入 Git 数据库的。这些对象最早以单个文件形式存放在 .git/objects 中(不可直接访问),然后通过事件或手动触发的方式打包为压缩文件以减少磁盘占用。当开启一个新项目并添加大量文件到 git 库时,会产生大量的松散对象。这时执行垃圾回收可以确保松散对象被压缩打包,同时未引用对象会被清除。后者尤其适用于本地删除了某些分支或版本、并希望引用它们的对象也一并清理的场景。

功能演示:

bash 复制代码
# Prepare repo
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_hello_world_flow_model.git  
$ cd hello_world_flow_model 
$ git checkout develop 
$ git reset --hard origin/develop 
$ git count-objects
0 objects, 0 kilobytes
$ git fsck --unreachable 
Checking object directories: 100% (256/256), done.
Checking objects: 100% (58/58), done.
$  du -sh .git
56K     .git
$ git remote rm origin 
$  git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (58/58), done.
unreachable commit 85d110c5bbe00733ad0f5404d7b88ef320217808
unreachable blob e26174ff5c0a3436454d0833f921943f0fc78070
unreachable tree f03964e50809d5a0a9d35c208001b141ac36d997
$ git gc
Enumerating objects: 55, done.
Counting objects: 100% (55/55), done.
Delta compression using up to 16 threads
Compressing objects: 100% (38/38), done.
Writing objects: 100% (55/55), done.
Total 55 (delta 20), reused 48 (delta 15), pack-reused 0
$ git count-objects
3 objects, 1 kilobytes
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (55/55), done.
unreachable commit 85d110c5bbe00733ad0f5404d7b88ef320217808
unreachable blob e26174ff5c0a3436454d0833f921943f0fc78070
unreachable tree f03964e50809d5a0a9d35c208001b141ac36d997
Verifying commits in commit graph: 100% (19/19), done.
$ du -sh .git
60K     .git
# before
$ git count-objects
3 objects, 4 kilobytes
$ git gc --prune=now
Enumerating objects: 55, done.
Counting objects: 100% (55/55), done.
Delta compression using up to 16 threads
Compressing objects: 100% (33/33), done.
Writing objects: 100% (55/55), done.
Total 55 (delta 20), reused 55 (delta 20)
# after
$ git count-objects
0 objects, 0 kilobytes
$ git fsck --unreachable
Checking object directories: 100% (256/256), done.
Checking objects: 100% (55/55), done.
$ du -sh .git
56K     .git

可以看到,不可及对象已被删除,且不存在松散对象,仓库大小重新还原。

原理剖析

git gc 命令通过压缩修订文件、删除未被引用对象等实现了对 git 库的优化。在被废弃(已删除)的分支上,通过 git add 调用的 blob、通过 git commit --amend 或其他命令丢弃、撤回的提交或其他命令可能会留下对象。

默认情况下,对象在创建时已经用 zlib 进行了压缩,并且当移动到包文件中时,Git 确保只存储必要的更改。例如,如果在一个大文件中只更改一行,那么将整个文件再次存到包文件中会浪费一些空间。实际情况是,Git 将最新文件作为一个 整体 存储在包文件中,并且仅存储旧版本的变更增量。这样的设计非常巧妙,因为最新的文件更有可能被用到,而 Git 不必为此进行增量计算。这似乎与第一章中的相关论述相矛盾。当时提到,Git 存储快照而不是增量。但回忆一下快照是怎么来的就清楚了。 Gitblob 中的所有文件内容进行哈希散列,生成 treecommit 对象;commit 对象使用根节点树的 sha-1 散列值描述完整的树状态。将对象存储在包文件中对树状态的计算 没有影响 。签出较早版本时,Git 会确保 sha-1 哈希值与请求的分支、提交或标签相匹配。

9.3 关闭自动垃圾回收

垃圾回收的自动触发可以设置为关闭,需要是再手动执行。这样在查询丢失版本时,就不会受到自动回收的影响。

要禁用自动垃圾回收,需要设置 gc.auto0

bash 复制代码
# (on Linux) Query the default value
$ git config gc.auto
$ echo $?
1
# disable auto gc
$ git config gc.auto 0 
$ git config gc.auto 
0
# test gc automatically (disabled)
$ git gc --auto
# manually invoke gc
$ git ac

9.4 分隔仓库

有时,Git 托管的一个逻辑上的完整项目,实际上是有多个子系统构成的。这可能是刻意为之;也有可能同一个项目被其他 Git 托管的项目所依赖;亦或是,随着开发到了某个时间节点,其中一个子项目需要单列出来等等。这时可以通过剥离相关的子文件夹、关联文件,并携带相关的提交历史记录来实现。

bash 复制代码
$ git clone https://git.eclipse.org/r/jgit/jgit
$ cd jgit
$ git checkout master
# Save current branch
$ current=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD)
# create local branches from all the remote branches
$ for br in $(git branch -a | grep -v $current | grep remotes | grep -v HEAD); do git branch ${br##*/} $br; done
Branch 'next' set up to track remote branch 'next' from 'origin'.
Branch 'stable-0.10' set up to track remote branch 'stable-0.10' from 'origin'.
Branch 'stable-0.11' set up to track remote branch 'stable-0.11' from 'origin'.
Branch 'stable-0.12' set up to track remote branch 'stable-0.12' from 'origin'.
...

注意:

  • 这里的 grep -v $current 以及 grep -v HEAD 是滤除含有 $currentHEAD 头的分支,为的是仅保留远程跟踪分支;
  • git branch ${br##*/} $br 是从最后一个 / 开始截取字符串,作为本地分支名(如: remotes/origin/stable-0.10 的本地分支名称变为 stable-0.10);

接下来,在与 jgit 库平级的位置新建一个 shell 脚本 clean-tree。该脚本接收一个路径参数,作用是从 jgit 库删除除指定路径外,所有 git 索引库中的内容:

clean-tree

bash 复制代码
#!/bin/bash 
# Clean the tree for unwanted dirs and files 
# $1 Files and dirs to keep 

clean-tree () { 
  # Remove everything but $1 from the git index/staging area 
  for f in $(git ls-files | grep -v -E "$1" | grep -o -E "^[^/\"]+" | sort -u); do 
    git rm -rq --cached --ignore-unmatch $f 
  done 
} 

clean-tree $1 

其中:

  • grep -v -E "$1":剔除包含 $1 的查询结果;
  • grep -o -E "^[^/\"]+":仅提取首个 / 之前的路径部分;
  • sort -u:排序并剔除重复项;
  • git rm -rq --cached --ignore-unmatch $f:从暂存区删除(--cache)、文件不存在时不中断执行(--ignore-unmatch)、静默执行(-q)、包含子文件夹(-r

事实上,git 的暂存区内包含了自上一次提交为止被 git 托管的 所有文件内容 ,以及通过 git add 命令引入的文件。但执行 git status 时,只会列出暂存区与上一次提交、以及暂存区与当前工作区内 的变更情况。

本节示例演示如何将 org.eclipse.jgit.http 下的内容,连同项目许可、忽略文件、README 文件以及 .gitattributes 文件单列出来,重写原 git 提交历史的具体步骤:

先将 jgit 库与 clean-tree 脚本放到桌面 splitTest 文件夹内,执行以下命令:

bash 复制代码
# (on Linux)
$ chmod +x clean-tree
$ keep="org.eclipse.jgit.http|LICENSE|.gitignore|README.md|.gitattributes"
$ git stash
$ git filter-branch --prune-empty  --index-filter "\"/mnt/c/Users/z/Desktop/splitTest/clean-tree.sh\" \"$keep\"" --tag-name-filter cat -- --all

注意,第 4 行的 git stash 是实操时临时加入的,因为时隔 3 年克隆该仓库后,当前工作区还有大量被修改内容,不满足仓库剥离条件。因此执行第 5 行命令时,必须确保工作区是"干净的"。正常情况下该命令执行过程如下:

由于需要处理所有的 commit 节点,命令执行会持续一段时间。待执行结束,可以通过如下命令检验是否仅有指定内容被保留:

bash 复制代码
$ git ls-tree --abbrev HEAD 

此时 git filter-branch 命令在命令空间 /refs/original 下保留了所有之前的引用、分支、标签等。经过确认,新的提交历史无需保留之前的 refs 引用,它们不仅不被新版引用,还占用了大量磁盘空间。清理这些内容要用到 git 的垃圾回收相关命令:

bash 复制代码
# before cleanup
$ du -sh .git
# do the cleanup with git gc
$ git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --prune=now
# after cleanup
$ du -sh .git

git filter-branch 可以配置不通的筛选条件。本例仅演示了如何从原仓库清除文件及文件夹,其中 index-filter 参数的作用十分关键,可以在不实际签出一个 tree 对象到本地、并在将提交信息写入 git 数据库之前变更 git 索引的具体内容,因此节约了大量的磁盘读写开销。这使得手动创建的 clean-tree 脚本可以从索引直接删除不需要的文件和文件夹。

git filter-branch 还可以设置如下筛选参数:

  • env-filter
  • tree-filter
  • msg-filter
  • subdirectory-filter

关键提示

从实操截图可以看到,git 命令行提示 git filter-branch 存在大量问题(执行极慢(平均一个节点重写需要 1 ~ 2 秒,实操时的 8700+ 提交节点需要连续执行数小时之久)、重写历史时存在其他连带问题等,详见 https://github.com/newren/git-filter-repo/),目前已不推荐使用;`git` 官方推荐换用 git filter-repo 命令。

9.5 重写提交历史 -- 变更单个文件

本节将使用 git filter-branch 命令的 tree-filter 参数,演示如何从 git 库删除敏感信息。

bash 复制代码
$ git clone https://github.com/PacktPublishing/Git-Version-Control-Cookbook-Second-Edition_Remove-Credentials.git 
$ cd Git-Version-Control-Cookbook-Second-Edition_Remove-Credentials 
$ cat .\.credentials
username = foobar
password = verysecret
$ git filter-branch --prune-empty  --tree-filter "test -f .credentials && sed -i '' -e 's/^\(.*=\).*$/\1/' .credentials || true" -- --all
$ cat .\.credentials
username =
password =

同上一节示例类似,此时 git 也保留了之前的所有引用,需要执行垃圾回收。这里由于文件本来不多,且已经可以看到演示效果了,垃圾清理环节就省略了。

注意

Git 会对每一个提交节点应用 tree-filter 参数,如果返回一个非零的退出码(non-zero exit code),filter-branch 命令就会中断。因此一定要记得处理异常情况。这也是为什么执行 sed 命令前先判定目标文件 .credentials 是否存在的根本原因。存在则执行 sed;否则返回 true 以保证 filter-branch 命令不中断。

9.6 以镜像的方式创建仓库备份

Git 的分布式设计使得每一次克隆都可视为一次备份。备份 Git 库也有一些实用技巧。通常,工作区内拥有当前跟踪分支的最新备份,.git 文件夹则存放 git 的完整提交历史。供数据推送或拉取用的服务端仓库,则通常以 bare 库的形式存在,即没有工作区的 git 库,大致就是 .git 文件夹的内容。而一个镜像库(mirror repository)除了获取 refs/* 下的内容与 bare 库(获取 refs/heads/*)不同外,两者几乎是相同的。

本节仍然以 JGit 库为例,分别演示常规库、bare 库、以及镜像库的克隆:

bash 复制代码
$ git clone https://git.eclipse.org/r/jgit/jgit 
$ git clone --reference jgit --bare https://git.eclipse.org/r/jgit/jgit 
$ git clone --mirror --reference jgit https://git.eclipse.org/r/jgit/jgit jgit.mirror
# Check local branches
$ cd jgit 
$ git branch 
* master 
$ cd ../jgit.git # or cd ../jgit.mirror 
$ git branch 
* master 
  stable-0.10 
  stable-0.11 
  stable-0.12 
... 
# List the fetch refspec of origin 
$ cd ../jgit.mirror
$ git config remote.origin.fetch 
+refs/*:refs/* 
# List the different refs namespaces in the mirror repository
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u 
refs/cache-automerge 
refs/changes 
refs/heads 
refs/meta 
refs/notes 
refs/tags 
# test in bare repo
$ cd ../jgit.git
$ git config remote.origin.url 
https://git.eclipse.org/r/jgit/jgit 
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u 
refs/heads 
refs/tags 
# test in normal repo
$ cd ../jgit 
$ git config remote.origin.fetch 
+refs/heads/*:refs/remotes/origin/* 
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u 
refs/heads 
refs/remotes 
refs/tags 

可以看到,只有镜像库才有如下命名空间:refs-cache-automergechangesmeta、以及 notes;也只有常规库才有命名空间 refs/remote

The normal and bare repositories are pretty similar, only the mirror one sticks out. This is due to the refspec fetch on the mirror repository, +refs/*:refs/*, which will fetch all refs from the remote and not just refs/heads/* and refs/tags/* as a normal repository (and a bare repository) does. The many different ref namespaces on the JGit repository is because the JGit repository is managed by Gerrit Code Review. It uses different namespaces for repository-specific content, such as change branches for all commits submitted for code review, and metadata on code review score.

The mirrorrepositories are ideal when you would like a quick way to back up a Git repository. It ensures that you have everything included without the need for additional access than the Git access to the machine that hosts the Git repository.

发散

GitHub 上的仓库在获取到本地时,如果有 pull request 记录,还会产生相应的命名空间 refs/pull/*

bash 复制代码
$ git clone --mirror git@github.com:jenkinsci/extreme-feedback-plugin.git 
$ cd extreme-feedback-plugin.git 
$ git show-ref | cut -f2 -d " " | cut -f1,2 -d / | sort -u 
refs/heads 
refs/meta 
refs/pull 
refs/tags

9.7 一个快捷子模块操作演示

从 9.7 小节开始,由于使用场景不常见,仅做概念介绍,不实测演示用例。

有时在一个项目中可能会有使用另一个用 git 托管的项目的需求。这个需要的"项目"既可以是你开发的另一个项目,也可以是一个第三方库。而且你希望这些项目相互独立。Git 为这样的应用场景提供了专门的处理机制,叫 子模块submodules)。其中心思想是允许用户以子目录的形式将另一个仓库克隆到当前仓库,同时确保两个库相互独立。如下图所示:

代码演示:(略)

9.8 子树合并

另一个 子模块策略 的备选方案,是 子树合并subtree merging)。该策略应用于 git 的合并操作中,可以将目标分支(或本节演示用例中用到的,一个目标项目)合并到当前项目的子目录下,而非常规的根目录下。该模式会将子项目的提交历史合并到当前项目中,这一点与子模块模式不同。

代码演示:(略)

9.9 子模块与子树对比

无论是用子模块策略还是子树策略来处理,都不存在简单粗暴的选择模式。选用子模块,则压力更多向当前项目的开发人员倾斜,因为需要确保子模块和父级项目的同步;如果选用子树策略,对开发人员而言复杂度没有额外增加,子项目的更新同步、以及提交节点指定到子项目的工作就落在了仓库运维人员身上。两种模式无所谓优劣,顶多是需要熟悉哪一种模式罢了。

另一个全新的思路,是在利用父项目的代码构建系统(如 MavenGradle 等)拉取必要的依赖。


后话

当时自学本章内容时,过程比较曲折,在公司加班较多,因此个别知识点可能也没有理解得很透彻。为此,我将这本书上传到了 我的资源 里,感兴趣的朋友可以自行对照学习。如有疑问或任何建议、意见,欢迎留言交流!

相关推荐
AITIME论道36 分钟前
论文解读 | EMNLP2024 一种用于大语言模型版本更新的学习率路径切换训练范式
人工智能·深度学习·学习·机器学习·语言模型
明明真系叻2 小时前
第二十六周机器学习笔记:PINN求正反解求PDE文献阅读——正问题
人工智能·笔记·深度学习·机器学习·1024程序员节
青春男大3 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
sin22014 小时前
idea集合git使用
git
mashagua4 小时前
RPA系列-uipath 学习笔记3
笔记·学习·rpa
nikoni234 小时前
828考研资料汇总
笔记·其他·硬件工程
沐泽Mu4 小时前
嵌入式学习-QT-Day05
开发语言·c++·qt·学习
锦亦之22335 小时前
cesium入门学习二
学习·html
m0_748256145 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
IT古董6 小时前
【机器学习】机器学习的基本分类-半监督学习(Semi-supervised Learning)
学习·机器学习·分类·半监督学习