3

I have a problem, please be kind enough to advise! I have an existing git repo, and for various reasons(that I wont go into here), I am trying to create a ROOT commit

Say this is my git commit history:

(ROOT) C1 <-C2 <-C3 <-C4 <-C5   <--branchname (HEAD)

I want to add an initial commit (CX, which is not empty) BEFORE C1. So it should end up like this:

(NEW ROOT) CX-C1 <-C2 <-C3 <-C4 <-C5   <--branchname (HEAD)

I found a similar question here: Insert a commit before the root commit in Git? But it's for appending an EMPTY git commit before the existing root. I also tried the steps in the answer here: https://stackoverflow.com/a/9736098/11491070 with one change: I replaced git commit --allow-empty -m 'initial' with git add .; git commit -m "initial laravel commit"; git push; and then this rebase step: git rebase --onto newroot --root master is failing with a TON of merge conflicts:

First, rewinding head to replay your work on top of it...
Applying: add initial quickadminpanel, with admin intrface and APIs for tags. Also added (but not yet enabled) ajax datatables module
Using index info to reconstruct a base tree...
.git/rebase-apply/patch:4537: trailing whitespace.
     * 
.git/rebase-apply/patch:4539: trailing whitespace.
     * 
.git/rebase-apply/patch:4547: trailing whitespace.
     * 
warning: 3 lines add whitespace errors.
Falling back to patching base and 3-way merge...
CONFLICT (add/add): Merge conflict in webpack.mix.js
Auto-merging webpack.mix.js
CONFLICT (add/add): Merge conflict in routes/web.php
Auto-merging routes/web.php
CONFLICT (add/add): Merge conflict in routes/api.php
Auto-merging routes/api.php
CONFLICT (add/add): Merge conflict in resources/views/welcome.blade.php
Auto-merging resources/views/welcome.blade.php
CONFLICT (add/add): Merge conflict in resources/sass/app.scss
Auto-merging resources/sass/app.scss
CONFLICT (add/add): Merge conflict in resources/sass/_variables.scss
Auto-merging resources/sass/_variables.scss
CONFLICT (add/add): Merge conflict in resources/lang/en/validation.php
Auto-merging resources/lang/en/validation.php
CONFLICT (add/add): Merge conflict in resources/lang/en/passwords.php
Auto-merging resources/lang/en/passwords.php
CONFLICT (add/add): Merge conflict in resources/lang/en/pagination.php
Auto-merging resources/lang/en/pagination.php
CONFLICT (add/add): Merge conflict in resources/lang/en/auth.php
Auto-merging resources/lang/en/auth.php
CONFLICT (add/add): Merge conflict in resources/js/bootstrap.js
Auto-merging resources/js/bootstrap.js
CONFLICT (add/add): Merge conflict in resources/js/app.js
Auto-merging resources/js/app.js
CONFLICT (add/add): Merge conflict in package.json
Auto-merging package.json
CONFLICT (add/add): Merge conflict in database/seeds/DatabaseSeeder.php
Auto-merging database/seeds/DatabaseSeeder.php
CONFLICT (add/add): Merge conflict in database/migrations/2014_10_12_100000_create_password_resets_table.php
Auto-merging database/migrations/2014_10_12_100000_create_password_resets_table.php
CONFLICT (add/add): Merge conflict in database/factories/UserFactory.php
Auto-merging database/factories/UserFactory.php
CONFLICT (add/add): Merge conflict in database/.gitignore
Auto-merging database/.gitignore
CONFLICT (add/add): Merge conflict in config/services.php
Auto-merging config/services.php
CONFLICT (add/add): Merge conflict in config/logging.php
Auto-merging config/logging.php
CONFLICT (add/add): Merge conflict in config/database.php
Auto-merging config/database.php
CONFLICT (add/add): Merge conflict in config/cache.php
Auto-merging config/cache.php
CONFLICT (add/add): Merge conflict in config/broadcasting.php
Auto-merging config/broadcasting.php

How can I fix this problem? Please help!

kp123
  • 1,250
  • 1
  • 14
  • 24

1 Answers1

4

First, a side note: you've drawn your repository forwards. Git does them backwards. Put the root commit last, i.e., on the left, and the latest commit first, i.e., on the right:

C1 <-C2 <-C3 <-C4 <-C5   <--branchname

The branch name always holds the hash ID of the latest commit. That's how Git knows which one is the latest. Git uses C5 to find C4, because commit C5 itself, which has a unique hash ID, contains the unique hash ID of commit C4. Meanwhile C4 holds the hash ID of C3, and so on; C2 holds the unique hash ID of C1, and C1 is a root commit because it has no parent hash ID inside it.

Second, no commit can actually be changed. So what you will do here is not "change commit C1"—neither you nor Git can do this—but rather, make a new commit, C1' or C21 or whatever we want to call it. Before this new commit we want another new commit, CX, that is a root commit and contains the desired content.

You can use the method in Antony Hatchkins's answer, but these days, the way to do that would be to start with git checkout --orphan (rather than tricky symbolic ref commands):

git checkout --orphan newroot
git rm -rf --cached .            # same as before
<arrange work tree as desired>   # same as before
git add .                        # or git add each file, same as before
git commit                       # same as before

