#!/usr/bin/python
# ----------------------------------------------------------------
# Automates batch operations for svn and Java.  Only tested on Mac OS X and
# Linux (sliderule.csse); almost certainly won't work on Windows.
#
# Takes a single command line argument giving the name of the course for which
# the script is being used, like 'csse220'.  Uses that argument to find the
# meta-data regarding projects names, test cases, and repositories.
#
# Substitute the special course name 'testing' to use the course meta-data defined
# in the same subdirectory as this script.
#
# by Curt Clifton, December 2008-September 2009
# ----------------------------------------------------------------

# from __future__ import with_statement

from batchSVNTypes import *
import sys
import os

# Grab command-line arguments
# CONSIDER: may want to switch to the getopt package
if len(sys.argv) == 2:
    courseName = sys.argv[1].lower()
else:
    print "Usage: %s <courseName>" % (sys.argv[0])
    print "for example: %s csse220" % (sys.argv[0])
    sys.exit(1)

dataModuleName = "batchSVNDataRobotics"

# ----------------------------------------------------------------
# Data for use in menu item functions
dataBlob = DataBlob()
dataBlob.menuWidth = 72
dataBlob.rootURL = 'http://svn.csse.rose-hulman.edu/repos'
dataBlob.lockFileName = '.batchEclipseLock'
dataBlob.batchLogFileName = 'batchLogFile.txt'
dataBlob.scriptDir = os.path.dirname(__file__)
dataBlob.activeProject = Project("undefined")
dataBlob.workingDir = os.getcwd()
if courseName != "testing":
    # tweaks sys.path so import grabs right metadata
    courseDataPath = "/class/csse"
    courseDataPath += os.sep + courseName + os.sep + "scripts"
    print courseDataPath
    sys.path.insert(0, courseDataPath)
    if not os.access(courseDataPath + os.sep + dataModuleName + ".py",os.F_OK):
        print '*** No course data found for %s ***' % (courseName)
        sys.exit(1)
courseData = __import__(dataModuleName)
dataBlob.activeRepoSet = courseData.reposSets[0]


# ----------------------------------------------------------------
# A list of MenuItem objects representing the main menu
menuItems = []

# ----------------------------------------------------------------
# Creates a dictionary of menu items and functions to execute:
menuItems.append(MenuSeparator('SVN:'))

def checkout(data):
    forEachRepo('svn checkout ${rootURL}/${repo}/${proj} ${shortName}', data,
                create=True)
    return False
menuItems.append(MenuItem('Checkout from SVN',checkout))
    
def update(data):
    forEachRepo('svn update ${shortName}', data)
    return False
menuItems.append(MenuItem('Update from SVN',update))
    
def commit(data):
    if os.access(data.activeProject.name + '/' + data.lockFileName,os.F_OK):
        print '\a\n*** Remove Eclipse metadata before committing. ***\n'
        return False
    message = raw_input('Enter the commit message: ').strip().replace("'", '"')
    forEachRepo('svn commit ${shortName} -m ${msg}', data, msg=message)
    return False
menuItems.append(MenuItem('Commit to SVN',commit))

def checkLog(data):
    forEachRepo('svn log --limit 5 ${shortName}', data)
    return False
menuItems.append(MenuItem('Get data on last 5 commits',checkLog))
    
