2

I did by mistake more than 100 amend commits. How can i convert them to usual commits? Or at least to get git log with difference for each amend commit? I can see only difference for all amend commits at once if i run gitk or git log -p now. i can see all amend commits hashes but only with reflog. I can also see difference with git diff amend_hash1 amend_hash2 but not in gitk or git log -p. They jump over these amends although they are linked correctly in .git/logs/refs/heads/master and .git/logs/HEAD

i just ran git commit --amend 100 times, one time for each of 100 changes. Then I got one big commit for all 100 changes when i ran git commit without amend.

I found how to undo only one amend commit...

l0pan
  • 476
  • 7
  • 11
  • amended commits are "usual" commits. No real difference between a commit that was made as usual and another that was amended. Could you explain with a short example what you did and what you would like to do or get? – eftshift0 Sep 24 '18 at 19:12
  • These commits are displayed as one commit in gitk – l0pan Sep 24 '18 at 19:17
  • Amending 100 commits is unusual. Did you squash them? That's an entirely different matter. Could you show the exact commands you ran? – Schwern Sep 24 '18 at 19:17
  • Please add the exact commands you ran to "amend" the commits. – Schwern Sep 24 '18 at 19:24
  • @l0pan That will only amend one commit. What else did you run to make it amend 100? BTW this is better and safer done with [`git filter-branch`](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History). – Schwern Sep 24 '18 at 19:29
  • @l0pan When you say "these commits are displayed as one commit in gitk" what *exactly* do you mean? Could you provide a screenshot? – Schwern Sep 24 '18 at 19:32
  • "These commits are displayed as one commit in gitk." No, `gitk` is showing the *one* commit created by the last call to `git commit --amend`, which replaces the previous HEAD commit with a modified version. The other 100 or so orphan commits you created are either being held onto by the reflog or waiting to be garbage collected. – chepner Sep 24 '18 at 20:36
  • @I0pan I have faced a similar situation but I am using gerrit. Please have a look at https://stackoverflow.com/questions/59713349/amended-commits-revert-to-previous-commit/ – TechTotie Jan 14 '20 at 07:19

3 Answers3

3

I think this is more explainable with pictures, although my ASCII-art ability runs out after just a few --amends. The trick is to realize that what git commit --amend does, vs git commit without --amend, is to change the parent hash stored in the new commit.

A normal git commit freezes the index contents into a tree and makes the new commit using the new tree, you as the author and committer, your log message, and the current commit as the parent of the new commit:

...--F--G   <-- [you were here]
         \
          H   <-- branch (HEAD) [you are here now]

Then we make new commit I, after straightening out the drawing:

...--F--G--H--I   <-- branch (HEAD)

and new commit J:

...--F--G--H--I--J   <-- branch (HEAD)

and so on.

Using git commit --amend, however, we make the new commit H as usual except that the parent of H is F instead of G:

       G   [you were here]
      /
...--F--H   <-- branch (HEAD)

Then, making I, we make it as usual except that the parent of I is F instead of H:

       G   [you were here]
      /
...--F--H   [you were here too]
      \
       I   <-- branch (HEAD)

and so on.

If you imagine running git log right at this point—with commit I pointing back to commit F—you will see commit I, then commit F, then E, and so on. Commits G and H will be invisible to gitk or git log (but will show up in git reflog output, since that does not follow parent chains).

After 100 such operations, I've run out of commit letters and cannot possibly draw the crazy fan of commits all pointing back to F, but you can imagine them; or I can draw just 9 such commits, G through O:

     GH
     | I
     |/ J
...--F==K
     |\ L
     | M
     ON

Each of these various commits has the tree and the message that you want. What is wrong with each of these commits, except for G itself, is that it has the wrong parent: you'd like G to have F as its parent (which it does), but then you would like H to have G as its parent (which it does not).

This means you must copy the wrong commits. Let's start by copying H to H' that has G as its parent, but otherwise uses the same tree and message and other metadata as H:

      H'
     /
     GH
     | I
     |/ J
...--F==K
     |\ L
     | M
     ON

Now we need to copy I to a new commit I'. The parent of I' is not G but rather H':

      H'-I'
     /
     GH
     | I
     |/ J
...--F==K
     |\ L
     | M
     ON

We repeat for J to J', using I' as the parent for J', and so on until we have copied every "wrong" commit to a "right" one. Then we can set a branch name to point to the last such copied commit:

      H'-I'-J'-K'-L'-M'-N'-O'   <-- repaired
     /
     GH
     | I
     |/ J
...--F==K
     |\ L
     | M
     ON

Running git log while on repaired, or gitk --all, will now show commit N' leading back to M' leading back to L' and so on. Remember that git log (and gitk) follow the parent linkages backwards, without looking at the reflog at all.

If you're willing to let some of the metadata (author and committer name, email, and timestamp) be clobbered, it's easy to make each of these commits with a shell script loop using git commit-tree. if you want to preserve that metadata, it's harder: you need to set a series of Git environment variables before calling git commit-tree each time, setting:

  • GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE: for the author
  • GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, GIT_COMMITTER_DATE: similar but for the committer

The log message can be copied directly from the incorrect commits, using sed or similar to chop off everything up to and including the first blank line (note that this discards any i18n encoding data, and sed may behave badly with unterminated final text lines in commit messages, but these may be tolerable; if not, extract the relevant code from git filter-branch).

