#!/usr/local/bin/python # # Perforce review daemon # # This script determines new changelists and jobs and emails interested users. # Users express interest in reviewing changes and/or jobs by setting the # "Reviews:" entry on their user form. Users are notified of changes if they # review any file involved in that change. Users are notified of jobs if # they review //depot/jobs (configurable - see jobpath, below). # NOTE: the job review function requires a 98.2 or later server. # # If run directly with "repeat=1" (see below) the script will sleep for # "sleeptime" seconds and then run again. On UNIX you can run the script from # cron by setting "repeat=0" and adding the following line with crontab -e: # * * * * * /path/to/p4review.py # This will run the script every minute. Note that if you use cron you # should be sure that the script will complete within the time allotted. # # See the CONFIGURATION VARIABLES section below for other values which may # need to be customized for your site. In particular, be sure to set # administrator, mailhost, repeat, p4, P4PORT and P4USER. # # # Common pitfalls and debugging tips # - "command not found" (Windows) or "name: not found" (UNIX) errors # -> check that "p4" is on your PATH or set # p4='"c:/program files/perforce/p4"' or as appropriate (Windows) # (NOTE the use of " inside the string to prevent interpretation # of the command as "run c:/program with arguments files/perforce/p4...) # p4='/usr/local/bin/p4' or as appropriate (UNIX) # - "You don't have permission for this operation" # -> check that the user you set os.environ['P4USER'] to (see below) # has "review" or "super" permission (use "p4 protect") # You should be able to run "p4 -u username review -c 42 -t test" # - this sets the value of a counter named "test" to 42 # - "Unable to connect to SMTP host" # -> check that the mailhost is set correctly - try "telnet mailhost 25" # and see if you connect to an SMTP server. Type "quit" to exit # - it seems to run but you don't get email # -> check the output of "p4 counters" - you should see a counter named # "review" # -> check the output of "p4 reviews -c changenum" for a recent change; # if no one is reviewing the change then no email will be sent. # Check the setting of "Reviews:" on the user form with "p4 user" # -> check that your email address is set correctly on the user form # (run "p4 reviews" to see email addresses for all reviewers, # run "p4 user" to set email address) # - multiple job notifications are sent # -> the script should be run on the same machine as the Perforce server; # otherwise time differences between the two machines can cause problems # (the job review mechanism uses a timestamp; the change review mechanism # uses change numbers, so it's not affected by this problem) import sys, os, string, re, time, smtplib # CONFIGURATION VARIABLES notify_changes = 1 # set to 0 to disable change notification completely notify_jobs = 0 # set to 0 to disable job notification completely bcc_admin = 1 # set to 0 to disable Bcc: of all email to administrator send_to_author = 0 # set to 1 to have mail sent to author of change or job reply_to_admin = 0 # set to 1 to enable Reply-To: administrator administrator = None # If you set this to an email address then you will be # notified of problems with the script (e.g. invalid # email addresses for users) and, if bcc_admin is set, # you will get a copy of all email the script generates mailhost = 'localhost' # set to hostname of machine running an SMTP server p4='p4' # set to path of p4 executable, or just 'p4' if the # executable is on your path (use forward slashes even # on Windows! Backslash has a special meaning in Python) repeat = 0 # set to 0 to run just once (do this if running from # cron!), set to 1 to repeat every sleeptime seconds sleeptime = 60 # number of seconds to sleep between invocations # (irrelevant if repeat == 0) limit_emails = 10 # don't send more than this many emails of each type # (job and change) at a time datefield = 'Date' # field used to determine job updates # ***Currently you must set this to a field which has the # last modified date rather than creation date (which # means you'll be notified of changed jobs as well as # new ones), since Perforce sets the creation date when # the editor is launched, not when the job is stored. # (Hopefully this will be fixed in future). jobpath = '//depot/jobs' # send job review mail to users reviewing jobpath os.environ['P4PORT'] = 'perforce:1666' os.environ['P4USER'] = 'james' # user must have Perforce review privileges # END OF CONFIGURATION VARIABLES bcc_admin = bcc_admin and administrator # don't Bcc: None! if administrator and reply_to_admin: replyto_line='Reply-To: '+administrator+'\n' else: replyto_line='' def complain(mailport,complaint): '''Send a plaintive message to the human looking after this script if we have any difficulties. If no email address for such a human is given, send the complaint to stderr. ''' complaint = complaint + '\n' if administrator: mailport.sendmail('PerforceReviewDaemon',[administrator],\ 'Subject: Perforce Review Daemon Problem\n\n' + complaint) else: sys.stderr.write(complaint) def mailit(mailport, sender, recipients, message): '''Try to mail message from sender to list of recipients using SMTP object mailport. complain() if there are any problems. ''' try: failed = mailport.sendmail(sender, recipients, message) except: failed = 'Exception ' + repr(sys.exc_info()[0]) + ' raised.' if failed: complain( mailport, 'The following errors\n' +\ repr(failed) +\ '\noccurred while trying to email from\n' + repr(sender) + '\nto ' +\ repr(recipients) + '\nwith body\n\n' + message) def parse_p4_review(command,ignore_author=None): reviewers_email = [] reviewers_email_and_fullname = [] for line in os.popen(command,'r').readlines(): # sample line: james (James Strickland) # user email fullname (user,email,fullname) = \ re.match( r'^(\S+) <(\S+)> \((.+)\)$', line).groups() if user != ignore_author: reviewers_email.append(email) reviewers_email_and_fullname.append('"%s" <%s>' % (fullname,email)) return reviewers_email,reviewers_email_and_fullname def change_reviewers(change,ignore_author=None): '''For a given change number (given as a string!), return list of reviewers' email addresses, plus a list of email addresses + full names. If ignore_author is given then the given user will not be included in the lists. ''' return parse_p4_review(p4 + ' reviews -c ' + change,ignore_author) def review_changes(mailport,limit_emails=100): '''For each change which hasn't been reviewed yet send email to users interested in reviewing the change. Update the "review" counter to reflect the last change reviewed. Note that the number of emails sent is limited by the variable "limit_emails" ''' change = None for line in os.popen(p4 + ' review -t review','r').readlines(): # sample line: Change 119424 james (James Strickland) # change author email fullname (change,author,email,fullname) = \ re.match( r'^Change (\d+) (\S+) <(\S+)> \(([^\)]+)\)', line).groups() if send_to_author: (recipients,recipients_with_fullnames) = change_reviewers(change) else: (recipients,recipients_with_fullnames) = change_reviewers(change,author) if bcc_admin: recipients.append(administrator) if not recipients: continue # no one is interested message = 'From: ' + fullname + ' <' + email + '>\n' +\ 'To: ' + string.join(recipients_with_fullnames,', ') + '\n' +\ 'Subject: PERFORCE change ' + change + ' for review\n' +\ replyto_line +\ '\n' +\ os.popen(p4 + ' describe -s ' + change,'r').read() mailit(mailport, email, recipients, message) limit_emails = limit_emails - 1 if limit_emails <= 0: complain( mailport, 'email limit exceeded in job review - extra jobs dropped!') break # if there were change(s) reviewed in the above loop, update the counter if change: # NOTE: the use of "p4 review -c" is for backwards compatibility with # pre-99.1 servers; with 99.1 or later servers use "p4 counter" if os.system(p4 + ' review -c ' + change + ' -t review') !=0: complain(mailport,'Unable to set review counter - check user "' +\ os.environ['P4USER'] + '" has review privileges\n(use p4 protect)') def job_reviewers(jobname,ignore_author=None): '''For a given job, return list of reviewers' email addresses, plus a list of email addresses + full names. If ignore_author is given then the given user will not be included in the lists. ''' return parse_p4_review(p4 + ' reviews ' + jobpath,ignore_author) # not the most efficient solution... def review_jobs(mailport,limit_emails=100): '''For each job which hasn't been reviewed yet send email to users interested in reviewing the job. Update the "jobreview" counter to reflect the last time this function was evaluated. Note that the number of emails sent is limited by the variable "limit_emails" - ***currently this causes extra job notifications to be dropped...not optimal... ''' start_time = 0 for line in os.popen(p4 + ' counters').readlines(): if line[:len('jobreview')] == 'jobreview': start_time = int(line[len('jobreview')+3:]) query_time = int(time.time()) query = datefield + '>' +\ time.strftime('%Y/%m/%d:%H:%M:%S',time.localtime(start_time)) + '&' +\ datefield + '<=' +\ time.strftime('%Y/%m/%d:%H:%M:%S',time.localtime(query_time)) for line in os.popen(p4 + ' jobs -e "' + query + '"','r').readlines(): # sample line: job000001 on 1998/08/10 by james *closed* 'comment' # jobname date author (jobname,author) = re.match( r'^(\S+) on \S+ by (\S+)', line).groups() (email,fullname) = re.match( r'^\S+ <(\S+)> \(([^\)]+)\)', \ os.popen(p4 + ' users ' + author,'r').read() ).groups() if send_to_author: (recipients,recipients_with_fullnames) = job_reviewers(jobname) else: (recipients,recipients_with_fullnames) = job_reviewers(jobname,author) if bcc_admin: recipients.append(administrator) if not recipients: continue # no one is interested message = 'From: ' + fullname + ' <' + email + '>\n' +\ 'To: ' + string.join(recipients_with_fullnames,', ') + '\n' +\ 'Subject: PERFORCE job ' + jobname + ' for review\n' +\ replyto_line +\ '\n' for line in os.popen(p4 + ' job -o ' + jobname,'r').readlines(): if line[0] != '#': message = message + line mailit(mailport, email, recipients, message) limit_emails = limit_emails - 1 if limit_emails <= 0: break # NOTE: the use of "p4 review -c" is for backwards compatibility with # pre-99.1 servers; with 99.1 or later servers use "p4 counter" if os.system(p4 + ' review -c ' + repr(query_time) + ' -t jobreview') !=0: complain(mailport,'Unable to set jobreview counter - check user "' +\ os.environ['P4USER'] + '" has review privileges\n(use p4 protect)') def loop_body(mailhost): # Note: there's a try: wrapped around everything so that the program won't # halt. Unfortunately, as a result you don't get the full traceback. # If you're debugging this script, strip off the special exception handlers # to get the real traceback, or try figuring out how to get a real traceback, # by importing the traceback module and defining a file object that # will take the output of traceback.print_exc(file=mailfileobject) # and mail it (see the example in cgi.py) try: mailport=smtplib.SMTP(mailhost) except: sys.stderr.write('Unable to connect to SMTP host "' + mailhost + '"!\n' +\ 'Will try again in ' + repr(sleeptime) + ' seconds.\n') else: try: if notify_changes: review_changes(mailport,limit_emails) if notify_jobs: review_jobs(mailport,limit_emails) except: complain(mailport,'Exception ' + repr(sys.exc_info()[0]) + ' raised.') try: mailport.quit() except: sys.stderr.write('Error while doing SMTP quit command (ignore).\n') if __name__ == '__main__': while(repeat): loop_body(mailhost) time.sleep(sleeptime) else: loop_body(mailhost)