Skip to content
This repository has been archived by the owner on Sep 29, 2021. It is now read-only.
ldmosquera edited this page Oct 26, 2012 · 14 revisions

Here's how I use git to version my dotfiles.

Note: I made this repo from scratch to translate comments to English and tidy up things so it could hope to be useful to others.
Here's a subset of my actual, messy, Spanish ridden repo: https://github.com/ldmosquera/actual-dotfiles

Set up

I use many different VMs and workstations. Whenever I set up a new box, I do:

cd $HOME
git init
git remote add origin $REPO  #I host my repo in my home server
git fetch
git reset --hard origin/platforms/linux
echo 'source $HOME/.bash_user_config' >> .bashrc

So I treat my homedir itself as my working directory; .git will be in $HOME/.git.

.gitignore file

I use .gitignore to whitelist specific dotfiles and dirs that I actually want to track, and ignore the rest:

#ignore everything
/*
#but whitelist these:
!/.gitmodules     #used by git submodule
!/bin             #scripts to be included in PATH
!/.profile.d      #all files here are sourced by bash
!/.vim            #my beloved editor's dotdir
.vim/.netrwhist   #do ignore this; created when you navigate directories in vim; 
.vim/doc/tags     #do ignore this; automatically maintained by ':helptags ~/.vim/doc'

To start tracking a dotfile, just add it with git add -f and commit.
When you create new files in whitelisted dirs, they show up as new files in git status, so you won't forget to commit them.
You can turn certain subdirectories into submodules, to isolate commits to things that change a lot. Everything else is just ignored, so clean git status output means your homedir is clean.

My bash profile:

All my bash customizations go in a separate file, so I don't have to commit the rest of .bashrc, which differs a lot between every Linux distribution, Cygwin, OSX, and so on.
The important bits are these:

#put $HOME/bin and its subdirectories in PATH (ignores */.git/* so you can safely throw submodules in here):
if [[ -d $HOME/bin ]]; then
    currentIFS="$IFS"; IFS=$'\n'
    for d in $(find $HOME/bin -type d -a \! -wholename '*/.git/*'); do
        PATH="$d:$PATH"
    done
    IFS="$currentIFS"; unset currentIFS
fi

#source any file in here:
partsDir="$HOME/.profile.d"
if [[ -d "$partsDir" ]]; then
    for f in "$partsDir"/*; do
        source "$f"
    done
fi

Branches for each computer/VM/installation

If you use very few machines or only one OS, then you can use just one branch with what I've described so far.
But if you frequently use many different boxes and platforms, each with subtle differences between them (different dotfile contents, etc), you'd probably like a versioning scheme that's handy and scales in boxes vs. effort.

I have a separate branch for each box, where I commit stuff that's local to that box.
For example, if I'm at work and I come up a useful alias, I commit it and push it into my central repo; then I can cherry-pick it into or from any of my other boxes.

If each box branch had all commits, then each branch would have a lot of common commits plus their unique stuff.
After a while, gitk's tree view would become unusable (in a UX sense): common commits repeated everywhere and juicy unique commits buried in between.

To solve this, I factor common stuff hierarchically into parent branches:

platforms/Unix
├── platforms/Cygwin
│    ├── hosts/CygwinBox1
│    ├── hosts/CygwinBox2
│    └── ...
├── platforms/Linux
│    ├── hosts/LinuxBox1
│    ├── hosts/LinuxBox2
│    └── ...
└── platforms/OSX
     ├── hosts/OSXBox1
     ├── hosts/OSXBox2
     └── ...

So the base stuff (.gitignore, .bash_user_config, POSIX compliant scripts and aliases) goes into a branch called platforms/Unix.
Other platforms branch out (and thus "inherit") from Unix, and add commits with stuff specific to them but incompatible or irrelevant in other platforms.
Finally, individual boxes branch out from their specific platform and add their local stuff; I factor as much as possible up into platforms, to keep box branches short (or even non-existant, some boxes can just use the platform remote branches).

As I commit stuff and cherry-pick it between boxes as needed, branches start to diverge and get messy again.
So from time to time I "garbage collect": I move commits from branches back to a suitable parent (either Unix or their specific platform), and then I rebase all branches into their updated parents. The next time I stop by one of my boxes, I pull to get the latest changes.
This is why I have a .profile.d directory for bash definitions; so I can move newer commits "in front" of old commits with no risk of stupid merge conflicts in independent bash declarations (since order between them doesn't matter semantically).

I usually keep old "ancestors" around, with names like platforms/Unix_2012_05 (as in, superseded in May 2012); they don't get in the way becase I never commit into them again, so they stay way down in gitk's tree view, but they're right there if you want to browse or search for old stuff.

All this reshuffling is tedious to do by hand, but it can trivially be scripted. It shouldn't be too hard to completely automate it, but of course it would require intervention in case of conflicts.
I've pondered better schemes that don't require this maintenance, but this is the best I've come up with so far.

Pros:

  • simple concept; use plain git to version your dotfiles in-place
  • straight-forward set up of new boxes; no symlink sorcery required
  • all your dotfiles EVER from all your boxes EVER, neatly arranged in one repo; cherry-pick them around, globally grep them, anything.
  • a Unix Beard materializes in your face. Admit it, you always wanted one.

Cons:

  • periodic pruning required; effort proportional to amount of branches
  • requires a bit of thought, versus just having a single branch. This repo should be a useful template though.
Clone this wiki locally