def generateCommitTimeList(data):
    forEachRepo('svn info ${shortName} | tail -n 4 | head -n 3', data, 
                 withCapture=True, suppressOutput=True)
    # Processes batchLogFileName to create sorted list
    # Output format:
    # <shortName>: committed by <committer> on <commitDate>
    #  - or -
    # <shortName>: unexpected data format
    results = [] # list of pairs of (commitDate or None, formatted output line)
    unexpected = True
    shortName = 'unknown'
    f = open(data.batchLogFilename, 'r')
    for line in f:
        # Extracts shortName and svn info from batch file line
        line = line.strip()
        spl = line.split(':',1)
        if len(spl) == 2:
            name, rest = spl
            rest = rest.lstrip()
        else:
            # Handles unexpected data (poorly)
            name = spl[0]
            rest = spl[0]
                
        # Handles possibilities for rest
        if rest.startswith('Last Changed Author: '):
            # Start of block
            unexpected = False
            shortName = name
            committer = rest[21:]
            commitDate = 'unknown'
        elif rest.startswith('Last Changed Date: '):
            # End of block
            if name == shortName:
                commitDate = rest[19:]
                outLine = '%s: committed by %s on %s' % (shortName, 
                                                             committer, 
                                                             commitDate)
                results.append((commitDate, outLine))
            else:
                unexpected = True
        elif rest.startswith('Last Changed Rev: '):
            # Ignored data
            if name != shortName:
                unexpected = True
        else:
            # Unexpected data
            unexpected = True
            
        # Records unexpected data as soon as it's detected
        if unexpected:
            results.append((None, 
                            '%s: unexpected data format' % (shortName)))
            unexpected = False
            shortName = 'unknown'
                
    # Sorts and prints results
    results.sort(key = lambda x: x[0])
    print "\nMost recent commit info, in chronological order:"
    for k,v in results:
        print v
    print '\n'
    return False
menuItems.append(MenuItem('Generate list of commits, sorted by commit time',generateCommitTimeList))

menuItems.append(MenuSeparator('Java:'))

def compile(data):
    forEachRepo("mkdir ${shortName}/bin", data)
    forEachRepo("javac -cp '${scriptDir}/junit-4.5.jar:${shortName}/bin' -d '${shortName}/bin' `find ${shortName} -type f -name '*.java'`", data)
    return False
menuItems.append(MenuItem('Compile student code',compile))    
    
def runTests(data):
    forEachRepo("cd ${shortName} && java -cp '${scriptDir}/junit-4.5.jar:bin' org.junit.runner.JUnitCore ${testClasses}", data, withCapture=True)
    return False
menuItems.append(MenuItem('Run JUnit tests',runTests))

menuItems.append(MenuSeparator('Eclipse:'))

def convertToEclipse(data):
    # Change project names to short names by editing the Eclipse metadata
    forEachRepo("sed -i~ 's:<name>${proj}</name>:<name>${shortName}</name>:' '${shortName}/.project'", data)
    # Add lock file to block commits
    lockFile = data.activeProject.name + '/' + data.lockFileName
    if os.access(data.activeProject.name,os.F_OK):
        os.system('touch %s' % (lockFile))
    if not os.access(lockFile, os.F_OK):
        print '\a\n*** DANGER: Unable to create lock file.  Do NOT commit! ***\n'
    else:
        print """\a\a\a
        In Eclipse, use Import to batch import all of the student
        projects. Be sure to NOT copy the projects into your workspace
        or you won't be able to add comments to their code.
        
        WARNING!!! DO NOT COMMIT TO SVN FROM WITHIN ECLIPSE.  YOU COULD
        WEDGE THE STUDENT'S VERSION OF THEIR PROJECT.
        """
    return False
menuItems.append(MenuItem('Add Eclipse metadata (allows importing all projects into Eclipse)',convertToEclipse))

def convertFromEclipse(data):
    # Revert project names within .project files
    forEachRepo("sed -i~ 's:<name>${shortName}</name>:<name>${proj}</name>:' '${shortName}/.project'", data)
    # Remove lock file to enable commits
    lockFile = data.activeProject.name + '/' + data.lockFileName
    if os.access(lockFile,os.F_OK):
        os.system('rm -f %s' % (lockFile))
        if os.access(lockFile,os.F_OK):
            print '\a\n*** Unable to remove lock file: %s ***\n' % (data.lockFileName)
            return False
    return False
menuItems.append(MenuItem('Remove added Eclipse metadata (required before committing)',convertFromEclipse))

menuItems.append(MenuSeparator('Utility:'))

