Time Machine: poor man's version control
There are a has been a number of version control systems en vogue over time, CVS, SVN, Git etc.
I try to keep up with them, use them where possible, but don't put EVERYTHING I do in version control. Since I am on a Mac running Leopard, I do have, and use [Time Machine](http://arstechnica.com/apple/reviews/2007/10/mac-os-x-10-5.ars/14) and so wanted to see if it would be pretty easy to use that to do some quick diffs between some source files.
The result is a quick and dirty python script. In the unlikely event that I ever have time, this would be a cool pyObjC project, a file browser panel, date versions picker, and a webkit view (with some better css).
The source to the script is below the fold - it will look for the first attached volume that has time machine backups for the current machine. It will not work with network based time machine backups that are on disk images. This script will run on a stock Leopard install without any extra python modules needed.
#!/usr/bin/env python# encoding: utf-8"""Created by Preston Holmes on 2009-02-23.Copyright (c) 2009 __MyCompanyName__. All rights reserved."""import sysimport osimport getoptimport difflibimport timeimport pdbfrom subprocess import Popen, PIPE# if you set time_machine_path, it should be to the full path of this machines backup drive:# ie '/Volumes/TM_Drive/Backups.backupsdb/Joes-Mac/'# if not set explicitly - the script will use the first TM drive it finds, # and the first host folder it finds - which should work for most cases# Will Not work with network or disk image based time machine backupstime_machine_path = Nonecmd = 'tell application "Finder" to name of (path to startup disk)'boot_volume = Popen('osascript -e \'%s\'' % cmd,shell=True,stdout=PIPE,stderr=PIPE).communicate()[0][0:-1]help_message = '''Call this script with the path to one or more text based files as arguments'''header = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> <title></title> <style type="text/css"> table.diff {font-family:Courier; border:medium;} .diff_header {background-color:#e0e0e0} td.diff_header {text-align:right} .diff_next {background-color:#c0c0c0} .diff_add {background-color:#aaffaa} .diff_chg {background-color:#ffff77} .diff_sub {background-color:#ffaaaa} </style></head><body>'''footer = ''' <table class="diff" summary="Legends"> <tr> <th colspan="2"> Legends </th> </tr> <tr> <td> <table border="" summary="Colors"> <tr><th> Colors </th> </tr> <tr><td class="diff_add"> Added </td></tr> <tr><td class="diff_chg">Changed</td> </tr> <tr><td class="diff_sub">Deleted</td> </tr> </table></td> <td> <table border="" summary="Links"> <tr><th colspan="2"> Links </th> </tr> <tr><td>(f)irst change</td> </tr> <tr><td>(n)ext change</td> </tr> <tr><td>(t)op</td> </tr> </table></td> </tr> </table></body></html>'''class Usage(Exception): def __init__(self, msg): self.msg = msgdef find_versions(path): #pdb.set_trace() print 'looking for versions of %s' % path #print time.strftime("%m/%d/%Y %I:%M:%S %p",time.localtime(os.path.getmtime(fname))) backups = os.listdir(time_machine_path) backups.sort() pathlist = [os.path.join(time_machine_path,b,boot_volume,path[1:]) for b in backups] pathlist.append(path) versions = [] mod_times = [] for f in pathlist: if os.path.exists(f): mod_time = time.localtime(os.path.getmtime(f)) if not mod_time in mod_times: mod_times.append(mod_time) versions.append({mod_time:f}) return versions def getTMLocation(): global time_machine_path if time_machine_path and os.path.exists(time_machine_path): return True volumes = os.listdir('/Volumes') #hostname = os.uname()[1].split('.')[0] #cmd = 'scutil --get ComputerName' #machine_name = Popen('osascript -e \'%s\'' % cmd,shell=True,stdout=PIPE,stderr=PIPE).communicate()[0][0:-1] for v in volumes: if os.path.exists(os.path.join('/Volumes',v,'Backups.backupdb')): backupsdb = (os.path.join('/Volumes',v,'Backups.backupdb')) time_machine_path = os.path.join(backupsdb,os.listdir(backupsdb)[0]) return True # candidate_path = os.path.join('/Volumes',v,'Backups.backupdb',machine_name) # if os.path.exists(candidate_path): # time_machine_path = candidate_path # return True return False def main(argv=None): if not getTMLocation(): print 'No Time Macine Backup Found' sys.exit() print 'time machine path: ' + time_machine_path if argv is None: argv = sys.argv try: try: opts, args = getopt.getopt(argv[1:], "ho:v", ["help", "output="]) except getopt.error, msg: raise Usage(msg) # option processing for option, value in opts: if option == "-v": verbose = True if option in ("-h", "–help"): raise Usage(help_message) if option in ("-o", "–output"): output = value if not args: raise Usage(help_message) differ = difflib.HtmlDiff(tabsize=4) html = header for path in args: path = os.path.join(os.getcwd(),path) html += '<h1>Changes for %s</h1' % os.path.basename(path) versions = find_versions(path) if len(versions) < 2: html += '<h2>Less than 2 Versions found on Time Machine Backup</h2>' else: html += '<h2>%s versions found</h2>' % len(versions) for i in range(0,len(versions)): if i: #skip the first d1 = time.strftime("%m/%d/%Y %I:%M:%S %p",versions[i-1].keys()[0]) d2 = time.strftime("%m/%d/%Y %I:%M:%S %p",versions[i].keys()[0]) html = html + '<h2>changes from %s to %s</h2>' % (d1,d2) l1 = open(versions[i-1].values()[0]).readlines() l2 = open(versions[i].values()[0]).readlines() table = differ.make_table(l1,l2,context=True,numlines=3) html += table html += footer o = open('/tmp/diff.html','w') o.write(html) o.close() import webbrowser webbrowser.open('/tmp/diff.html') except Usage, err: print >> sys.stderr, sys.argv[0].split("/")[-1] + ": " + str(err.msg) print >> sys.stderr, "\t for help use –help" return 2if __name__ == "__main__": sys.exit(main())