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
。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
。如果您计划仅使用Git的"porcelain"命令,则很明显是“否”的,因为这些命令与一个(单个)工作树的概念一起使用,该工作树包含所有常规格式文件,以及一个索引(保存该工作树的当前状态并构建下一个提交)。有一个HEAD
文件,保存着当前分支名称的概念。您需要至少两个单独的porcelain命令,并按照以下顺序执行:
git add <path>
git commit <arguments>
更新单个工作树版本中<path>
文件的索引,然后使用该索引和当前的HEAD
进行提交。Git在进行提交时会锁定它更新的一些内容,但您需要将自己的锁定叠加在这些锁定之上,以便添加-提交序列看起来是原子的。
(即使您使用--work-tree
和/或--git-dir
参数重定向各个步骤的各个部分,这仍然是有效的:共享索引在“添加”和“提交”步骤之间必须保持稳定。)
另一方面,如果您愿意走出纯粹的porcelain舒适区,您可以将提交本身作为原子实体完成——但您仍然需要解决某种竞争问题,因此在答案真正从“否”变为“是”之前,您需要解决此问题。要了解其工作原理,我们必须将git add
和git 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-index
或git 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.name
和user.email
,因此如果需要,请在此提供它们。)refname部分是引用的完整名称,例如分支branch
的refs/heads/branch
。 newvalue部分是我们要存储的哈希ID,而oldvalue部分 - 我们将提供它来检查比赛 - 是我们期望该分支名称当前存储的值。commit_id=$(git rev-parse $refname)
该命令读取引用的当前值(如果是分支名称,则可以假设底层对象的类型为commit
)。
由于update-ref
步骤具有自己的原子性(通过锁定强制执行),因此我们在这里获得了我们的原子性。然而,在失败情况下该如何处理却是困难的一部分。还要考虑并处理每个中间步骤的失败情况,例如,如果git rev-parse
失败,或者任何git read-tree
、git write-tree
或git commit-tree
都失败。