yet more effort toward keeping config out of git
03/01/2011
It seems that I spend most of my waking life concerned about keeping usernames and passwords out of version control. It causes at least 117% of the merge/rebase conflicts that I come across every day, and I'm getting pretty tired of it, to tell you the truth.
My latest genius idea involves the much-lauded pre-commit hook in git. To save your mouse-clickin' finger; the pre-commit hook is executed before git-commit does anything dangerous, and if you exit non-zero, it'll stop the commit. That's pretty useful in my book.
So, here's the idea. You do some work involving something that needs a config file. You commit it with blank config. Lovely. Now, you add your configuration to the files that need it, but don't commit. Instead, format a patch of the changes to your working copy. I use the script below:
1 #!/bin/bash 2 if [ $# -ne 1 ]; then 3 echo "Supply a patch name." 4 exit 5 fi 6 git diff --no-prefix --no-ext-diff > $@
I save my patch to the following location: ~/Patches/repo.directory.branchname.patch, then install this pre-commit hook:
1 #!/usr/bin/env ruby 2 3 dir = File.expand_path( File.dirname('.') ) 4 repo = dir.split('/').last 5 branch = `git branch | grep '^*'`.to_s.gsub(/^\*/ , '').strip 6 patchname = "#{repo}.#{branch}.patch" 7 patchdir = File.expand_path( '~/Patches' ) 8 patchpath = "#{patchdir}/#{patchname}" 9 10 if File.exists? patchpath 11 # try to dry-run removing the patch - if it spits shit out that doesn't 12 # contain the word "FAILED", means the patch was already removed so we 13 # can continue 14 # 15 # if the patch runs silently or contains the word "FAILED" it means 16 # something's up 17 dry = `patch -s --no-backup --dry-run -p0 -R < "#{patchpath}"`.to_s.strip 18 if dry == '' 19 puts 'There\'s a patch that you should remove. Remove it then commit.' 20 puts "Patch File: #{patchpath}" 21 exit 1 22 elsif dry.index( 'FAILED' ) != nil 23 puts 'A broken patch was found. Fix it before proceeding.' 24 puts "Patch File: #{patchpath}" 25 exit 1 26 end 27 end 28 exit 0
This basically dry-runs the removal of the appropriate patch. If the patch would exit without complaint, that means that the patch could be applied and what you're committing contains configuration, so the commit fails. If the output of the dry-run contains the word "FAILED", that means that the patch is invalid, so we err on the side of caution and fail. If the patch is noisy, but doesn't contain the word fail, we can safely assume that the patch has already been removed and we're OK to continue exiting.
I'm sure that next week, there'll be a totally new, genius way to solve this problem. For now, however, this is working really well.
For ease, I have the following git commands, also:
git-apatch
1 #!/usr/bin/env ruby 2 3 dir = File.expandpath(</span><span class="Constant">git rev-parse --show-toplevel</span><span class="Delimiter">
) 4 repo = dir.split('/').last.strip 5 branch =</span><span class="Constant">git branch | grep '^*'</span><span class="Delimiter">
.tos.gsub(/^*/ , '').strip 6 patchname = "#{repo}.#{branch}.patch" 7 patchdir = File.expand_path( '~/Patches' ) 8 patchpath = "#{patchdir}/#{patchname}" 9 10 if File.exists? patchpath 11 puts</span><span class="Constant">patch -p0 < "</span><span class="Delimiter">#{</span>patchpath<span class="Delimiter">}</span><span class="Constant">"</span><span class="Delimiter">
12 else 13 puts "No patch in #{patchpath}" 14 end
git-rmpatch
1 #!/usr/bin/env ruby 2 3 dir = File.expandpath(</span><span class="Constant">git rev-parse --show-toplevel</span><span class="Delimiter">
) 4 repo = dir.split('/').last.strip 5 branch =</span><span class="Constant">git branch | grep '^*'</span><span class="Delimiter">
.tos.gsub(/^*/ , '').strip 6 patchname = "#{repo}.#{branch}.patch" 7 patchdir = File.expand_path( '~/Patches' ) 8 patchpath = "#{patchdir}/#{patchname}" 9 10 if File.exists? patchpath 11 puts</span><span class="Constant">patch -p0 -R < "</span><span class="Delimiter">#{</span>patchpath<span class="Delimiter">}</span><span class="Constant">"</span><span class="Delimiter">
12 else 13 puts "No patch in #{patchpath}" 14 end
All these do is look for a patch to be replied or moved for the current repo/branch and apply or remove it, failing noisily if the patch doesn't exist. I realise they share some code that could be extracted, but for the sake of easy display, this is what the scripts basically contain.
The only thing I think that could make this more useful would be to detect which files the patch would modify, apply the patch and unstage those files to allow the commit to proceed, thus causing fewer interruptions. That's a nice-to-have, though. Absent-minded as I am, this has already saved me from committing config twice today!
If you've got a different approach that works well for you, I'd love to hear about it on the twitters