That's the sequence of operations that creates commit CX, so that we have:

C1--C2--C3--C4--C5   <-- master

CX   <-- newroot

Now we get to the most interesting part. Using git rebase is the wrong way to go because rebase works by cherry-picking. This turns commit C1 into a request to add every file—commit C1, being a root commit, gets compared to the empty tree to see what changed—and as you've seen, this results in an add/add conflict for every file you added in C1 that is already in CX.

The question you must answer here—I cannot answer it for you—is: What content do you want in your new copy of C1 that is like C1 except that it has CX as its parent? That is, in the end, we'll have:

C1--C2--C3--C4--C5    [abandoned]

CX--C1'-C2'-C3'-C4-C5'   <-- master

in our repository. If I run git checkout on the hash ID of C1', should I see the same content as if I run git checkout on the hash ID of C1? The two commits will have different hash IDs, but the same author and committer and timestamp and log messages and so on. If they should have the same tree (snapshot) too, the job is now pretty easy, using git replace and git filter-branch. Here is the basic recipe:

git replace --graft <hash-of-C1> newroot

(and at this point you can run git log to make sure it looks right). Then run:

git filter-branch -- master

(assuming you want only the commits reachable from master copied, and ref master updated). The no-op filter-branch (no filters specified) copies each reachable commit, in the right (i.e., forwards, which is backwards to Git) order, copying C1 first, then C2, and so on, but—crucially—obeying the replacement directive. So now we have this:

C1--C2--C3--C4--C5    refs/original/refs/heads/master

CX   <-- refs/replace/<hash-of-C1>
  \
   C1'-C2'-C3'-C4-C5'   <-- master

If this is the desired final result, we now need only remove the refs/original/ name, and the no-longer-useful refs/replace/<hash-of-C1> name:

git update-ref -d refs/original/refs/heads/master
git update-ref -d refs/replace/<hash-of-C1>

Your history is now rewritten. All old clones must be destroyed (or at least, you should stop using them) because the hash IDs in them are those of the original commits, that you don't want to come back into your life to make that life miserable. The abandoned C1-through-C5 in your updated clone are harmless: they can be left there, in their abandoned state, until Git's Grim Reaper Collector, git gc, gets around to garbage-collecting them.

torek
  • 448,244
  • 59
  • 642
  • 775
  • @torek...omg you are a JEDI MASTER of git. This worked perfectly (even though half of what you said went over my head). I cannot thank you enough. Ps. I've updated the original question to show the correct commit sequence in my illustration). Thank you kindly! – kp123 Dec 31 '19 at 21:31
  • @torek @kp123 At the point where `git filter-branch -- master` must be used, I always get the error `You need to run this command from the toplevel of the working tree.` – Any ideas why that is not working for me? I followed every step, only difference that I work on a "git clone" of my repo (before doing everything "productive"). – Oliver Mar 14 '21 at 21:42
  • @Oliver: run `git rev-parse --show-toplevel` and/or `git rev-parse --show-cdup` to see where you are with respect to the top level of the working tree for the repository you are in. I thought filter-branch was, at some point, adjusted so that it could run from a subdirectory, but perhaps not. (Filter-branch is officially deprecated already, in favor of the new filter-repo that's not distributed with Git, which is kind of a messy situation. You might want to obtain filter-repo and experiment with it though.) – torek Mar 15 '21 at 08:03
  • @torek thank you!! I feel a bit dumb now, as I figured out the solution was to simply `cd ..` a dir level up to be literally in the git repo‘s root when firing the command ^^ – Oliver Mar 16 '21 at 18:55
  • This is indeed a jedi master-level solution! Incredible. May you ealborate a bit more on the very last part about destroying clones & waiting for `git gc`: is there any possibility to actively destroy / remove the original `master`-branch & only leave my `newmaster`-branch with the rewritten history in place? Tried force running `git gc --prune=now --aggressive` without any visible results locally. Is the way to go to just push the `newmaster` to my orign & remove there the old `master`-branch there? – Oliver Mar 16 '21 at 21:27
  • @Oliver: The basic problem—which is somewhat reduced in modern Git, but was a big deal back in the Git 1.5 to 1.7 days when I was managing a big company's Git repos—is that if you have an old clone that *hasn't* been filtered, and a new one that *has*, and someone uses `git pull` between them, you double up all the history you thought you'd gotten rid of. Git really likes to add "new" (to it) commits, and really hates to ditch outdated commits, so it's way too easy to get things back that you thought you were rid of. – torek Mar 16 '21 at 23:37
  • @torek yes I see, experienced that behavior a few times while trying to get the history rewritten already. However, I am the owner of an open source project & got access to old versions from pre-git initial commit, that I want to add to the (new) history. We only have 2 collaborators in close exchange. What would you recommend to make sure the new „history“ is the single source of truth? Is there even a way for to do that based on your Jedi trick? – Oliver Mar 17 '21 at 07:59
  • With just the two collaborators, manual cooperation is probably fine. It's basically just control over pull / merge operations that's needed. – torek Mar 17 '21 at 12:34