#!/usr/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. import sys, os, string, re, time, smtplib # CONFIGURATION VARIABLES notify_changes = 1 # set to 0 to disable change notification completely notify_jobs = 1 # set to 0 to disable job notification completely bcc_admin = 0 # 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 = 'adminname@yourcompany.com' # 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= 'C:\Progra~1\Perforce\p4.exe' # set to path of p4 executable repeat = 1 # set to 0 to run just once (do this if running from # cron!), set to 1 to repeat every sleeptime seconds sleeptime = 1000 # number of seconds to sleep between invocations # (irrelevant if repeat == 0) limit_emails = 40 # 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 # Note that if you set this to a field which has the # last modified date rather than creation date, you # will get notification of changed jobs as well. jobpath = '//depot/jobs' # send job review mail to users reviewing jobpath urlsub = '... //' # Depot path to replace with URL urlreplace = '//webserver/' # URL to display os.environ['P4PORT'] = 'localhost:1666' os.environ['P4USER'] = 'reviewuser' # user must have Perforce review privileges os.environ['P4PASSWD'] = 'reviewuserpassword' # 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 dev1 Rev 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(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' # process a change description - note only want to HTMLify # lines containing perforce path names. # These begin with "... //" and have a "#" symbol too. # subhash = re.compile ('#') for line in os.popen (p4 + ' describe -s ' + change).readlines(): dbslashref = string.find(line, '... //') if dbslashref == -1: message = message + line else: if string.find(line, '#',dbslashref) < 0: message = message + line else: templine = string.split (line,'#') if string.find(templine[1],'delete') < 0: # don't reformat "delete" lines as they don't have URLs message = message + 'http:' +\ string.replace(string.replace (templine[0], urlsub, urlreplace, 1),' ','%20') +\ ' #' + templine[1] else: message = message + line 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: 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 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): 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') while(repeat): loop_body(mailhost) time.sleep(sleeptime) else: loop_body(mailhost)