能否在git仓库之外添加和提交文件?

4
我们系统中有散布在各个角落的文本文件,我们计划将这些文件的所有修改添加到git仓库中。每当脚本对这些文件进行修改时,都是由脚本完成的。因此,我们计划向该脚本添加新命令以将文件添加到git仓库中。但是,这些修改是并发的。我们可以为每个文件构建一个路径,表示其原始路径的原始位置。是否可能同时将这些文件添加到git仓库中?类似于原子操作加入add+commit并指向外部文件路径及其存储库对应路径。类似于以下内容:git --user="Script1 <script1@localhost>" --git-dir=/home/repo/filescollection.git/.git add --external-path=/home/user1/file.txt --repo-path=home_user1_files.txt
1个回答

7
答案既是“是”的,也是“否”的。1

如果您计划仅使用Git的"porcelain"命令,则很明显是“否”的,因为这些命令与一个(单个)工作树的概念一起使用,该工作树包含所有常规格式文件,以及一个索引(保存该工作树的当前状态并构建下一个提交)。有一个HEAD文件,保存着当前分支名称的概念。您需要至少两个单独的porcelain命令,并按照以下顺序执行:

git add <path>
git commit <arguments>

更新单个工作树版本中<path>文件的索引,然后使用该索引和当前的HEAD进行提交。Git在进行提交时会锁定它更新的一些内容,但您需要将自己的锁定叠加在这些锁定之上,以便添加-提交序列看起来是原子的。

(即使您使用--work-tree和/或--git-dir参数重定向各个步骤的各个部分,这仍然是有效的:共享索引在“添加”和“提交”步骤之间必须保持稳定。)

另一方面,如果您愿意走出纯粹的porcelain舒适区,您可以将提交本身作为原子实体完成——但您仍然需要解决某种竞争问题,因此在答案真正从“否”变为“是”之前,您需要解决此问题。要了解其工作原理,我们必须将git addgit commit步骤分开。

首先,git add实际上就是git update-index。我们可以创建一个新的、临时的、私有的索引,并从我们选择的某个特定提交中填充它:

commit_id=...insert some magic here, see below...
export GIT_INDEX_FILE=$(mktemp) # remember to clean it up later too
git read-tree $commit_id

现在,我们可以使用git update-index(或更熟悉和习惯的git add:该环境变量也适用于此)替换索引中的任何给定文件。由于这是我们自己的私有索引,因此它与可能修改任何其他索引的所有其他进程隔离开来。
现在,我们可以执行git commit所执行的步骤:
tree_id=$(git write-tree)

这将临时索引(现在成为我们的临时索引)转换为一个新的顶级树形结构,包括任何子目录的子树,这些都是基于我们之前读入到索引中的内容(使用git read-tree)并更新了(使用git update-indexgit add)。现在,存储在仓库中的是这个顶级树形结构和任何必要的子树,这些子树之前没有被存储在仓库中。新对象会在配置的过期时间内(默认为14天)免受自动 git gc 的影响,因此我们有这么长的时间来完成提交。该命令将新树形结构的ID打印到其标准输出中,我们将其捕获到$tree_id变量中。

接下来,我们需要编写一个提交对象,它引用我们刚刚创建的树形结构,并具有适当的父哈希值。正确的父哈希值显然是$commit_id。我们必须构建一个提交消息,然后运行:

new=$(git commit-tree -p $commit_id $tree_id < message_file)

或类似的操作。这将提交对象写入仓库,并像git write-tree一样打印新对象的ID,我们将其捕获到$new中。(注意,此步骤使用作者和提交者的姓名和电子邮件,您可以提供-c user.name=...-c user.email=...参数。)

最后,也是最重要的,我们准备在某个地方记录这个新对象。这就是我们必须解决竞争条件的地方(每个对象写入步骤都有自己的锁定,以确保部分是适当原子化的)。

我假设您想将它们存储在某些分支名称下,并且这些分支名称可能被其他进程读取和更新。(如果它们是只读的,从未被任何其他东西更新,那么我们现在已经自由了。)我们有一个原子更新操作,以git update-ref的形式:

git update-ref [-m <reason>] <refname> <newvalue> <oldvalue>

可选的-m <reason>部分将存储在引用日志中,如果此引用有引用日志。(此步骤还使用user.nameuser.email,因此如果需要,请在此提供它们。)refname部分是引用的完整名称,例如分支branchrefs/heads/branchnewvalue部分是我们要存储的哈希ID,而oldvalue部分 - 我们将提供它来检查比赛 - 是我们期望该分支名称当前存储的值。
现在,假设我们正在与其他进程竞争,有两种可能情况:
  • 我们赢得了比赛:我们在开始时读取的树是与当前位于分支顶端的提交相对应的树。 因此,我们的提交已准备好以直线方式添加到分支中。
或者:
  • 我们输掉了比赛:我们在开始时读取的树是有效的,但是分支名称现在指向一个更新的提交。 因此,我们的提交没有价值,或者需要放在侧分支上,或者其他事情。 如果我们的提交真的没有价值,我们可以重新开始并重新执行整个过程:也许这次我们会赢得比赛。
如何处理“输掉比赛”情况取决于您。 但是现在我们看到了“魔法”的来处:当我们开始整个过程时,我们想要的提交ID是与引用相关联的当前提交哈希。 因此,“魔法”就是:
commit_id=$(git rev-parse $refname)

该命令读取引用的当前值(如果是分支名称,则可以假设底层对象的类型为commit)。

由于update-ref步骤具有自己的原子性(通过锁定强制执行),因此我们在这里获得了我们的原子性。然而,在失败情况下该如何处理却是困难的一部分。还要考虑并处理每个中间步骤的失败情况,例如,如果git rev-parse失败,或者任何git read-treegit write-treegit commit-tree都失败。


1不要向精灵寻求建议,因为他们会说“是”也会说“否”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接