#!/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 " % (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: # : committed by on # - or - # : 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:${proj}:${shortName}:' '${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:${shortName}:${proj}:' '${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. " 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)