Rod McLaughlin

This is not an April Fools' Day Joke (01 apr 21)

How to copy nested folders, overwriting .git folders if necessary

If you use the source control system called GIT, you end up with lots of folders containing .git folders. If you want to copy a new version of one of these folders onto an old version, you can't just copy the contents of the new .git into the old .git. Git won't let you, and if you did, it might get its knickers in a twist. The old version might have a file called 38e46da630c7f6abd877b800c78a7b9136b57c, and the new one has one called cf7c6ef6b2101ea8ab548fe9335662e8481617. but not 38e46da630c7f6abd877b800c78a7b9136b57c.If you just copied all from the new .git into the old one, you'd end up with both of these files, and Git wouldn't understand what that means.

Unless you're Linus, the guy who created Git, there is no way you could work out which files to copy and/or rename.

So I had to create a script which copies all the files and folders from new into old, but if new has a .git folder, and so does old, delete the contents of the old .git folder before overwriting its contents with the contents of the new one. It goes like this:

shopt -s dotglob

if [ "" == "$2" ] ; then
  echo "Takes two arguments, a source folder and a target folder"
  echo "The second argument should be simple, eg. just 'dev', not /Users/me/dev"
  echo "If you are in the folder directly above dev/, the first argument could"
  echo "be e.g. /Volumes/mydrive/dev, and the second argument could be dev"
  exit 1

if [[ ! -d $1 || ! -d $2 ]] ; then
  echo "Takes two arguments, a source folder and a target folder"
  exit 1

targetgits=`find $target -type d -name .git`
sourcegits=`find $source -type d -name .git | cut -d'/' -f2-`

for git in $targetgits ; do
  shortpath=`echo $git | cut -d'/' -f2-`
  if echo ${sourcegits[@]} | grep -q -w $shortpath; then
    echo "$shortpath is in source"
    echo "So - deleting everything in $git before replacing it"
    rm -Rf $git/*
    echo "$shortpath is not in source - leaving $git alone"

sourcedirs=`find $source -type d -depth 1 -exec basename {} \;`
for dir in $sourcedirs ; do
  if [ ! -d $target/$dir ] ; then
    echo "Creating $target/$dir"
    mkdir $target/$dir
  echo "Copying $source/$dir/* into $target/"
  cp -r $source/$dir/* $target/$dir/

shopt -u dotglob

I tested it by creating a folder called tmp, and in it, a folder called dev. I cd’d into tmp/dev, and created folders foo and bar. In them, I created the following folders, with the following characteristics.


   one - containing one file, and three git commits

   two - containing one file, and one git commit

   three - one file, no git

   five - empty

Then I created the following


   one - containing one file, and one git commit

   two - containing one file, and no git

   three - one file, one git commit

   four - empty


If my script worked, foo bar

I would expect

foo - no changes at all

bar - the following state:

   one - one file, three git commits, same as foo/one

   two - one file, one git commit, same as foo/two

   three - one git commit

   four - empty

   five - empty

This is what in fact happened. The only thing which went wrong was when I cd'd to /Volumes/mydrive, and tried to copy dev/* to ~/dev {/Users/rod/dev}, it didn't like the second argument beginning with a /. But when I cd'd into /Users/rod, and issued the following command, it worked. /Volumes/mydrive/dev dev

Portland London