Use git reflog to obtain the hash IDs of all commits to be copied, in the correct order (oldest-to-copy first, last-to-copy = newest last). Place these in a file, one entry per line. Then the following untested shell script will probably suffice:

parent=$hash  # hash ID of commit G, onto which new chain will be built
while read tocopy; do
    tree=$(git rev-parse $tocopy^{tree})
    parent=$(git cat-file -p $tocopy | sed '1,/^$/d' |
        git commit-tree -p $parent -t $tree)
done < $file_of_commits
git branch repaired $parent

This creates a new branch name repaired to hold the newly built chain.

torek
  • 448,244
  • 59
  • 642
  • 775
  • hmm, then why amends commits are linked correctly in `.git/logs/refs/heads/master` and `.git/logs/HEAD` ? They don't have one parent there – l0pan Sep 24 '18 at 20:42
  • @l0pan: Reflog entries don't use parent linkages. Each reflog entry names one specific commit, and `git reflog` aka `git log -g` looks at (or in Git terms, "walks") the reflog entries *instead of* walking parent chains. – torek Sep 24 '18 at 20:44
1

These commits are displayed as one commit in gitk – l0pan

I suspect what's actually happened is you've squashed 100 commits together, possibly with git merge --squash. You're in luck, this is much easier to reverse than actually amending 100 commits (which would be very unusual). We need to find the original head of the branch.

If you're in luck, ORIG_HEAD is still set to the original head of the branch. Check with git log ORIG_HEAD.

Otherwise use the git reflog to find the tip of the branch before you did the squash merge. You're looking for something like this...

a83ad6b (master) HEAD@{1}: commit: Squashed commit of the following:
78c06ed HEAD@{2}: checkout: moving from feature to master

a83ad6b is the new, squashed commit. 78c06ed is the original, unsquashed tip of the branch.

Once you find the original commit, move your branch back to it.

git branch -f <commit id>

If you did really amend 100 commits, here's how you'd find them.

You'd use the git reflog to find the original unamended commits. You're looking for something like this...

6493a4c HEAD@{2}: commit (amend): The log message
a2a99ea HEAD@{3}: commit: The log message

a2a99ea is the original, unamended commit. 6493a4c is the new, amended commit.

Generally you should be able to grep for commit (amend) and use the previous reflog with the same commit message. However that is not guaranteed. For example, there could be checkouts and pulls between the commit and the amend.

0742a3a HEAD@{111}: commit (amend): chore: Add a simple monitoring process.
b71620d HEAD@{112}: pull: checkout e29402b1fda83664cf17782b1a34e2bb4a77d44f: returning to refs/heads/master
b71620d HEAD@{113}: pull: checkout e29402b1fda83664cf17782b1a34e2bb4a77d44f: chore: Add a simple monitoring process.
e29402b HEAD@{114}: pull: checkout e29402b1fda83664cf17782b1a34e2bb4a77d44f
fab188c HEAD@{115}: commit: chore: Add a simple monitoring process.

Or the amend could change the commit message.

6493a4c HEAD@{2}: commit (amend): A different log message
a2a99ea HEAD@{3}: commit: The log message
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Yes, i can see their hashes with reflog. But i want to see them in gitk or with 'git log -p' – l0pan Sep 24 '18 at 19:22
  • @l0pan Without knowing exactly what you did to amend 100 commits at a time, I can't tell you how to recover them. – Schwern Sep 24 '18 at 19:30
  • i just ran 'git commit --amend' 100 times for each of 100 changes. I didn't run merge. Then i ran commit without amend and got one big commit for all 100 changes – l0pan Sep 24 '18 at 19:40
  • @l0pan That last one should not have happened. Please edit your question to show ***exactly*** what commands you ran. Preferably copy & paste from your terminal. – Schwern Sep 24 '18 at 19:45
  • Are you sure your last `git commit` without `--amend` actually did *anything*? If there are no staged changes, then `git commit` is basically a no-op. – chepner Sep 24 '18 at 20:37
  • The last commit, like each earlier commit, uses the contents of the index (which presumably match the work-tree). The comparison of the last commit against the earlier commit—which is the same parent as for every other `git commit --amend`ed commit—will show every change made. – torek Sep 24 '18 at 20:46
0

They're all in the reflog of the repo that did the amending, and they're completely ordinary commits so you can e.g. git show them to see what changes they made and git cherry-pick them to nab those changes and so on. See the git reflog docs, and look in e.g. .git/logs/refs/heads/master to see what it's wrangling.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • Using `cherry-pick` will be painful; they should each have the right tree, so using `git commit-tree` should be painless. – torek Sep 24 '18 at 20:26
  • If they are ordinary commits then why `gitk` and `git log -p` ignore them? They jump over these amends although they are linked correctly in `.git/logs/refs/heads/master` – l0pan Sep 24 '18 at 20:28
  • Reflog is the history of what things referred to. If you want gitk and git log to show what you've had a ref to in the last .. I forget, month or so? whether or not it's still reachable by some current ref, add `--reflog` to either command. Try `git log --reflog --oneline --no-walk` for fun. – jthill Sep 24 '18 at 21:02