无需放弃变更、关闭占用程序!用暂存区和 git底层命令实现 Git 变更备份

无需放弃变更、关闭占用程序!用暂存区和 git底层命令实现 Git 变更备份

前言

在日常开发中,git stash虽为我们备份数据的常用命令,但其操作过程需要放弃当前的变更,这对于 win 平台下编辑二进制资源时会造成很多麻烦,由于主程序会对修改的二进制资源进行占用,此时执行储藏大概率会报错,如果想要储藏成功,就需要关闭主程序,然后再操作储藏,才能完成数据的备份,如果主程序比较大比如游戏引擎,那么整个操作流程将相当繁重耗时。

而本文则将介绍一种更灵活轻便的 git仓库变更数据的备份方案:通过暂存区(Index)git create-commit等底层 git 命令,手动构建出stash的提交,从而在不放弃任何变更,也不需要关闭占用程序前提下,完成工作区变更数据的备份,从而提高研发效率。

问题

使用 git 进行版本控制时,通常会在工作区产生变更,而某些情况我们不想要提交而仅想要备份一下修改的数据,常规的做法是git stash,它会自动打包这些状态并自动将工作区的状态清空,但是这个过程无法干预,并且如果出现变更文件被其他程序占用(例如打开并修改的 excel 文件),就会导致储藏失败。

如下,当一个 hello.xlsx 的文件,并用 excel 打开修改,此时执行 git stash -u 会报错:

其根本原因是 git stash 包含的是备份数据+放弃变更的连贯过程,但由于 xlsx 文件被 excel 程序占用,导致 stash 不能完成放弃变更而最终报错退出。

而我们有时的数据备份,通常只需要将当前工作区的变更读取出来,进行备份操作,无需放弃变更,而本文则提供这样一种数据备份的方法。

如果你不想了解实现机制,则可以直接跳转最后查看如何具体实现。

相关知识

Git object

理解 Git 对象的工作原理,有利于了解 Git 的暂存区、提交机制以及接下来要介绍的git create-commit 操作。

在 Git 的世界里,一切操作都围绕着 Git 对象(Git object)展开,版本数据库内存储的是k-v数据(<hash_ID, object>),通过hash ID 可以直接获取到对象值,其中object主要分为四种类型:

  • blob(文本对象)。blob对象用于存储文件的内容,比如文本文件的具体代码、图片文件的二进制数据等,它不包含任何文件名或目录结构信息。

  • tree(树对象)。tree对象就像文件系统中的目录,用于组织和管理blob对象以及其他tree对象,记录了文件的名称、权限和指向blob或子tree的指针,从而构建出完整的目录树结构。

  • commit(提交对象)。commit对象则是版本控制的核心,每个提交都包含了指向了根目录的tree对象的指针 ,用于记录项目在某一时刻的状态;同时还包含了作者信息、提交者信息、提交时间以及指向父提交的指针,通过这些指针可以串联起整个项目的版本演进历史,形成一个有向无环图

  • tag(标签对象)。tag对象主要用于给特定的commit添加标签,常用于标记发布版本、重要里程碑等,分为轻量级标签(仅保存提交对象的 SHA-1 哈希值)和附注标签(包含标签名、标签说明、标签创建者等详细信息)。

    本文【不放弃变更完成储藏】功能的实现,本质上是将创建 git 对象,将其存储到git 数据库下,并操作这些 Git 对象的创建、引用关系。

暂存区中的 tree object

暂存区(也称为索引,Index)是 Git 用于存储下次提交内容的区域,它在 Git 对象模型中扮演着关键角色,作为提交前置状态的暂存区,暂存区实际上可以作为 tree object 创建的"工具"。

当执行git add命令将修改的文件添加到暂存区时,Git 会创建一个tree object(树对象) 来记录这些文件的状态。Tree object 本质上是一个文件和子目录的目录树结构,它包含了文件的元数据信息,如文件名、文件模式(可执行、普通文件等)以及指向 blob object(存储文件实际内容的对象)的指针。如果暂存区中存在多个目录层级的文件修改,Git 会递归地构建多层 tree object,形成一个完整的目录树结构,从而完整记录整个项目在暂存区的状态。

详细参考此文Git - Git 对象,文中详细介绍了如何基于 git 的 plumbing commands 构造一个提交(对象)。

Git Stash 的底层结构

最终备份的数据还是会以储藏形式呈现,所以我们需要了解储藏(提交)的结构,方便指引我们后续去手动创建这样的储藏。

每个储藏其实也是一笔提交。

