Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
975 views
in Technique[技术] by (71.8m points)

git - Is there a way to merge with Strategy "ours" without producing a new commit?

What I need to do is close a branch and make the tip (last commit) of another branch looking like a merge with that branch without actually change its content. I tried

git merge -s ours other_branch --squash

but nothing happened (which made sense after I read what squash actually does)

I.E. before command

  * other_branch
 / 
*---* HEAD

expected result after command

  * other_branch
 / 
*---* HEAD

Note: by content here I mean committed stuff: metadata would change as the operation I want to achieve is actually adding one more parent to the commit; I'm aware this is changing history and would affect at least the shasum.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

PetSerAl's comment has the (or "a", at least) command you can use to achieve what you want. The only thing missing is an explanation as to why:

git reset --soft $(git log --format=%B -n 1 | 
    git commit-tree HEAD^{tree} -p HEAD^ -p other_branch)

works. I'll get to that in a bit, but first...

A literal answer

The literal answer to your question as asked, though, is "no". Since subject lines can be edited away, let me quote it:

Is there a way to merge with Strategy “ours” without producing a new commit?

The word merge, in Git, can refer to two things: the act of merging, or the result of a previous act-of-merging. The first of these is a verb, to merge; the second is an adjective, a merge commit, or even a noun, a merge. A merge commit is a commit with two parents (or more, but we only really care about two).

Running git merge --squash tells Git to perform the merge—i.e., do the verb part—but at the end, when making a new commit, make an ordinary, non-merge commit, i.e., suppress the merge-as-adjective effect.1 The point of this is that when you have a series of commits, e.g., a "branch" in the sense that's not just "a name pointing to a specific commit" (see What exactly do we mean by "branch"?)

Running git merge -s ours tells Git to perform a faked-up act of merging—that is, prentend to do merge-as-a-verb, but not really do anything—resulting in a merge-as-an-adjective/noun commit. Since the verb form is gone, the only thing remaining is the after-effect. This is why using --squash is useless here: you propose to eliminate both the verb and noun, and there's nothing left.


1For no particularly good reason, git merge --squash has the side effect of setting the --no-commit argument as well, so that you have to manually run git commit at the end. Note that a real merge (that makes a merge commit) can also be run with --no-commit, making you run git commit manually as well; or it can stop due to conflicts, which you must resolve, making you finish the commit manually. The latest versions of Git have added git merge --continue to make this feel less awkward, but it just runs git commit.


Git has --amend but it doesn't quite get us there

The diagram shows what you want—which, if Git supported it, might be spelled git commit --amend --add-parent, for instance—but doesn't produce the key insight, and in fact git commit --amend is a small, or perhaps huge, lie, as it doesn't change a commit, it just makes a new commit that uses an unusual parent hash.

The normal git commit process executes a bunch of steps as if they were done all at once, so that either they all finish correctly, or nothing happens (or seems to, at least). The steps are:

  1. Obtain the current commit's hash ID (git rev-parse HEAD). Call this P: the existing HEAD will be the new commit's parent. (If you're concluding a merge, MERGE_HEAD will also exist and git commit reads it to get additional parents. That's just for finishing merges, though.)
  2. Write the current index to make a tree object (git write-tree). Call this T, the tree hash ID. (This may or may not be the same tree as some previous commit.)
  3. Obtain your name and email address and the current time as committer. Normally, use these as author too (you can override them).
  4. Obtain a commit message.
  5. Write out a new commit with all of this information (tree T, parent P, author and committer as obtained in step 3, and commit message obtained in step 4. The result is a new commit hash C.
  6. Write the new hash C into the current branch name, so that git rev-parse HEAD now produces C.

Using git commit --amend changes the procedure right at step 1: instead of getting HEAD as the parent commit, Git reads the current commit's parent hashes (there may be more than one, if you're --amend-ing a merge), and uses those in step 5.

The effect is to shove the current commit aside:

...--o--o--*   <-- master (HEAD)

becomes:

          *   [the commit that was HEAD before]
         /
...--o--o--@   <-- master (HEAD)

What you want to get Git to do is a bit different.

Why (and how) the shell command works

Git's commit-tree command produces new commit objects. It's like step 5 of the six-step commit sequence above. But it hasn't made a tree, and it does not have precomputed parent commit hashes ready to go, so it takes those as command line arguments:

git commit-tree tree-hash -p parent-hash-1 -p parent-hash-2

in this case. The tree-hash we want is, like git merge -s ours, the same tree that the current commit has. We can name that tree using HEAD^{tree}, which is described in the gitrevisions documentation. The two parent hashes we want start with the parent of the current commit. (We can assume there's only one such parent.) Again, gitrevisions syntax gives us a way to write this: we can use parent^1, or parent~1, or leave out the 1 from either of those expressions. The other parent hash we want is the commit to which other_branch points, so we can just name that. That gives us:

git commit-tree HEAD^{tree} -p HEAD^ -p other_branch

This command reads the commit message from its standard input. If we want to retain the commit message from the current commit, we can extract it with git log: --format=%B tells git log to show each commit by printing its subject-and-body as text, and -n 1 tells git log to show only one commit. The first commit that git log shows is, by default, the HEAD commit. So this gives us the:

git log --format=%B -n 1 |

part—we pipe this git log's standard output to git commit-tree's standard input.

What git commit-tree does after making the commit is to print its hash ID to its own standard output. Hence if we just ran this pipeline by itself, we'd see the new commit hash printed, but we would not store it anywhere. What we need to do is change the current branch name—whatever that is—to point to the new commit; and git reset --soft commit-hash will do that, hence:

git reset --soft $(...)

The $(...) construct is the last bit: the shell takes this to mean run the given command, capturing its standard output, then treat that standard output text as command arguments to git reset --soft. Since there's only one output-word—the new commit's hash—this runs git reset --soft on the new commit ID.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...