我发现经常在处理大型功能分支时,对真正属于自己分支的部分代码库进行更改。我知道我可以使用git add -p
进行我想要的更改,提交它们,隐藏我不想要的更改,从master上创建一个新分支,挑选我之前所做的提交,然后切换回原始分支,重置,合并到功能分支中,然后弹出我的更改,但这似乎应该做很多工作,而且应该更容易做到。应该有一种方法可以做到这一点而又不影响我的工作目录,对吧?
这是我正在尝试做的事情。
我很想拥有这样的命令
$ git commit --onto master --as new
这将在new
上创建一个master
分支,在那里提交更改,然后将其合并到我的HEAD分支中,而无需触摸我的工作目录。这样的命令存在吗?
答案 0 :(得分:1)
没有任何这样的命令,但是您可以构建自己的命令。这将有些棘手(或可能很多)。对于一般情况,您将需要一个临时工作树,在这种情况下,您将必须停止并让用户解决合并冲突。但是,如果您只是想超出范围地声明此特定的通用案例,并且仅处理无合并冲突的案例,则可以避免使用单独的工作树,如下所示。
(我将其作为一种学术练习来编写,以说明Git的工作方式以及如何将其各个部分插入到一起以编写 new Git命令(主要是作为外壳脚本)。)< / p>
请记住,当您运行git commit
时,Git会根据您的 index 中的内容构建一个新的提交。索引是不可见的类似缓存的数据结构,它占用HEAD
(或当前)提交和工作树之间的空间。
在您的图形中,标记为 staged 的虚线圆是您在git commit
将索引变成一棵树然后包裹该树之后,将要进行的提交具有新提交对象的对象。此过程中不使用您的工作树(一个例外是某人是否运行过git commit --only
或git commit --include
,它们构建了新的临时索引文件,然后内部使用了git add
可以从工作树复制到新的临时索引中,但是在这里避免这个特殊的棘手问题。
通常,您不需要了解所有这些内容:git commit
命令会处理所有这一切。实际上,您可以使用该命令,除了您不想要进行普通提交外,您还想要合并提交。因此,我们将需要手工做一些事情,和/或走更长的路线。让我们从观看 git commit
进行新的提交开始,如果我们现在只运行git commit
。
请注意,每个提交都包含所有文件的完整快照。标为 staged 的虚线圆将变为真正的提交,该提交还将自动更新dev
,其数量相当于以下过程。为了清楚起见,所有错误检查均已省略。我在这里假定该日志消息在shell变量中可用,尽管使用-F file
也可以从文件中获取日志消息。在查看了此处的四个命令后,我们将对其进行细分,但也请参见每个命令的单独手册页:
current_branch=$(git symbolic-ref HEAD) # will fail if HEAD is detached
tree=$(git write-tree) # will fail if, e.g., index is unmerged
commit=$(git commit-tree -p HEAD -m "$message" $tree) # can fail
git update-ref -m "commit: $subject" $current_branch $commit # can fail
git symbolic-ref
命令从HEAD
读取当前分支的名称。大多数Git操作都从HEAD
获取当前 commit 的哈希ID ,但是我们需要一个名称-在这种情况下,refs/heads/dev
与您一样在您的dev
分支上–最后一步。
write-tree
将索引打包为树对象。从本质上讲,这将永久冻结当前索引中文件的内容,其格式为现在拥有的格式。生成的顶级树对象适用于新提交。
commit-tree
创建使用此冻结树的提交对象。它需要知道新提交的 parent 是什么;这就是HEAD
通过-p HEAD
提供的哈希ID。它需要日志消息;这就是-m
(或-F
)参数的作用。并且,它需要进入提交的 tree 对象的哈希ID。这就是$tree
的目的。
(提交由git commit-tree
刚刚编写的提交对象本身,git write-tree
编写的树对象以及已经在索引中的所有blob对象以及将所有子树链接在一起所需的任何子树,git write-tree
是在编写顶级树时写的。)
此进行提交,但是当前分支refs/heads/dev
仍将 old 当前提交命名为是在我们进行此新提交之前是最新的。现在,我们必须解决此问题,以便尽管HEAD
本身仍仅引用refs/heads/dev
,而refs/heads/dev
本身仍引用 new 提交。这导致新的提交成为当前的提交!执行此操作的Git命令是我们四个命令git update-ref
中的最后一个。 -m
参数提供了进入我们的参考日志的消息。常规git commit
命令使用字符串commit:
作为日志消息,后跟完整日志消息的主题(第一行,或多或少),因此我们将其放入shell变量{{ 1}},并在这里使用。它还需要知道新的哈希ID,以将其填充到引用名称中,这当然是我们刚从$subject
进行的新提交$commit
。
这就是git commit-tree
现在为您所做的事情:它将在分支git commit
上进行普通的单亲提交,从而更新分支名称{{1 }}指向新创建的提交。新的提交将通过其树对象始终冻结当前索引中所有文件的内容。不幸的是,这不是您想要的。您想要的是让Git进行新的提交,其类型不是普通的单亲提交,而是类型为 merge的提交。 :有两个父母的承诺。此合并提交的第一父级应该照常是当前(dev
)提交,但是此提交的第二父级应该是 new 的提交是...很好,这就是棘手的地方。
要获得所需的结果(图形显示在图的右侧),我们需要首先进行标记为dev
的新提交。
为了进行提交,我们必须将所有文件的外观快照构建到索引中。请注意,我在这里说的是 索引,而不是 the 索引。我们开始了解一些并发症! (HEAD
和new
也在做这种事情。)
因为Git是基于快照而不是变更集构建的,所以我们必须首先将 current 索引转换为变更集。也就是说,我们必须将当前提交与索引进行比较,以查看我们正在此处更改的文件以及我们对其执行的操作:
git commit --only
这里的输出(大部分)与git commit --include
的输出相同,但是它使用的是 plumbing命令,而不是瓷器(用户可配置) git diff-index --cached -p HEAD
前端。这样可以确保输出格式美观,稳定,易于理解,可供其他程序(包括其他Git命令)使用。
请注意,这种差异会将git diff --cached
中的树与由索引/暂存区表示的树进行比较。它完全忽略了工作树中的树。这就是我们想要的,因为git diff
会提交:索引中的内容。与HEAD
中的冻结树相比,我们现在希望索引中的内容以补丁的形式出现。
此修补程序现在适合应用于分支git commit
尖端(图片两边标记为HEAD
的圆形实心圆)中的提交中的树。
在普通的Git用法中,我们将此补丁应用于这棵树的方法是将树(与master
的尖端相关联的树)提取到工作树。但这是您不需要想要的。而且,除非应用此修补程序时存在无法解决的冲突,否则我们根本不打算使成为临时工作树。
还是,让我们先探讨一下。
在这里,我们可以使用master
,自Git 2.5起可用。由于存在一个非常讨厌的小错误,因此除非您拥有最新的Git,否则最好将它们保留两周以上,但是我们这里的计划可能是将其使用不超过几个秒,这样就可以了。该错误已在Git 2.15中修复。
添加的工作树带有其自己的master
和其索引。它还提供了我们进行完整git worktree add
所需的所有空间,并允许合并冲突。所以我们可以:
HEAD
创建一个名为git apply -3
的新分支,指向与path=$(mktemp -d)
git worktree add -b new $path master
相同的提交,并将添加的工作树存储在new
(这是一个新的临时目录)中。
已经在其私有工作树中创建了这个新分支,现在我们只需要应用刚刚提取的补丁:
master
$path
命令将应用补丁。 # this bit of clumsiness is due to the subshell problem
# (there are multiple workarounds, this one is simple)
status_file=$(mktemp)
echo fail > $status_file
git diff-index --cached -p --full-index HEAD |
(cd $path
if git apply -3; then
git commit -m "$message" && echo success > $status_file
fi
)
read status < $status_file; rm $status_file
case $status in
success)
new_commit=$(cd $path && git rev-parse HEAD)
git worktree remove $path
... finish up the job (see below) ...
;;
fail)
echo "oops, sorry, things went wrong"
echo "the mess is left in $path"
echo "you will need to finish the merge and finish the job"
;;
esac
标志指示它在必要时使用三向合并。我还向git apply
操作中添加了-3
,以便我们在补丁程序中获得完整的哈希ID,这使--full-index
的工作更加轻松,尽管从技术上讲,在现代Git中这不是必需的(这确保了索引行已足够-对于旧版本的Git,在大型存储库中需要git diff
。
请注意,我们可以在此处使用git apply
,而不是--full-index
。从技术上讲,这实际上是更好的选择,因为它可以处理diff-and-apply技术无法处理的某些文件重命名情况。但是我们正在考虑不添加工作树就这样做,而当我们这样做时,将无法使用git cherry-pick
。
我们现在可以做的是开始使用特殊的环境变量git diff... | git apply
来指导Git使用临时索引。这里有一些特殊之处:git cherry-pick
中的任何路径,Git要求文件不存在或具有有效索引的形式。所以我们可以这样做:
GIT_INDEX_FILE
这将创建一个具有唯一名称的临时文件,然后将其删除。现在$GIT_INDEX_FILE
适合用作tf=$(mktemp)
rm $tf
,因为它命名了不存在的文件。
我们也可以将临时文件放在$tf
目录中:
GIT_INDEX_FILE
但是我认为在这里是不必要的。
或者,我们可以借用.git
使用的方法:
tf=$(TMPDIR=$(git rev-parse --git-dir) mktemp)
但是将git stash
替换为我们自己的脚本的名称,无论如何—我在下面使用的是TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$
,而不是stash
。请注意,$tf
本身在Git 2.13中是新的,因此,如果您的Git较旧,请不要使用此方法。
现在我们有了一个临时索引,我们可以指示各种Git命令使用该而不是常规索引。
要构建我们的新提交,我们必须:
TMPindex
的顶端提取树到该索引中忽略故障情况,我们现在需要[编辑:根据注释,我在下面分别放置了git rev-parse --git-path index
和master
; --full-index
模式不能进行三向合并]:
-3
--cached
命令从给定的提交(在本例中为GIT_INDEX_FILE=$tf git read-tree refs/heads/master
git diff-index --cached -p HEAD |
GIT_INDEX_FILE=$tf git apply --cached
tree=(GIT_INDEX_FILE=$tf git write-tree)
new_commit=$(git commit-tree -p refs/heads/master -m "$message_for_new" $tree)
git update-ref -m "$subect_for_new" refs/heads/new $new_commit
的尖端)中提取树到索引文件中,我们将其重定向到临时索引。
我们已经看到了read-tree
命令。它使用真实索引。
这次master
命令添加了diff-index
,因此它将 only 更改应用于索引,如果需要,进行三向合并。我们为此使用临时索引。 (我们失去了进行适当的三向合并的能力,因此比以前有更多的失败可能性!)
apply
命令将临时索引写入一棵树,该树现已准备好提交,而--cached
命令将该树变为提交。我们之前已经看到了所有这些内容–这次的区别是新提交的父级是分支write-tree
(commit-tree
)的尖端,当然,我们还有一条不同的提交消息。 master
创建或更新名为refs/heads/master
的分支-粗暴地丢失任何先前名为update-ref
的分支,因此谨慎行事可能是明智的,或根本不理会分支 name (即完全放弃new
步骤)。
现在我们有了新的提交,我们在变量new
中拥有其哈希ID,我们准备回到原来的四命令序列,该序列在git update-ref
上创建新的提交然后更新$new_commit
。要将此新提交创建为 merge提交,而不是普通提交,我们只需给它两个父母即可。
因此,再次忽略所有错误处理,命令顺序为:
dev
这整个都是未经测试的,有些危险的,没有错误处理的脚本:
dev