5

I have a Git repository with two main branches: master and dev. It currently looks like this:

master  ( )----( )----(A)----(B)----(C)
                \
                 \
dev              ( )----( )----( )----( )----(~ 30 additional commits...)

Pretty simple, except that commit A on master contains a major change to the file and folder structure (basically, I used the steps shown here to move the repository up one level in the hierarchy).

I would like to resume work on the dev branch, but when I tried to rebase dev onto master to bring the new file structure into dev, Git spewed a ton of errors. I git reset my way back to a sane state, but in my (somewhat limited) experience with Git, that many errors usually means that I was taking the wrong approach.

Is there a better way to get the reorganized folder structure over to the dev branch? If it helps, the changes in the other commits on master (B and C) are pretty minor and I would be OK with losing them if it would make things easier.

Community
  • 1
  • 1
Matt Peterson
  • 5,169
  • 4
  • 32
  • 34
  • I haven't actually *tried* this, but if your Git is new enough, `rebase` now has `-s ` and `-X `, so you can add `-X find-renames=` to force rebase to use a merge style cherry pick with a particular rename detection threshold, or simply `git rebase -m` to force it to use merge-enabled cherry-picking in the first place (though whether you're getting that now is also Git version-dependent, as well as rebase-command dependent). Since you're getting into Git minutiae, you may want to include exact commands used. – torek Dec 30 '16 at 23:10

1 Answers1

18

Yes, there is a sane way to do this.

Preface

master is now (if I understood correctly):

project
   src
     file.c

dev is, since it did not contain commit A:

src
  file.c         # with some changes

Your branches are like this:

master  ( )----(A0)----(A)----(B)----(C)
                \
                 \
dev              (D1)----(D2)----(D3)----( )----(~ 30 additional commits...)

I also assume that A differs from A0 in no way whatsoever, except project/ being inserted into all file paths ("moving your tree up" into the new project root, as per your linked question).

Let's do it...

We do exactly what git rebase would do internally, while fooling git a bit, on every step, by doing our own "cherry-pick" step instead of what git would do itself.

We use a second working directory. $WD1 is the absolute path to your original working directory, $WD2 is some other absolute path (outside of that).

First, create your helper directory as a clone of your original:

cd $WD2
git clone $WD1 dev .

Then, start a branch newdev which will eventually be dev rebased on A.

cd $WD1
git checkout A
git checkout -b newdev

We get the D1 commit into newdev without giving git any chance to mess it up:

cd $WD2
git checkout D1

cd $WD1
rm -r project/src
cp -r $WD2/src project/
git add -A
git commit -m "`cd $WD2 ; git log -1 --format=%s`"

The situation is now:

newdev                       (D1')     
                           /
                         /
master  ( )----(A0)----(A)----(B)----(C)
                \
                 \
dev              (D1)----(D2)----(D3)----( )----(~ 30 additional commits...)

Now, repeat for D2:

cd $WD2
git checkout D2

cd $WD1
rm -r project/src
cp -r $WD2/src project/
git add -A
git commit -m "`cd $WD2 ; git log -1 --format=%s`"

The situation is now:

newdev                       (D1')----(D2')     
                           /
                         /
master  ( )----(A0)----(A)----(B)----(C)
                \
                 \
dev              (D1)----(D2)----(D3)----( )----(~ 30 additional commits...)

Repeat until finished.

Of course, you will want to create a small shell script for those commands, which iterates through all commits D1 ... D30.

Afterwards, a simple git rebase master will do the rebase from A to C as usual. Then, get rid of newdev by changing the name around:

git checkout newdev
git branch olddev dev         # just in case...
git branch -D dev
git checkout -b dev

General notes

Why the second working directory?

Note that in this particular case there might be ways to avoid the secondary working directory, by using rm -r project/src ; git checkout D1 src ; mv src project/ directly. I prefer to do it as shown above instead, just to be 100% sure everything is very clean and "visible" at all times. This way the checkout operation is cleanly separated from the modification that we apply on our own.

There is literally nothing that could go wrong this way, and the approach works with all other kinds of changes as well. For a non-trivial change, assume somebody changed every whitespace in every single source file in A. This approach makes it trivial to rebase other branches onto this (if you can put that whitespace change into a script as well).

AnoE
  • 8,048
  • 1
  • 21
  • 36
  • 1
    Thanks for the very thorough solution. This got me pointed in the right direction: cloning the repo into a second folder then looping through the commits on dev and manually "replaying" them on top of master. I used a diff tool to apply the changes in each commit from dev (instead of rm and cp), but it worked! – Matt Peterson Jan 04 '17 at 15:56
  • 1
    Good job, "get'er done", it's the idea that counts. :) – AnoE Jan 04 '17 at 20:26
  • 1
    This answer deserves way more up votes for being so thorough. Thanks. – NealeU Sep 06 '19 at 08:38