How do I Re-root a git repo to a parent folder while preserving history?
What you want is git filter-branch
, which can move a whole repository into a subtree, preserving history by making it look as if it's always been that way. Back up your repository before using this!
Here's the magic. In /foo/bar
, run:
git filter-branch --commit-filter '
TREE="$1";
shift;
SUBTREE=`echo -e 040000 tree $TREE"\tbar" | git mktree`
git commit-tree $SUBTREE "$@"' -- --all
That will make the /foo/bar
repository have another 'bar' subdirectory with all its contents throughout its whole history. Then you can move the entire repo up to the foo
level and add baz
code to it.
Update:
Okay, here's what's going on. A commit is a link to a "tree" (think of it as a SHA representing a whole filesystem subdirectory's contents) plus some "parent" SHA's and some metadata link author/message/etc. The git commit-tree
command is the low-level bit that wraps all this together. The parameter to --commit-filter
gets treated as a shell function and run in place of git commit-tree
during the filter process, and has to act like it.
What I'm doing is taking the first parameter, the original tree to commit, and building a new "tree object" that says it's in a subfolder via git mktree
, another low-level git command. To do that, I have to pipe into it something that looks like a git tree i.e. a set of (mode SP type SP SHA TAB filename) lines; thus the echo command. The output of mktree
is then substituted for the first parameter when I chain to the real commit-tree
; "$@"
is a way to pass all the other parameters intact, having stripped the first off with shift
. See git help mktree
and git help commit-tree
for info.
So, if you need multiple levels, you have to nest a few extra levels of tree objects (this isn't tested but is the general idea):
git filter-branch --commit-filter '
TREE="$1"
shift
SUBTREE1=`echo -e 040000 tree $TREE"\tbar" | git mktree`
SUBTREE2=`echo -e 040000 tree $SUBTREE1"\tb" | git mktree`
SUBTREE3=`echo -e 040000 tree $SUBTREE2"\ta" | git mktree`
git commit-tree $SUBTREE3 "$@"' -- --all
That should shift the real contents down into a/b/bar
(note the reversed order).
Update: Integrated improvements From Matthew Alpert's answer below. Without -- --all
this only works on the currently-checked out branch, but since the question is asking about a whole repo, it makes more sense to do it this way than branch-by-branch.
Rather than create a new repository, move what's in your current repository into the right place: create a new directory bar
in your current directory and move the current content in (so your code is in /foo/bar/bar
). Then create a baz
directory next to your new bar
directory (/foo/bar/baz
). mv /foo /foo2; mv /foo2/bar /foo; rmdir /foo2
and you're done :).
Git's rename tracking means that your history will still work and Git's hashing of content means that even though you've moved things around, you're still referencing the same objects in the repository.
I had a solution no one seems to have said yet:
What I specifically needed was to include files from the parent directory in my repository (effectively moving the repo up one directory).
I achieved this via:
- move all files (except for .git) into a new subdirectory with the same name. And tell git about it (with
git mv
) - move all files from the parent directory into the now empty (except for .git/) current directory and tell git about it (with
git add
) - commit the whole thing into the repo, which hasn't moved (
git commit
). - move the current directory up one level in the directory hierarchy. (with command line jiggery-pokery)
I hope this helps the next guy to come along -- I'm probably just having a brainless day, but I found the answers above over-elaborate and scary (for what I needed.) I know this is similar to Andrew Aylett's answer above, but my situation seemed a little different and I wanted a more general view.
Most common solution
In most normal situations, git looks at all files relatively to its location (meaning the .git directory), as opposed to using absolute file paths.
Thus, if you don't mind having a commit in your history which shows that you have moved everything up, there is a very simple solution, which consists in moving the git directory. The only slightly tricky thing is to make sure git understands that the files are the same and that they only moved relatively to him :
# Create sub-directory with the same name in /foo/bar
mkdir bar
# Move everything down, notifying git :
git mv file1 file2 file3 bar/
# Then move everything up one level :
mv .git ../.git
mv bar/* .
mv .gitignore ../
# Here, take care to move untracked files
# Then delete unused directory
rmdir bar
# and commit
cd ../
git commit
The only thing to be careful, is to correctly update .gitignore when moving to the new directory, to avoid staging unwanted files, or forgetting some.
Bonus solution
In some settings, git manages to figure out by itself that files have been moved when it sees new files that are exactly the same as deleted files. In that case, the solution is even simpler :
mv .git ../.git
mv .gitignore ../.gitignore
cd ../
git commit
Again, be careful with your .gitignore
This specifically answers "how do I move my git repo up one or more directories and make it look like it was always that way?"
With the advent of git >= 2.22.0
git filter-repo
can be leveraged to rewrite history to appear as if that parent directory had always been part of it.
This is the same thing that @Walter-Mundt's answer accomplishes using git filter-branch
, but is simpler and not as fragile to execute.
Note that these days git filter-repo
is advertised by git filter-branch
itself as the safer alternative.
So, given that your repo lives in /foo/bar/baz
and you want to move it up to /foo
First, to prevent any changes to the files in the workspace while history is being rewritten, temporarily turn the repository into a so-called "bare" one like this:
cd /foo/bar/baz
git config --local --bool core.bare true
The actual history rewriting can now be done directly in the .git
directory itself:
cd ./.git
git filter-repo --path-rename :bar/baz/
This will rewrite the repo's complete history as if every path has always had bar/baz/
prepended to it (which they would have had, had the repo's root been two levels up). The actual files are untouched by this operation because this is a bare repository now.
To wrap up, turn it un-bare again, move the .git
directory up to its designated position, and reset:
git config --local --bool core.bare false
cd ..
mv ./.git ../..
cd ../..
git reset
I think, the git reset
cancels the after-effects of the repository having been turned bare and back again. Try a git status
before doing git reset
to see what I mean.
A final git status
should now prove that all is well, modulo some new untracked files in /foo/qux
to deal with.
CAVEAT - if you try the above on an un-cloned repository, git filter-repo
will refuse to do its magic unless you --force
it to... Have a backup at the ready and consider yourself warned.