Tuesday, March 8, 2011

Managing user files with git

So now that I'm moving some of my projects over to git, I decided I should better automate syncing of user files across machines. Specifically, I want (some of) my $HOME/.* files and (some of) my $HOME/bin/* files version controlled and sym-linked to where they normally are.

So I made a repository with a conf directory and bin directory and symlinked the files to their respective $HOME and $HOME/bin locations. But then I wondered if I could do something a bit better. Specifically, I was thinking about what would happen when I added a new file to the repository. Inevitably, things would be fine, but on at least one computer, I'd forget to symlink the new files and I'd be confused for a bit.

In order to prevent what is for all practical purposes, a non-problem, I spent mucking around with git-hooks and git's config options, I came up with a solution. Beginning with a simpler solution that merely asks which files to replace and symlink when its run, I came up with a more elegant (stupid) solution:

#! /bin/bash
# Move all existing files for which a versioned file exists to a local branch
# and symlink to the versioned location. But make sure to stash the current
# branch, commit the local branch after you're done and then move back to the
# original branch and pop the stash back to where it was
#
function move_to_branch () {
VERSIONED_DIR=$1
TARGET_DIR=$2
cd $VERSIONED_DIR
FILES=`git ls-tree --name-only HEAD`
for FILE in $FILES; do
# if isn't already a symlink that points to the the versioned copy
if [ ! -h $TARGET_DIR/$FILE ] ||
[ ! `readlink -f $VERSIONED_DIR/$FILE` ==
`readlink -f $TARGET_DIR/$FILE` ]; then
#if it exists, remove the versioned copy in the local branch and replace with local file
if [ -e $TARGET_DIR/$FILE ]; then
rm -rf $VERSIONED_DIR/$FILE
mv $TARGET_DIR/$FILE $VERSIONED_DIR/$FILE
fi
ln --force --symbolic $VERSIONED_DIR/$FILE $TARGET_DIR/$FILE
fi
done
}


cd `dirname $0`/../
#echo $0
ROOTDIR=`pwd`

if [ -z `git branch | grep local` ]; then git branch local; fi

CURRENT_BRANCH=`$ROOTDIR/scripts/get_current_branch.sh`
git stash
git checkout local


move_to_branch $ROOTDIR/conf $HOME
if [ ! -e "$HOME/bin" ]; then mkdir $HOME/bin; fi
move_to_branch $ROOTDIR/bin $HOME/bin

git commit -a -m "Backing up local copy of config files"
git checkout $CURRENT_BRANCH
git stash pop
This script goes through the file that are version controlled in the conf or bin directories, and for each one, replaces the current file with a symlink to the version-controlled file. However, it also places a copy of the original file (if there was one) in a branch called local. Now of course, to avoid horribleness, it also records the current branch before stashing it, creates the local branch if it doesn't exist, and the returns to the original branch and pops the stash.

Why? It's a perfectly reasonable question, but what this allows you to do, is have no user interaction, which allows you to call it from post-merge:

$ cat scripts/post-action
#!/bin/sh
./scripts/link_files_branch.sh

Now, every time I pull in a new version, this script will automatically symlink any new files and commit a copy of the original (non-versioned) file in a local branch. This way, I can keep an up-to-date version of everything in master and any local changes in local. "You fool!" you might say, wondering what will happen when I eventually push/pull from the local branch. Well, yeah, that'll be annoying, but I can minimize that by setting default branches to push/pull to/from:

$ cat scripts/setup_repository.sh
#!/bin/bash

cd `dirname $0`

git config remote.origin.pull master
git config remote.origin.push master

#echo $0
SCRIPT_DIR=`pwd`
ROOT_DIR=$SCRIPT_DIR/../
cp $SCRIPT_DIR/post-action $ROOT_DIR.git/hooks/post-merge

Well, now that I've got that 5-minute possible future confusion averted I can go back to work.

No comments:

Post a Comment