How can I move a directory in a Git repo for all commits?

28,073

Solution 1

You can use the subdirectory filter to achieve this

 $ git filter-branch --subdirectory-filter blog/ -- --all

EDIT 1: If you don't want to effectively make _posts the root, use a tree-filter instead:

 $ git filter-branch --tree-filter 'mv blog/_posts .' HEAD

EDIT 2: If blog/_posts did not exist in some of the commits, the above will fail. Use this instead:

 $ git filter-branch --tree-filter 'test -d blog/_posts && mv blog/_posts . || echo "Nothing to do"' HEAD

Solution 2

While Ramkumar's answer is very helpful and worthwile, it will not work in many situations. For example, when you want to move a directory with other subdirectories to a new location.

For this, the man page contains the perfect command:

git filter-branch --index-filter \
  'git ls-files -s | sed "s-\t\"*-&NEWSUBDIR/-" |
   GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
   git update-index --index-info &&
   mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD

Just replace NEWSUBDIR with your desired new directory. You can also use nested dirs like dir1/dir2/dir3/-"

Solution 3

Created a generic script for arbitrary moves/renames: https://gist.github.com/xkr47/f766f4082112c086af63ef8d378c4304

Examples:

git filter-mv 's!^!subdir/!'

➜ moves all files to a subdirectory "subdir/" in all commits of the current branch

git filter-mv 's!^foo/bar.txt$!foo/barbar.txt!'

➜ renames foo/bar.txt to foo/barbar.txt in all commits of the current branch

Share:
28,073
mipadi
Author by

mipadi

Just some guy, y'know? I write code for Industrial Light & Magic. I'm writing a book on Objective-C. Or rather, I was. Thanks to Apple, I'm now writing a book on Swift. You can check it out here. Oh, I also wrote a URL shortener for Stack Overflow profiles. Feel free to use it if you want!

Updated on July 05, 2022

Comments

  • mipadi
    mipadi almost 2 years

    Let's say I have a repo that includes this directory structure:

    repo/
      blog/
        _posts/
          some-post.html
      another-file.txt
    

    I want to move _posts to the top level of the repo, so the structure will look like this:

    repo/
      _posts/
        some-post.html
      another-file.txt
    

    This is simple enough with git mv, but I want to make the history look as though _posts always existed at the root of the repo, and I want to be able to get the entire history of some-post.html via git log -- _posts/some-post.html. I imagine I can use some magic with git filter-branch to accomplish this, but I haven't figured out exactly how to do that. Any ideas?

  • Cascabel
    Cascabel almost 14 years
    It's also much faster to use --index-filter, since it doesn't have to check out the tree.
  • sehe
    sehe about 13 years
    Yeah index-filter is faster, but it won't work because the commands shown do not affect the index. You need to do index manipulations only if you want to use index-filter (e.g. git rm --cached instead of rm)
  • Jeremy Huiskamp
    Jeremy Huiskamp over 10 years
    And since it's not immediately obvious from looking at that command or the resulting errors, the \t doesn't work on os x's version of sed. There's lots of ways around that, but perhaps the quickest is to delete the \t and replace it with a literal tab by typing ctrl-v, <tab>.
  • joshcomley
    joshcomley over 9 years
    How do you specify the original folder, or does this just move the entire branch? When I try to run this from a folder I'd like to move I get You need to run this command from the toplevel of the working tree
  • Lucius
    Lucius about 9 years
    What does the sed command do? I'm trying this on Windows and need an alternative.
  • Will
    Will about 9 years
    Thanks, this helped me a lot! @Lucius, the sed command is much more clear if you replace the "&" with another \t and the "-"s with an @.
  • KCD
    KCD almost 9 years
    Brilliant. But yes the sed is confusing so try the second line alone to test it. I.e. to remove unnecessary top level directories I did a simple git ls-files -s | sed "s-\tdir1/dir2/dir3/-\t-"
  • dr0i
    dr0i about 8 years
    I tried so many answers on the internet, this is the only one working - thx a lot!
  • Knut Forkalsrud
    Knut Forkalsrud about 8 years
    If you're filtering a commit that effectively deletes all files you end up with git update-index not creating the file "$GIT_INDEX_FILE.new" and thus the mv command fails. I ended up with test -f \$GIT_INDEX_FILE.new && mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE || touch \$GIT_INDEX_FILE inside the filter-branch script.
  • Admin
    Admin almost 8 years
    @KnutForkalsrud I get the error of index.new': No such file or directory, as this git repository was a result of an svn-migration, there is no initial commit. Could you share your command, or is it just insert that into the filter, with no quotations?
  • Knut Forkalsrud
    Knut Forkalsrud almost 8 years
    @Jägermeister - just replace the "mv" command in theduke's example with the "test && mv || touch" combination from my comment.
  • polm23
    polm23 almost 7 years
    Something about using - as a separator confused my sed, so I had to change that like so: sed "s:\t\"*:&NEWSUBDIR/:".
  • briceburg
    briceburg almost 7 years
    As noted, BSD/macos sed does not support \t, and the pattern fails on paths with hyphens in their name -- so I wrote a cross platform solution that uses awk; git ls-files -s | awk '{ sub(/\t/, "\tsubdir/"); print}' full usage: set DEST to desired subdir DEST="subdir/path/" git filter-branch -f --index-filter 'git ls-files -s | awk -v prefix="$DEST/" "{ sub(/\t/, \"\t\" prefix); print}" GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD
  • Michael
    Michael almost 7 years
    Can I keep tags too? It looks like they are gone in my case.
  • jthill
    jthill over 6 years
    git filter-branch --index-filter 'git read-tree --prefix=/ $GIT_COMMIT:_posts; git rm -r --cached _posts' -- --all. Add --tag-name-filter cat if your tags aren't signed and you want to move them/invalidate the old ones.
  • gablin
    gablin about 6 years
    @briceburg The mv is missing between && and "$GIT_INDEX_FILE.new". Other than that, it works perfectly!
  • askb
    askb over 5 years
    Fails with the following error Rewrite 7576b38152b393793b1c9ec3df0ff86685f95236 (1/8246) (0 seconds passed, remaining 0 predicted) mv: cannot stat '/tmp/controller/.git-rewrite/t/../index.new': No such file or directory index filter failed: git ls-files -s | sed "s-\t\"*-&opendaylight/blueprint/-" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
  • Roi Danton
    Roi Danton over 5 years
    @askb: You can skip bad commits with appending ; /bin/true. See this excellent post.
  • SiliconMind
    SiliconMind over 4 years
    For those that are here just for copy-paste: remember to git push --force --all for all the branches. Otherwise you can end up with funny situations.
  • Etienne Maheu
    Etienne Maheu about 4 years
    For those Windows users who stumbled on this answers like me and wonder how to make it work on Windows: consider using WSLS and run the command from linux. Just make sure that your repository is checked out with AutoCRLF to false or else the git command in linux is going to think all files have changes and will block the execution.
  • AVIDeveloper
    AVIDeveloper about 4 years
    Running under Cygwin on Windows, the above didn't work. Eventually, my issue was that my subdir contains dashes (e.g. "sect-03"), which is the separator used in the sed section. Replacing sed "s-\t\"*-&sect-03/-" with sed "s/\t\"*/&sect-03\//" did the trick.