gitgit-branchgit-branch-sculpting

Git branch from old commits


I've researched this on SO and other documentation. I've tried rebasing, merging, cherry-picking, and detached heads.

After literally 6 hours of trying to do this, it's time to ask the question!

Here's what I'm starting with:

A-B-C-D-E-F-G master

Here's what I want to change it to:

A-B      F-G  master
   \    / 
    C-D-E dev

The closest I've come is:

A-B      C-D-E-F-G-H Main
   \    / 
    C-D-E dev
        \
         F-G master

I had to add another branch name and a dummy commit to get the branching to work at all.

But this is worse than no change.

Please help!

UPDATE AFTER REPLIES

Thanks for the replies.

Yes, my "what I want" was not very clear. Hopefully this clarifies it:

A-B   -  F-G  master
   \    / 
    C-D-E dev

In other words, "master" at F is the result of B plus branch "dev" merged into it.

STILL NOT WORKING

Thanks @frasertweedale for the attempted answer.

I've followed your instructions carefully and here's what I end up with:

A-B   -  "merge branch 'dev'"-G  master
   \    / 
    C-D-E dev
        \ 
         F-G-"merge branch 'dev'"-F-G

I'm glad this is hard: having now spent 7 hours on this I don't feel as bad!

FINAL RESULT As @frasertweedale said, this is exactly what I wanted except for the dangling commits.

So, I simply need to get rid of the dangles.

For clarity, I've redrawn below:

A-B   -  "merge branch 'dev'"-G2  master
   \    / 
    C-D-E dev
        \ 
         F1-G1-"merge branch 'dev'"-F2-G2

And this command does the cleanup:

git rebase -p --onto F1^ F1

Result:

A-B   -  "merge branch 'dev'"-G2  master
   \    / 
    C-D-E dev

"merge branch 'dev'" contains the original F commit, and can, of course, be re-commented that way.


Solution

  • Assuming you expand A, B, etc into their corresponding commit hashes, and that master is at G, the first step is to branch dev at E:

    git branch dev E
    

    Then reset master to B.

    git reset --hard B
    

    Next, merge dev into master, explicitly demanding a merge commit (it will otherwise do a fast-forward since dev has not diverged from master):

    git merge --no-ff dev
    

    master now contains commits up to E. Now you can cherry-pick the original F and G commits.

    git cherry-pick F
    git cherry-pick G
    

    Now you have something very close to what you wanted; the only difference is the addition of a merge commit (B+E):

    A-B   -  (B+E)-F-G  master
       \    / 
        C-D-E dev
    

    If you now want to include the content of F in the merge commit (B+E), you can perform an interactive rebase and squash the two commits together. The --preserve-merges (alias -p) option is essential.

    git rebase --interactive --preserve-merges B
    

    This will invoke an editor with content similar to the following (commit hashes and summaries will differ):

    pick d4057bd c
    pick 9419ef8 d
    pick ad9e208 e
    pick 120a90a Merge branch 'dev'
    pick 61552ad f     
    pick c947153 g
    

    Replace the pick directive at the start of the F commit line with the squash directive (in the above example: squash 61552ad f), then save and exit. You will be prompted to form a new commit message from the commit messages of the merge commit and F, and the rebase operation will then complete without further interation. After the rebase, you will have exactly the repository history you asked for (in your updated question).

    ABBREVIATED SCRIPT

    The following script creates a new repository with a linear history, then automatically edits the history using (mostly) the same steps as above. The interactive rebase is avoided so that user intervention is not required.

    mkdir foo ; cd foo
    git init
    for C in A B C D E F G; do
        echo $C > $C
        git add $C
        git commit -m $C
        git tag $C
    done
    git branch dev E
    git reset --hard B --
    git merge --no-ff dev
    git cherry-pick F
    git reset --soft HEAD~
    git commit --amend -m F
    git cherry-pick G
    for C in A B C D E F G; do
        git tag -d $C
    done