tadhg.com
tadhg.com
 

Python Script for Subversion Status

23:48 Sun 02 Aug 2009. Updated: 00:54 03 Aug 2009
[, , , , ]

I find in my use of Subversion that I often want to see a side-by-side list of files that aren’t under version control and files that have some other status. I also want these lists to be sorted alphabetically. Naturally, I ended up writing a Python script for this.

I suspect it could be cleaner, but it works and I’m trying to keep in mind that “the perfect is the enemy of the good”.

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Displays added and modified files in two columns (terminal space permitting)

import subprocess
import re

class SubversionSortedStatus(object):

    STATUS_NEW = "svn st | grep '^\?'" #check for new files
    STATUS_MODIFIED =  "svn st | grep -v '^\?'" #check for modified files

    def __init__(self):
        newfiles = self.get_new_files()
        newfiles_width = self.get_filename_width(newfiles)
        modfiles = self.get_modified_files()
        modfiles_width = self.get_filename_width(modfiles)
        terminal_width = self.terminal_size()[0]
        terminal_midpoint = self.get_midpoint(terminal_width, newfiles_width, modfiles_width)
        self.print_status(terminal_midpoint, newfiles, modfiles)

    def get_new_files(self):
        #the shell command:
        files = self.exec_shell_command(self.STATUS_NEW, stdout=subprocess.PIPE)

        #get rid of the status character:
        status_column = re.compile(r"^\?( +)(?=[^ ]*)")
        lines = files.split("\n")
        lines.sort()
        replaced_lines = []
        for line in lines:
            line = status_column.sub("", line)
            replaced_lines.append(line)
        files = "\n".join(replaced_lines)

        return files.strip()

    def get_modified_files(self):
        #the shell command:
        files = self.exec_shell_command(self.STATUS_MODIFIED, stdout=subprocess.PIPE)

        lines = files.split("\n")
        #sort using only the non-status columns:
        lines.sort(cmp = lambda x, y: cmp(x[6:], y[6:]))
        files = "\n".join(lines)

        return files.strip()

    def get_filename_width(self, files):
        width = 0
        lines = files.split("\n")
        for line in lines:
            if len(line) > width:
                width = len(line)
        return width

    def get_midpoint(self, width, newfiles_width, modfiles_width):
        if width <= newfiles_width + modfiles_width + 1:
            return 0
        else:
            return newfiles_width + 1

    def print_status(self, midpoint, newfiles, modfiles):
        if midpoint:
            newlines = newfiles.split("\n")
            modlines = modfiles.split("\n")
            if len(newlines) > len(modlines):
                maxlength = len(newlines)
            else:
                maxlength = len(modlines)
            for i in range(0, maxlength):
                if i < len(newlines):
                    newfile = newlines[i]
                else:
                    newfile = ""
                if i < len(modlines):
                    modfile = modlines[i]
                else:
                    modfile = ""
                separator = " " * ((midpoint - len(newfile)) + 1)
                print "%s%s%s" % (newfile, separator, modfile)
        else:
            print newfiles
            print "-----"
            print modfiles

    def exec_shell_command(self, command, stdout=None, wait=None, test_mode=False):
        if test_mode:
            print command
            return command

        output = ""
        if stdout:
            commandprocess = subprocess.Popen([command], shell=True, stdout=stdout)
            output = commandprocess.communicate()[0]
        else:
            commandprocess = subprocess.Popen([command], shell=True)
            commandprocess.wait()

        if wait:
            import time
            time.sleep(wait)

        return output

    """
    The next two functions are from Chuck Blake's categorized ls, found at http://pdos.csail.mit.edu/~cblake/cls/cls.py

    """
    def ioctl_GWINSZ(self, fd):                  #### TABULATION FUNCTIONS
        try:                                ### Discover terminal width
            import fcntl, termios, struct, os
            cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
        except:
            return None
        return cr

    def terminal_size(self):                    ### decide on *some* terminal size
        import os
        env = os.environ
        cr = self.ioctl_GWINSZ(0) or self.ioctl_GWINSZ(1) or self.ioctl_GWINSZ(2)  # try open fds
        if not cr:                                                  # ...then ctty
            try:
                fd = os.open(os.ctermid(), os.O_RDONLY)
                cr = self.ioctl_GWINSZ(fd)
                os.close(fd)
            except:
                pass
        if not cr:                            # env vars or finally defaults
            try:
                cr = (env['LINES'], env['COLUMNS'])
            except:
                cr = (25, 80)
        return int(cr[1]), int(cr[0])         # reverse rows, cols

if __name__ == '__main__':
    svnstatus = SubversionSortedStatus()

Leave a Reply