Home > OS >  Reorder git history and preserving content rather than commit/patch
Reorder git history and preserving content rather than commit/patch

Time:10-08

For example, with a git history like this

root-commit 1 @ day 1
  d1

commit 2 @ day 3
- d1
  d3

commit 3 @ day 2
- d3
  d2

Two commits from day 2 and day 3 were out of order, and I want to swap them. I would expect the result to be

root-commit 1 @ day 1
  d1

commit 2 @ day 2
- d1
  d2

commit 3 @ day 3
- d2
  d3

The history is linear without any branches or merging. I tried to use rebase but it seems to be operating the existing commits (patches), which I'm going to discard, for example, the - d1 d3 commit. Rebasing will fail to apply due to conflict and requires manual resolve, which is not feasible for me.

The way I could think of is to do git archive (or git diff with the root commit) for all commits, order them and create a completely new history. Is there any better way to do this?

CodePudding user response:

Rebase is the wrong tool, but Git doesn't come with the right tool for your job, because that job is ... not something Git was ever designed to do.

Still, Git can do it. It's just a matter of using the underlying components, rather than any of the top-level, user-facing ("porcelain") commands.

The way I could think of is to do git archive (or git diff with the root commit) for all commits, order them and create a completely new history. Is there any better way to do this?

Using git archive would be okay, but it's more work than needed. Using git diff against the root each time would be even more work than that, and less appropriate.

The simplest method is to use git commit-tree. This a low-level ("plumbing") command that needs three inputs:

  • It needs a list of parent hash IDs. You'll pick, as your parent commit (singular), the root commit the first time, then the last commit you just made for each subsequent commit.

  • It needs a (single) tree hash ID for the source tree to store in the commit. You'll pick, for each new commit you make, the source tree associated with that day's commit.

  • Last, it needs a commit message. You can feed it that day's commit's message.

You may also want to override the default settings for the committer-and-committer-date and author-and-author-date. You can do all four through six separate environment variables, GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE, and the three corresponding variables with COMMITTER in place of AUTHOR. Or, in tcsh/bash brace-expansion syntax, GIT_{AUTHOR,COMMITTER}_{NAME,EMAIL,DATE}. The place to get these values would be the original commits that you're transposing.

Overall, then, your script might look like this:

last=$root    # find and set the root commit hash ID

for commit in $(get-list-of-original-commits-to-copy-in-new-order); do
    set_author_and_committer $commit   # set-and-export GIT_{AUTHOR,COMMITTER}_*
    new=$(git log --no-walk --format=%B $commit |
          git commit-tree -p $last ${commit}^{tree})
    last=$new
done
git branch rewrite $last

(untested, and you'll need to write a few functions). The git log --no-walk --format=%B extracts the original commit message in the form git commit-tree needs, and git commit-tree reads the commit message from stdin by default, so there's no special work here. We just provide the (singular) parent hash ID via -p and the tree hash via the symbolic method of specifying the tree associated with the commit being copied (see the gitrevisions documentation).

The code to set and export the GIT_* variables can be found in the old git filter-branch script.

Note that this script wrecks any GPG signatures in the commits; handling them requires something much fancier.

  •  Tags:  
  • git
  • Related