5

I was doing a refactoring, and just kept amending every time I made any decent progress. It has gone very well, and I realized it would be nice to be able to show people how I handled such a huge refactoring (lots of tiny steps, just nudging code around, with a green test suite at every point).

The commits are all in my reflog. Is there a way to turn each of those amended commits into its own real commit (sorry, don't know the terminology) so that users can see each step rather than just the aggregated ones on lines 81 and 57? I don't need unique messages for the commits or anything, just that they are captured in the history.

This is all still in my local repo.

Joshua Cheek
  • 30,436
  • 16
  • 74
  • 83

3 Answers3

3

Reconstructing the reflog might not be as difficult as it first appears. You can regenerate the reflog's commits into distinct branches, or you can use git read-tree to generate a single newbranch containing a portion of the reflog.

One commit per branch solution:

Suppose you wanted distinct branches for each commit. First, copy your repository. You're going to change your reflog in this process, so you might as well use a throwaway version. Second, examine your reflog and find the commit with which you want to start. It's going to look like HEAD@{n}. Suppose it were HEAD@{49}. Then, try this script, replacing the head -50 with head -<n + 1>, whatever that is in your case:

#!/bin/sh
reflog=$(git reflog | head -50 | awk '{ print $1 }')
i=0
for ref in $reflog; do
    git checkout -B "reflog_$i" $ref
    i=$(expr $i + 1)
done

This is grabbing your reflog's commit history once, then iterating over it, generating reflog_$i branches along the way. You can cherry-pick, merge, or manipulate them however you want. If this is simply for a presentation, you could write a short script that executes git checkout on the branches, runs the test suite, and shows greens the whole way. Remember, reflog_1 represents the most current history; reflog_<n+1> the oldest.

#!/bin/sh
for i in $(seq 50 1); do
    git checkout "reflog_$i"
    ./test-suite.sh
done

Throw it up on a projector while you explain your commit method and it'd be a nice backdrop.

If you want to combine all the branches, you can run this ruby script to apply them in order (or create an equivalent in whatever language you're comfortable with). Let me reiterate that you should have your directory backed up, because this is rather destructive.

#!/usr/bin/env ruby
n = 50

n.downto 0 do |i|
  system "
    git read-tree reflog_#{i}
    git commit -m 'Refactoring #{n - i}'
    git checkout -- .
    git br -D refog_#{i}
  "
end

Putting all commits on the same branch using git read-tree

First, copy your repository. Then use a script like the one below. Per the discussion in the first solution, change the two <n + 1> to whatever depth you want in your reflog, plus one. Note the additional pipe to sed. You must use it or something like it, lest the reflog apply to newbranch in reverse chronological order. There's surely a way to do this without using both awk and sed, but this works:

#!/bin/sh
reflog=$(git reflog | head -<n + 1> | awk '{ print $1 }' | sed -n '1!G;h;$p')
git checkout -B newbranch HEAD@{<n + 1>}
for ref in $reflog; do
    git read-tree "$ref"
    git commit --no-verify -m "Adding commit $ref"
    git checkout -- .
done

The final result will be newbranch, which should contain all the commits between HEAD@{0} and HEAD@{n}, based upon commit HEAD@{n+1}.

Christopher
  • 42,720
  • 11
  • 81
  • 99
  • Thanks! I had to do a bit more research, but eventually found `git read-tree` which was able to build on the initial answer. Amended your response with that second part that did it for me. – Joshua Cheek Jun 19 '12 at 13:01
  • Ohhh `git read-tree`. Very nice. I added another solution based on your ruby script's use of that command. It should build the branch from the outset, without the intermediary 'branch-per-commit' step. – Christopher Jun 19 '12 at 14:24
1

It's possible, but likely difficult.

Every time you did an --amend, you essentially "threw away" the previous commit and replaced it with the new amended commit, which contains the previous changes plus whatever was amended.

So:

A<--B (master)

now commit --amend:

  ---B
 /
A<--C (master)

now commit --amend again:

 --B
/
A<--D (master)
\
 --C

Commits B and C still exist. However, they're not pointed to by anything and will eventually be garbage collected.

So to get the graph you want:

git checkout master
git reset --hard A
git cherry-pick B
git cherry-pick C
git cherry-pick D

Leaves:

A<--B'<--C'<--D' (master)

(Note the prime (apostrophe) symbol - indicating they are new commits with new hashes)

Now, the one thing I'm unsure about is whether or not you'll have conflicts. I believe you will, since each of the later commits contain some of the same changes as the previous commits, given that they were amended. If so, you'll have to resolve those.

Keep in mind, given the way git works, if you try this and it doesn't work, getting back to where you are right now is a simple matter of:

git reset --hard D
wadesworld
  • 13,535
  • 14
  • 60
  • 93
  • Hi, this is a great response, thank you :) The `cherry-pick` did unfortunately hit conflicts at each step. I was able to use `git read-tree` to handle that part, though. – Joshua Cheek Jun 19 '12 at 13:02
0

You could make a new branch out of the first reflog entry, and then for each reflog entry, git checkout the tree to workarea and then commit it.

This way you shouldn't get any conflicts, because you are essentially only rewriting the workarea tree with a newer reflog tree.

user1338062
  • 11,939
  • 3
  • 73
  • 67