Skip to content

Instantly share code, notes, and snippets.

@gngdb

gngdb/README.md

Last active May 25, 2021
Embed
What would you like to do?

This gist shows how to use rebase to change the Git history of a repo. Imagine you realise that the first commit to your repo should be different (maybe it's incomplete, includes something it shouldn't or simply could be cleaner). However, since then you and others have made many changes to the repo that you want to preserve. Here we show how to split the first commit into multiple and re-attach the remaining version history to these new commits.

Let's make a dummy git repo rebase-root and add two files to it. We commit them with the commit #1.

> mkdir rebase-root
> cd rebase-root/
> git init
Initialised empty Git repository in /h/username/repos/rebase-root/.git/
> echo junk > junk1.txt
> echo junk > junk2.txt
> git add *
> git commit -m "original initial commit"
[master (root-commit) 5ff16e2] original initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 junk1.txt
 create mode 100644 junk2.txt

Then we add another file and commit it with the commit #2 (later commit).

> echo other-stuff > happened.txt
> git add happened.txt
> git commit -m "some other stuff happened"
[master 913be84] some other stuff happened
 1 file changed, 1 insertion(+)
 create mode 100644 happened.txt

The log of commits now looks like this:

> git log
commit 913be844f45387f02e6048017dcf69870a422c45 (HEAD -> master)
Author: username <username@email>
Date:   Sun Sep 6 15:04:05 2020 -0400

    some other stuff happened

commit 5ff16e2360dc15cde556c7de22daf51044cfcf27
Author: username <username@email>
Date:   Sun Sep 6 15:03:31 2020 -0400

    original initial commit

Let our goal be to split the initial commit into two, adding each file separately. Looking on StackOverflow, we might try to follow these instructions. Unfortunately, this answer links the procedure for splitting an arbitrary commit in the middle of a repository's history, which will fail in our case. Let's see how!

We start by running the rebase command, which opens an interactive editor showing the commit log. The command takes -i flag for interactive mode and commitID for which commit you want to rebase to:

git rebase -i commitID # commitID can be e.g. HEAD, HEAD~i, commit's SHA1, --root

The editor also displays other options for rebasing e.g. re-ordering commits, changing commit message etc. To split the initial commit we run:

> git rebase -i --root
<in interactive editor, change "pick" to "edit" for the initial commit>
Stopped at 5ff16e2... original initial commit
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

To split the first commit, we need to move HEAD to the state before the two files were added and committed. The instructions tell us to run git reset HEAD~, which fails as there is nothing before the initial commit:

> git reset HEAD~
fatal: ambiguous argument 'HEAD~': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

One way around this problem is to insert another commit before the first commit. Note: We first tried to do this with an empty commit but rebase will later ignore any empty commits, making the next steps impossible. This is specified in the instructions in the interactive editor.

We can add another commit, again, with rebase, creating an empty dummy file. We will make it hidden so it doesn't get in our way in case we forget to delete it later. Before doing this, abort the rebase we just started.

> git rebase --abort
> git rebase -i --root
<select first commit by changing it from "pick" to "edit">
> printf "\n" > .init
> git add .init; git commit -m "dummy initial commit"
[detached HEAD 8b5b116] dummy initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 .init
> git rebase --continue
Successfully rebased and updated refs/heads/master.

Now a dummy commit is inserted after the initial commit, even though the timestamp is later than the "some other stuff happened" commit. Magic!

> git log
commit 47fafc012213854d221955accaa00c9e75ee564b (HEAD -> master)
Author: username <username@email>
Date:   Sun Sep 6 15:04:05 2020 -0400

    some other stuff happened

commit 8b5b1163871549187aa52ba6f378bd2c1bfb664f
Author: username <username@email>
Date:   Sun Sep 6 16:27:30 2020 -0400

    dummy initial commit

commit cbfb04e602a773229dcfed8cdb09e0b26a278bb0
Author: username <username@email>
Date:   Sun Sep 6 15:03:31 2020 -0400

    original initial commit

Now we rebase again and rearrange the dummy commit to be the first:

> git rebase -i --root
<edit the file to make the dummy initial commit the first commit:

pick 8b5b116 dummy initial commit
pick cbfb04e original initial commit  
pick 47fafc0 some other stuff happened

and change the original initial commit from pick to edit>
Stopped at cbfb04e... original initial commit
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

Now we can follow the instructions on splitting commits.

> git reset HEAD~
> git status
interactive rebase in progress; onto 55a921b
Last commands done (2 commands done):
   pick 8b5b116 dummy initial commit
   edit cbfb04e original initial commit
Next command to do (1 remaining command):
   pick 47fafc0 some other stuff happened
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'master' on '55a921b'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        junk1.txt
        junk2.txt

nothing added to commit but untracked files present (use "git add" to track)
> git add junk1.txt
> git commit -m "add first file"
[detached HEAD 8c3f670] add first file
 1 file changed, 1 insertion(+)
 create mode 100644 junk1.txt 
> git add junk2.txt
> git commit -m "add second file"
[detached HEAD f2bea4c] add second file
 1 file changed, 1 insertion(+)
 create mode 100644 junk2.txt
> git rebase --continue

Looking at the log, the initial commit has been successfully split:

> git log
commit 998e23ae093fe0914831569b72a2c1334dc8b5d3 (HEAD -> master)
Author: username <username@email>
Date:   Sun Sep 6 15:04:05 2020 -0400

    some other stuff happened

commit f2bea4ce051b31f038c2f1973302d8963b9f341f
Author: username <username@email>
Date:   Sun Sep 6 16:44:53 2020 -0400

    add second file

commit 8c3f67027bfffdcb62d9e001761709beca2a8575
Author: username <username@email>
Date:   Sun Sep 6 16:42:47 2020 -0400

    add first file

commit 8fc8bb4c61ba3093e0af0969f82c4dc97fed902f
Author: username <username@email>
Date:   Sun Sep 6 16:27:30 2020 -0400

    dummy initial commit

Finally, to clean up we can remove the dummy initial commit using rebase again:

> git rebase -i --root
<edit to remove the line for the dummy initial commit>

The log now begins with adding the first file:

> git log
commit 3f573b86e19ee2715d9fe41ced4a172d2eac7733 (HEAD -> master)
Author: username <username@email>
Date:   Sun Sep 6 15:04:05 2020 -0400

    some other stuff happened

commit f7402c06e8b6e6b26116facd8e11c0ba12d906d6
Author: username <username@email>
Date:   Sun Sep 6 16:44:53 2020 -0400

    add second file

commit 7bdaba78959cf49f42a5129f7646f1406b0f4725
Author: username <username@email>
Date:   Sun Sep 6 16:42:47 2020 -0400

    add first file

Last note: let's say you want to split a commit by someone else into a commit by you adding some files followed by the remaining history. You can do that by splitting that commit as shown above, adding the files you want, and making a note of the other author's name, email address and the commit's date and message (by running git log). Then commit the remaining files you didn't add with amending the date and author details to those of the original commit you are splitting:

> git commit --amend --date <commit_date> -m "commit_message"
> git rebase --continue

That way you can add a commit at the beginning of the history with your desired contents and have the entire rest of the history of the repo preserved after your changes. Note that doing that will re-write all commit IDs in the history so local copies of the repo will need to be synced or re-cloned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment