I understand that this is a bad idea for lots of scenarios. I am learning Git and experimenting. No code will be harmed in this exercise.
I have created a structure like this:
* [cf0149e] (HEAD, branch_2) more editing
* [8fcc106] some edit
|
| * [59e643e] (branch_2b) branch 2b
| /
|/
| * [0f4c880] (branch_2_a) branch 2a
| /
|/
* [a74eb2a] checkout 1
* [9a8dd6a] added branch_2 line
|
|
| * [bb903de] (branch_3) branch 3
|/
|
| * [674e08c] (branch_1) commit 1
| * [7d3db01] added branch_1 line
|/
* [328454f] (0.0.0) test
Now I want to go through this graph and rename various commits so that they make sense.
For example:
* | [a74eb2a] checkout 1
* | [9a8dd6a] added branch_2 line
renamed to:
* | [a74eb2a] branch 2 commit 2
* | [9a8dd6a] branch 2 commit 1
Note that:
[cf0149e] (HEAD, branch_2) more editing
[59e643e] (branch_2b) branch 2b
[0f4c880] (branch_2_a) branch 2a
are all branched out of:
[a74eb2a] checkout 1
I've experimented with
git rebase -i 328454f
then changing "pick" to "edit" on the commits that I wanted to modify and subsequently running
git commit --amend -m "the new message"
as the rebase process continued.
The problem with this approach is that, after the last git rebase --continue
I end-up with two new commits (duplicates of the two I wanted to rename) on the branch I happened to be on. For example, if I ran the rebase while HEAD was at "branch_2" the graph might look something like this:
* [cf0149e] (HEAD, branch_2) more editing
* [8fcc106] some edit
* [3ff23f0] branch 2 commit 2
* [2f287a1] branch 2 commit 1
|
| * [59e643e] (branch_2b) branch 2b
| /
| /
| | * [0f4c880] (branch_2_a) branch 2a
| | /
| |/
| * [a74eb2a] checkout 1
| * [9a8dd6a] added branch_2 line
|/
|
| * [bb903de] (branch_3) branch 3
|/
|
| * [674e08c] (branch_1) commit 1
| * [7d3db01] added branch_1 line
|/
* [328454f] (0.0.0) test
In other words I now have two sets of commits that represent exactly the same code state.
All I wanted to do was change the commit messages.
I also want to rename the initial message from "test" to something like "Initial version". I can't seem to do it with git commit --amend -m "Initial version"
because I end-up in headless mode if I checkout that commit.
What am I doing wrong? Surely it can't be that hard.
EDIT:
Here's an approach I just tried that works.
Of course, it re-writes history. So, outside of very special cases it is a bad idea.
Here are the steps:
Checkout the branch to be modified. Create patch files:
git format-patch HEAD~x // Where x is how far back from HEAD you need to patch
Edit the patch files to change the commit message. Now reset the head.
git reset --hard HEAD~x // Same x as before
Apply the patches:
git am 000*
New commits will be created with new SHA1's.
If any branches now need to reference the new commits with the corrected messages you have to use git rebase
to move them over.
To use my own example, after applying the patch procedure I ended-up with this:
* [7761415] (HEAD, branch_2) branch 2 commit 4
* [286e1b5] branch 2 commit 3
* [53d638c] branch 2 commit 2
* [52f82f7] branch 2 commit 1
| * [bb903de] (branch_3) branch 3
|/
| * [59e643e] (branch_2b) branch 2b
| | * [0f4c880] (branch_2_a) branch 2a
| |/
| * [a74eb2a] checkout 1
| * [9a8dd6a] added branch_2 line
|/
| * [674e08c] (branch_1) commit 1
| * [7d3db01] added branch_1 line
|/
* [328454f] (0.0.0) test
So, I have my branch_2 commits nicely labeled.
Now I want to move branch_2a so that it branches out of [53d638c] branch 2 commit 2
Checkout branch_2a
git checkout branch_2a
Rebase it
git rebase 53d638c
Now I have:
* [fb4d1c5] (HEAD, branch_2a) branch 2a
| * [7761415] (branch_2) branch 2 commit 4
| * [286e1b5] branch 2 commit 3
|/
* [53d638c] branch 2 commit 2
* [52f82f7] branch 2 commit 1
| * [bb903de] (branch_3) branch 3
|/
| * [59e643e] (branch_2b) branch 2b
| * [a74eb2a] checkout 1
| * [9a8dd6a] added branch_2 line
|/
| * [674e08c] (branch_1) commit 1
| * [7d3db01] added branch_1 line
|/
* [328454f] (0.0.0) test
Same procedure with branch_2b results in this:
* [ca9ff6c] (HEAD, branch_2b) branch 2b
| * [fb4d1c5] (branch_2a) branch 2a
|/
| * [7761415] (branch_2) branch 2 commit 4
| * [286e1b5] branch 2 commit 3
|/
* [53d638c] branch 2 commit 2
* [52f82f7] branch 2 commit 1
| * [bb903de] (branch_3) branch 3
|/
| * [674e08c] (branch_1) commit 1
| * [7d3db01] added branch_1 line
|/
* [328454f] (0.0.0) test
Which is exactly what I was looking for. Not too messy. Again, not something you'd want to do outside of very special cases. In my case I am just playing to learn Git, so the above isn't really affecting a real code repository. It's nice to know that you can do this if you have to.
Now on to renaming the very first commit.
There are many ways to rewrite history with git. Use git commit --amend to change your latest log message. Use git commit --amend to make modifications to the most recent commit. Use git rebase to combine commits and modify history of a branch.
You can use the git commit –amend command to edit a commit message. To do so, use the -m flag and specify a new commit message in quotation marks. This command will replace the single commit log message in your last commit with the one that you state.
You can modify the most recent commit in the same branch by running git commit --amend. This command is convenient for adding new or updated files to the previous commit. It is also a simple way to edit or add comments to the previous commit. Use git commit --amend to modify the most recent commit.
You can't actually ever change a commit. If you make the tiniest single-bit change anywhere, from the spelling of the name in the email line to the exact second of the commit timestamp, you get a new, different commit with a new, different SHA1 (the SHA1 is the "true name", as it were, of each "object" in the git database, with commits being one of the four types of object).
One of the immutable pieces of a commit is its "parent commits", which build up the chain backwards from most-recent-commit to oldest-commit.
Hence, what git rebase -i
does is make a new commit-chain, with each commit in the chain having the same contents/effects as the original commit plus or minus any changes you make during the interaction. When it is all done it removes the label (the sticky-note, as it were) from the end of the old commit-chain and paste it on the end of the new commit-chain. It starts by making a copy of the oldest commit to be modified/rebased. This one has the rebased parent (which can be the same parent commit as in the original chain, or a different parent: either way it is OK because it is a new commit). Then it makes a copy of the next one in the old chain, but pointing to the new chain. It repeats all the way to the end of the old chain.
This is why all your other branches are now independent of your rebased branch. They have to be, because they use the old commit IDs. If you want them to branch off your new rebased branch, you must go into each of those and rebase them.
There is a powerful Swiss-Army-chainsaw-ish command, git filter-branch
, that you can use to do a very large series of "redo lots of commits, making all new commits that have (mostly) the same contents as the originals", sort of git rebase
on steroids as it were, that you can use for this purpose. (Run it with --all
to affect all branches.) Of course, since it does in fact redo all the commits, you wind up with a repo that is basically unrelated to the original repo.
It's tough to rewrite the initial commit (not impossible) because it has no parent, so regular rebase
just won't do it.1 (filter-branch
can.) Because these change SHA1 IDs, and anyone who has cloned your repo is using / depending-on those, it's normally a feature that you can't go fiddling with commits like this. When you know nobody else is depending on some particular set of commits, then you can rebase
all you like, but you won't be going back to the initial commit because that will be in the shared parts of the repo. It's pretty rare (though of course not "never") that the entire thing all the way back to "initial commit" is all your own private stuff.
1Since I wrote this, git rebase
has learned to copy a root commit like the initial commit.2 Using conventional git rebase
syntax, you would have to name the parent commit of the root, and of course there is no parent (that's what makes it a "root" in the graph). So rebase
uses --root
as an argument to cover this case.
2It's possible to have multiple roots; use git checkout --orphan
, for instance, followed by git commit
, to create a new root commit. It's a bit unusual to have these, although the git source itself is kept in a git repo with multiple roots.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With