Time Machine: poor man’s version control

Tags:

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 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 sys
import os
import getopt
import difflib
import time
import pdb
from 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 backups

time_machine_path = None

cmd = '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">&nbsp;Added&nbsp;</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 = msg

def 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 2


if __name__ == "__main__":
    sys.exit(main())

1 comment so far ↓

#1 joshuadf on 04.24.09 at 1:08 pm

I really like the idea of this. Could post the code as an attachment or in a pre section so it’s easier to paste?

If you don’t mind it would also be nice to have an open source or CC license instead of “All rights reserved.”

Leave a Comment