p4review.py #2

  • //
  • guest/
  • paul_goffin/
  • review/
  • p4review.py
  • View
  • Commits
  • Open Download .zip Download (11 KB)
#!/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@perforce.com> (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@perforce.com> (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)
# Change User Description Committed
#2 298 paul_goffin Now manages spaces in file/path names.

And includes change type (add/edit/delete, etc.) - just as the
original version did.

And does not provide a URL for deleted files - as they don't
have one!
#1 297 paul_goffin Modified python review script to display URLs instead of depot
filenames in the change mail.

Converts perforce paths to MS-Outlook stype URLs.