Renaming folders in Git on case insensitive file systems

24 Nov '18

(Skip to the TL;DR if you’re in a hurry)

It’s best not to use git on a case insensitive file system. But hindsight is 20/20, and here I am, with a messed up repo:

$ ls -1
A Space
Upper
lower
$ mv Upper/ upper/
$ ls -1
A Space
lower
upper
$ git ls-files
A Space/.gitkeep
Upper/.gitkeep
lower/.gitkeep

Oh no, git hasn’t picked up the rename, since it knows the file system is case insensitive. (In the example, it’s easy to fix, when I hit the issue, there were ~80 directories.)

First, it’s clear we need to use git commands to show the paths, not the paths on the file system, i.e. find is out.

git ls-files is okay, but git ls-tree works better. Here are the switches:

--full-tree is also useful, especially for scripts, but I’m going to ignore it.

Here’s the repo after I’ve added some nested directories for the demo:

$ git ls-tree -dr HEAD
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e    A Space
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e    Upper
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e    lower
040000 tree 9da866107da034aefd6da5e47d18d3ea2176d9ed    nested
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e    nested/Upper
040000 tree d564d0bc3dd917926892c55e3706cc116d5b165e    nested/lower

The output format is <mode> SP <type> SP <object> TAB <file>, so we can split on tab (\t, which is the default for cut):

$ git ls-tree -dr HEAD | cut -f2
A Space
Upper
lower
nested
nested/Upper
nested/lower

For further processing, I’m going to use perl instead of awk. It’s more portable (there are several awk variants), and easier to read (crazy, I know).

Here are the switches (perlrun):

$ git ls-tree -dr HEAD | \
>   perl -lF'\t' \
>   -e 'print "$F[1]";'
A Space
Upper
lower
nested
nested/Upper
nested/lower
$ git ls-tree -dr HEAD | \
>   perl -lF'\t' \
>   -e '$a = $F[1]; $b = lc $a; print "$a\n$b" if $a ne $b'
A Space
a space
Upper
upper
nested/Upper
nested/upper

So now I have only folders with upper case in the names, and the lower case version of that, which is useful for the rename. Using xargs to apply two consecutive lines to a command:

$ git ls-tree -dr HEAD | \
>   perl -lF'\t' \
>   -e '$a = $F[1]; $b = lc $a; print "$a\n$b" if $a ne $b' | \
>   xargs -d'\n' -L2 printf '"%s" "%s"\n'
"A Space" "a space"
"Upper" "upper"
"nested/Upper" "nested/upper"

Good to see spaces aren’t an issue.

To avoid the error fatal: renaming 'foldername' failed: Invalid argument, I’m going to use a temporary directory for the rename (via stackoverflow). It’s a good idea to check if you already have a directory called temp, and use a different name if required.

$ git ls-tree -dr HEAD | cut -f2
A Space
Upper
lower
nested
nested/Upper
nested/lower
$ git ls-tree -dr HEAD | \
>   perl -lF'\t' \
>   -e '$a = $F[1]; $b = lc $a; print "$a\n$b" if $a ne $b' | \
>   xargs -d'\n' -L2 \
>   bash -c 'git mv "$0" "temp" && git mv "temp" "$1"'
$ git commit -m "rename"
[master 1b96fba] rename
 3 files changed, 0 insertions(+), 0 deletions(-)
 rename {A Space => a space}/.gitkeep (100%)
 rename {Upper => nested/upper}/.gitkeep (100%)
 rename {nested/Upper => upper}/.gitkeep (100%)
$ git ls-tree -dr HEAD | cut -f2
a space
lower
nested
nested/lower
nested/upper
upper

Boom, done.

TL;DR

git ls-tree -dr HEAD | \
  perl -lF'\t' \
  -e '$a = $F[1]; $b = lc $a; print "$a\n$b" if $a ne $b' | \
  xargs -d'\n' -L2 \
  bash -c 'git mv "$0" "temp" && git mv "temp" "$1"'

git

Older