git-将暂存的更改提交到另一个分支并合并

时间:2018-12-20 11:14:39

标签: git branch commit git-commit branching-and-merging

我发现经常在处理大型功能分支时,对真正属于自己分支的部分代码库进行更改。我知道我可以使用git add -p进行我想要的更改,提交它们,隐藏我不想要的更改,从master上创建一个新分支,挑选我之前所做的提交,然后切换回原始分支,重置,合并到功能分支中,然后弹出我的更改,但这似乎应该做很多工作,而且应该更容易做到。应该有一种方法可以做到这一点而又不影响我的工作目录,对吧?

这是我正在尝试做的事情。

A crude drawing depicting branches

我很想拥有这样的命令

$ git commit --onto master --as new

这将在new上创建一个master分支,在那里提交更改,然后将其合并到我的HEAD分支中,而无需触摸我的工作目录。这样的命令存在吗?

1 个答案:

答案 0 :(得分:1)

没有任何这样的命令,但是您可以构建自己的命令。这将有些棘手(或可能很多)。对于一般情况,您将需要一个临时工作树,在这种情况下,您将必须停止并让用户解决合并冲突。但是,如果您只是想超出范围地声明此特定的通用案例,并且仅处理无合并冲突的案例,则可以避免使用单独的工作树,如下所示。

警告:这是(a)长,(b)全部未经测试

(我将其作为一种学术练习来编写,以说明Git的工作方式以及如何将其各个部分插入到一起以编写 new Git命令(主要是作为外壳脚本)。)< / p>

请记住,当您运行git commit时,Git会根据您的 index 中的内容构建一个新的提交。索引是不可见的类似缓存的数据结构,它占用HEAD(或当前)提交和工作树之间的空间。

在您的图形中,标记为 staged 的虚线圆是您在git commit将索引变成一棵树然后包裹该树之后,将要进行的提交具有新提交对象的对象。此过程中不使用您的工作树(一个例外是某人是否运行过git commit --onlygit 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 的提交是...很好,这就是棘手的地方。

要进行新的合并提交,必须首先进行新的 other 提交

要获得所需的结果(图形显示在图的侧),我们需要首先进行标记为dev的新提交。

为了进行提交,我们必须将所有文件的外观快照构建到索引中。请注意,我在这里说的是 索引,而不是 the 索引。我们开始了解一些并发症! (HEADnew也在做这种事情。)

因为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命令使用该而不是常规索引。

要构建我们的新提交,我们必须:

  1. TMPindex的顶端提取树到该索引中
  2. 将补丁应用于该索引,而无需触摸任何工作树。这可能会失败!
  3. 使用该索引创建一个新的提交,就像我们使用常规索引创建一个新的提交一样,说明普通提交会发生什么。完成所有这些操作后,我们将准备创建 merge 提交。

忽略故障情况,我们现在需要[编辑:根据注释,我在下面分别放置了git rev-parse --git-path indexmaster--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-treecommit-tree)的尖端,当然,我们还有一条不同的提交消息。 master创建或更新名为refs/heads/master的分支-粗暴地丢失任何先前名为update-ref的分支,因此谨慎行事可能是明智的,或根本不理会分支 name (即完全放弃new步骤)。

进行合并提交

现在我们有了新的提交,我们在变量new中拥有其哈希ID,我们准备回到原来的四命令序列,该序列在git update-ref上创建新的提交然后更新$new_commit。要将此新提交创建为 merge提交,而不是普通提交,我们只需给它两个父母即可。

因此,再次忽略所有错误处理,命令顺序为:

dev

将它们放在一起

这整个都是未经测试的,有些危险的,没有错误处理的脚本:

dev