要实现自定义储藏,首先需明确:常规的git stash本质是创建一个 "特殊提交"。在储藏了未跟踪文件的情况下,该提交包含三个父节点;默认情况下则包含两个父节点,它们分别对应储藏前的不同状态:

  1. 父提交 1( stash@{0}^1 :即git log能看到的最后一个 commit,这是储藏时当前分支的最新提交(HEAD);

  2. 父提交 2( stash@{0}^2 :包含了储藏时暂存区(Index,又名 staging area)中已通过git add操作、但尚未提交的变更集;

  3. 父提交 3( stash@{0}^3 :仅在使用git stash -ugit stash -a时存在,包含了储藏时工作树中未跟踪的文件(-u)以及被忽略的文件(-a) 。

git stash 底层实现参考:what-is-in-a-stash,其详细介绍了 stash 各个parent 的含义及作用。

二、实现关键:从变更文件到暂存区(index)创建 commit

存储 blob对象------git add foo.txt

git add foo.txt 用于将工作区 foo.txt 的变更添加到暂存区。执行该命令时,会先创建变更文件对应的 blob objectgit add 实际上是一个多步骤操作,blob object 的创建只是其中一环。当变更被添加到暂存区后,新生成的 blob object 便会被暂存区的 tree object 所引用。

git 还提供了纯粹的的数据库操作方法------ git hash-object ,来让你 将某些 blob 对象存到git 数据库下,具体可以参考:git-scm.com/docs/git-ha...

暂存区中获取tree 对象------write-tree

git-write-tree 命令会扫描当前暂存区(index)的内容,将文件的状态、权限等信息组织成一个树状结构,构造一个指向树根节点的对象,并返回其对应的 hash_ID,该 tree对象代表了暂存区的当前状态。

使用示例:执行 git write-tree 后,会输出一个 40 位的哈希值,该哈希值对应 index 内部保存的 tree object。可通过 git cat-file -p <hash> 查看树对象的具体内容。

具体参考: Git - git-write-tree Documentation

tree对象创建commit对象------commit-tree

从前置的知识可以推得:要想将工作区的修改储藏起来,需要先构造出储藏的3个 parent 提交,而提交对象的创建,除了直接调用 git commit 命令,还有一个较为底层的 git 命令 commit-tree

git commit-tree 的作用是可以从已有的 tree 对象创建新的 commit 对象:

其基本语法为 git commit-tree <tree> [-p <parent>] [-m <message>],其中 <tree> 是必填的树对象哈希值,-p 用于指定父提交(可指定多次以创建合并提交),-m 用于添加提交信息。以下是几种简单的使用示例:

  1. 创建基础提交

    假设已通过 git write-tree 命令获取到树对象哈希值 abc123,创建一个无父提交(适用于初始提交场景)的提交:

sql 复制代码
git commit-tree abc123 -m "Initial commit"
  1. 创建合并提交

    假设有两个父提交 ghi789jkl012,以及对应的树对象 mno345,创建一个合并提交:

css 复制代码
git commit-tree mno345 -p ghi789 -p jkl012 -m "Merge two branches"

具体参考官方文档: Git - git-commit-tree Documentation

因此,我们知道了如何基于工作区变更创建 blob 对象,并如何获取 tree object,最后创建 commit object,而上述的步骤仅设计文件读取,不会产生程序占用导致的报错。那么接下来我们便可以将所有的环节串联,形成最终实现。

三、具体实现

假设当前场景:你在main分支开发,有部分变更是未追踪的(新增而且没有加到暂存区内的)、有部分变更文件是受追踪(修改的),你想要把当前所有修改、新增的文件都备份,所谓备份就是不放弃变更,也不希望受制于占用程序影响导致储藏失败。

步骤 0 备份现有 index

因为我们的核心思路时操作暂存区,为了防止中间步骤失败导致最开始的暂存区状态丢失,需要先备份当前的 index(暂存区)状态备份到 index_backup下:

bash 复制代码
cp .git/index .git/index_backup

后续若出现问题,可根据备份文件恢复暂存区。

步骤 1 git reset HEAD

执行 git reset HEAD 命令,它的作用是将暂存区重置为与 HEAD 所指向的提交一致。这一步会重置清空暂存区,将所有变更文件移除暂存区。由此,我们将工作目录与旧暂存区解耦,为后续精准处理变更文件做好准备。

bash 复制代码
git reset HEAD

步骤 2 git status 获取所有变更文件路径及变更状态

使用 git status 命令查看当前工作目录的状态,它会清晰列出所有变更文件的路径,以及每个文件的变更状态,如 modified(已修改)、deleted(已删除)、untracked(未追踪)等。这些文件路径和变更信息 是后续对不同类型文件进行差异化处理的依据。我们可以将输出内容复制到文本编辑器中,便于后续操作时快速查看和使用。

其中 git 提供了一个适合脚本处理的参数获取工作区的变更

bash 复制代码
git status --porcelain

步骤 3 git add 所有未追踪的文件,并基于 write tree + commit+tree 获取 untracked 文件的 parent 提交

执行 git add . 或指定未追踪文件路径,将所有未追踪的文件添加到暂存区。随后,通过 git write-tree 命令创建一个新的树对象,该对象代表当前暂存区的内容状态。接着使用 git commit-tree 命令创建一个新的提交对象,此提交对象包含了刚刚创建的树对象。为了明确提交的父子关系,指定当前 HEAD 提交作为父提交,这样就得到了未追踪文件的 parent 提交。具体命令如下:

bash 复制代码
git add <untracked_files>  # 添加所有未追踪文件

tree_untracked=$(git write-tree)  # 获取树对象哈希值

commit_untracked=$(git commit-tree $tree_untracked -p HEAD -m "Commit for untracked files")  # 创建提交对象

步骤 4 git add 所有追踪文件,并基于 write tree + commit tree创建追踪文件的 parent 提交

与步骤 3 类似,先执行 git add 命令,将所有已追踪且发生变更的文件添加到暂存区。然后同样通过 git write-tree 创建树对象,再使用 git commit-tree 创建提交对象,并指定合适的父提交(可以是 HEAD 提交或其他相关提交),从而得到追踪文件的 parent 提交。具体操作如下:

bash 复制代码
git add <tracked_changed_files>  # 添加已追踪的变更文件

tree_tracked=$(git write-tree)

commit_tracked=$(git commit-tree $tree_tracked -p HEAD -m "Commit for tracked changed files")

步骤 5 基于 git commit-tree -p [p1] -p [p2] -p [head] -m 'on main: xx'

这里的 [p1] 为步骤 3 中未追踪文件提交的哈希值,[p2] 为步骤 4 中追踪文件提交的哈希值,[head] 为当前分支 HEAD 提交的哈希值。使用 git commit-tree 命令,通过 -p 参数指定上述三个父提交,创建一个新的合并提交。新提交整合了所有变更内容,并且保留了清晰的提交历史脉络。命令示例:

bash 复制代码
git commit-tree -p $commit_untracked -p $commit_tracked -p HEAD -m "on main: Consolidated changes"

步骤 6 恢复 index

根据步骤 0 中备份的 index_backup 文件,恢复暂存区状态。

bash 复制代码
cp .git/index_backup .git/index

四、验证与回滚:确保 Stash 有效且可恢复

1. 验证 Stash 是否创建成功

执行git stash list可看到我们手动创建的 Stash,与常规 Stash 格式一致:

bash 复制代码
git stash list

# 输出示例:stash@{0}: WIP on main: a1b2c3d 初始化项目文档

2. 回滚 Stash(恢复变更)

由于我们的 Stash 完全符合 Git 原生格式,可直接用常规命令恢复:

bash 复制代码
# 恢复Stash并删除Stash记录(同git stash pop)

git stash pop stash@{0}

# 或仅恢复不删除(同git stash apply)

# git stash apply stash@{0}

四、注意事项

1. 版本要求

git create-commit是 Git 2.23.0 + 新增命令,若版本过低需先升级:

bash 复制代码
# 查看Git版本

git --version

# 升级(以Ubuntu为例,其他系统参考官方文档)

sudo apt update && sudo apt install git -y

2. 避免数据丢失的关键

  • 步骤 0 中 "备份索引(.git/index)" 必须执行,否则会覆盖已暂存的变更;

  • 操作前可以执行git diffgit diff --staged记录变更,便于异常时核对。

五、总结

本文方案的核心价值在于:不依赖 Git 自动打包,而是手动拆解 Stash 的底层结构 ,通过暂存区生成 tree object、并利用 git write-tree git create-commit构建父提交,最终实现 "不放弃变更、不用关闭占用程序" 的储藏。这种方式非常适合在游戏开发中引擎占用二进制资源时,进行数据备份操作。

相关推荐
Qres8211 天前
Git安装记录
git
wj3055853781 天前
Codex + Git 开发环境配置指南(WSL版)
linux·运维·git
楠枬1 天前
Git 分支管理
git
phenhorlin1 天前
我做了个工具,让切换 Homebrew 镜像像切 npm 源一样简单
后端·shell
奇怪的点1 天前
git clone失败
git
WaiSaa1 天前
Ubuntu配置Git免密操作
git·ubuntu·gitee
牛奶咖啡131 天前
Git实践——分支管理与标签管理及git个性化配置
git·禁用 fast forward·bug分支的创建与操作·远程分支的查看与推送·拉取仓库·推送指定分支到远程仓库·标签的创建与操作
千寻girling1 天前
五一劳动节快乐 [特殊字符][特殊字符][特殊字符]
java·c++·git·python·学习·github·php
波特率1152001 天前
git指令学习
git·学习
Karry_6661 天前
[特殊字符] Git 提交项目 全套命令(按顺序执行)
git