user80551
user80551

Reputation: 1164

How do I reword a commit (message) that is the parent of two branches?

Suppose I have a repo like this

$ git log --oneline --graph 
*   3e0a28f Merge branch 'other_branch'
|\  
| * d4fd67a Add something else
| *   d0f16bf Merge branch 'master' into other_branch
| |\  
| |/  
|/|   
* | 3684fe5 Make a change in master
| * 45b3ecb Make a change
|/  
* b2a9034 Added some text, reword me
* 7b1ac57 Initial commit

d0f16bf involves fixing some merge conflicts. (3684fe5 and 45b3ecb modify the same line). I want to reword b2a9034

$ git rebase -i b2a9034^
[detached HEAD a9bd978] Added some text, REWORDED
 1 file changed, 2 insertions(+)
error: could not apply 3684fe5... Make a change in master

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply 3684fe570517de37e1ce7661e3821372e1eee967... Make a change in master

Is there a way to reword b2a9034 without fixing the merge conflicts again?

Upvotes: 2

Views: 726

Answers (2)

torek
torek

Reputation: 488093

When you say "reword" I assume you mean "change the commit message, but not the committed files" (the git rebase -i meaning).

Git's "Swiss-Army Chainsaw" command, git filter-branch, has an option just for this. (Of course, filter-branch is difficult to use, hence the "Swiss-Army chainsaw" appellation.)

Specifically, filter-branch has --msg-filter, which allows copying commits while making changes to the message:

This is the filter for rewriting the commit messages. The argument is evaluated in the shell with the original commit message on standard input; its standard output is used as the new commit message.

What this means is that you can keep the existing message for commits that are not the rewrite target, and edit or replace the one that is the target, using a suitable --msg-filter, such as:

git filter-branch ... \
    --msg-filter 'if [ $GIT_COMMIT == b2a9034... ]; then \
        cat $HOME/new_msg; else cat; fi' \
    ...

This filter obviously copies the message as-is (cat) unless it's the one targeted commit, in which case it ignores the stdin message and prints instead the contents of your prepared-in-advance $HOME/new_msg file. You can of course write any valid shell script instead, just be sure to preserve the exact original message of other commits.

You will also need to fill in the full commit ID here (or use prefix matching but it's probably wiser to fill in the full ID). To get the full ID from a partial one, the easiest method is to use git rev-parse:

$ git rev-parse b2a9034
b2a9034...                  <-- full 40 char SHA-1 comes out
$ 

You will also need to fill in the rest of the ... parts for git filter-branch, which is nontrivial.

Since filter-branch is slow, you probably want to limit it to a smallish number of commits. You can do this with the git rev-list arguments: filter-branch will pass them on to git rev-list and will copy only those commits thus listed. Thus, you can test this all out first:

$ git rev-list ^b2a9034^ branch1 branch2

Here the two branches are the names of the branches whose branch-tips you want rewritten (probably one of them is master, based on your text above). The first argument, ^b2a9034^, should cause git rev-list to omit the parent commit of b2a9034 and all earlier commits. (The first ^ character is the "not" operator for git rev-list and the second is the parent-following operator of gitrevisions. This can be a bit confusing so an alternative spelling is ^b2a9034~1, which has exactly the same meaning, but doesn't use ^ in two different ways. I'm not sure how much less confusing it is in the end, though.)

(If your repository has few commits, this rev-limiting is not so important.)

Finally, note that, as the filter-branch documentation says:

The command will only rewrite the positive refs mentioned in the command line (e.g. if you pass a..b, only b will be rewritten). ...

What this phrase means was not obvious to me until I properly understood git's internals, and from there, how filter-branch does what it does. Internally, git only ever adds things, so git filter-branch simply copies existing commits to new ones. If the new commit is exactly the same as the original commit, so that the copy is bit-for-bit identical to the original, you get the original SHA-1 for the copy, meaning nothing is added and nothing changes. If you change anything, though, you get a new, different SHA-1 ID.

This means that as filter branch runs along copying commits, it "copies" any commits prior to the one being modified and gets the original SHA-1 again. Then it hits the first (and maybe only) one you want changed, and gets a new SHA-1. The original commit remains in the repository but now there's a new copy with a new SHA-1.

Once that's happened, all the subsequent commits that filter-branch copies have at least one change made, even if none of your filters change them. In particular, they have as (at least one of) their parent ID(s), a new ID. The first new child commit has the new parent ID so that it gets a new ID too; its child commit then has to pick up that new ID; and so on.

The end result is that the new copies of commits give you a new chain of commit-IDs ending with a new commit ID for the (in this case two) branch(es) you're filtering. Git must then save those IDs as new branch-tips. When the documentation says "only b will be rewritten", it means that filter-branch will update refs/heads/b—the file that holds the ID of the tip of branch b—to have the final SHA-1 of the copied branch-tip.

Thus, by listing, say, ^b2a9034^ master develop, you've provided two "positive refs", namely refs/heads/master and refs/heads/develop, and those are the two that filter-branch will update. The ^b2a9034^ is a "negative ref" ("exclude b2a9034^ and earlier) and hence filter-branch does nothing with it after passing it to git rev-list.

Upvotes: 4

Pankaj Singhal
Pankaj Singhal

Reputation: 16053

You could use git rerere feature.

You have to enable it using git config --global rerere.enabled 1, after that, every conflict you resolve get stored for later use and the resolution is reapplied in the same contexts. However, it will not be applied if you try it now because it has not yet recorded your conflict merge. Try to resolve the conflicts once after setting rerere & it'll take from there on.

You can check the stored resolutions with git rerere diff.

Take a look at this tutorial for more information on git rerere.


A word of Caution

As J. C. Hamano mentions in his article "Fun with rerere"

  • Rerere remembers how you chose to resolve the conflicted regions;
  • Rerere also remembers how you touched up outside the conflicted regions to adjust to semantic changes;
  • Rerere can reuse previous resolution even though you were merging two branches with different contents than the one you resolved earlier.

Even people who have been using rerere for a long time often fail to notice the last point.

So if you activate rerere on too broad a content, you might end up with surprising or confusing merge resolution because of the last point.

Forgetting Incorrect Merge

If you do a merge incorrectly, then discard it, then do the "same" merge again, it will be incorrect again. You can forget a recorded resolution, though. From the documentation:

git rerere forget <pathspec>

This resets the conflict resolutions which rerere has recorded for the current conflict in <pathspec>.

Be careful to use it on specific paths; you don't want to blow away all of your recorded resolutions everywhere. (forget with no arguments has been deprecated to save you from doing this, unless you type git rerere forget . to explicitly request it.)

But if you don't think to do that, you could easily end up putting that incorrect merge into your history..

Upvotes: 0

Related Questions