def deleteProjects(data):
    if os.access(data.activeProject.name,os.F_OK):
        os.system('rm -Rf %s' % (data.activeProject.name))
        if os.access(data.activeProject.name,os.F_OK):
            print '\a\n*** Unable to delete local project subdirectory ***\n'
    return False
menuItems.append(MenuItem('Delete local project subdirectory (and your copies of student projects)',deleteProjects))
    
def setProjectNameFromInput(data):
    projectPrompt = 'Enter the Eclipse project name (or number from list above): '
    projectNames = [p.name for p in courseData.projects]
    n, projectName = displayMenu(projectPrompt, projectNames, allowOthers=True)
    if n == None:
        if projectName in projectNames:
            data.activeProject = courseData.projects[projectNames.index(projectName)]
        else:
            data.activeProject = Project(projectName)
            print '\a\n*** No unit tests listed for project "%s" ***\n' % (projectName)
    else:
        data.activeProject = courseData.projects[n]
    return False
menuItems.append(MenuItem('Switch to a different project',setProjectNameFromInput))

def changeActiveRepoSet(data):
    repoList = [rs.name for rs in courseData.reposSets]
    prompt = "Enter number of repository set in which you are interested: "
    n, temp = displayMenu(prompt, repoList)
    data.activeRepoSet = courseData.reposSets[n]
    return False
menuItems.append(MenuItem('Change to a different set of repositories',changeActiveRepoSet))

def zipForDownload(data):
    if os.access(data.activeProject.name,os.F_OK):
        os.system('zip -r %s.zip %s' % (data.activeProject.name,
                                        data.activeProject.name))
        if not os.access(data.activeProject.name + '.zip',os.F_OK):
            print '\a\n*** Unable to create zip file ***\n'
    else:
        print '\a\n*** Unable to access local project subdirectory ***\n'
    return False
menuItems.append(MenuItem('Zip local project subdirectory for download',
                          zipForDownload))
def unzipForUpload(data):
    if os.access(data.activeProject.name,os.F_OK):
        print '\a\n*** Attempting to replace local copy of student work with uploaded zip.  Delete the local copy first. ***\n'
    else:
        os.system('unzip %s.zip' % (data.activeProject.name))
        if not os.access(data.activeProject.name,os.F_OK):
            print '\a\n*** Unzip attempt failed to create local copy of student work. ***\n'
    return False
menuItems.append(MenuItem('Unzip uploaded student work',
                          unzipForUpload))

menuItems.append(MenuSeparator())

# ----------------------------------------------------------------
# Brings in menu items from course.
# ----------------------------------------------------------------
if len(courseData.menuItems) > 0:
    menuItems.extend(courseData.menuItems)
    menuItems.append(MenuSeparator())

# ----------------------------------------------------------------
# Add the Quit menu item at the end.
# ----------------------------------------------------------------
def exitProgram(data):
    print "Buh-bye.  <waves />"
    return True
menuItems.append(MenuItem('Quit',exitProgram))

# ----------------------------------------------------------------
# Display prompt for project name
print """
Welcome to the CSSE batch SVN and JUnit script.
by Curt Clifton, Dec. 2008-Sep. 2009
"""
# Forces a project selection to begin with:
setProjectNameFromInput(dataBlob)

# Displays the main menu:
done = False
while not done:
    print '-' * dataBlob.menuWidth
    print 'Course:      ', courseData.courseDescription
    print 'Project Name:', dataBlob.activeProject.name
    print 'Working Dir.:', truncateMiddle(dataBlob.workingDir, dataBlob.menuWidth - 14)
    print 'Repositories:', dataBlob.activeRepoSet.name
    print 'Test Classes:', truncateMiddle(' '.join(dataBlob.activeProject.testClasses), 
                                          dataBlob.menuWidth - 14)
    print '-' * dataBlob.menuWidth
    
    n, item = displayMenu('\nEnter the number of your choice: ', menuItems)
    done = item.function(dataBlob)