Package buildbot :: Package changes :: Module p4poller
[frames] | no frames]

Source Code for Module buildbot.changes.p4poller

  1  # -*- test-case-name: buildbot.test.test_p4poller -*- 
  2   
  3  # Many thanks to Dave Peticolas for contributing this module 
  4   
  5  import re 
  6  import time 
  7   
  8  from twisted.python import log, failure 
  9  from twisted.internet import defer, reactor 
 10  from twisted.internet.utils import getProcessOutput 
 11  from twisted.internet.task import LoopingCall 
 12   
 13  from buildbot import util 
 14  from buildbot.changes import base, changes 
 15   
16 -def get_simple_split(branchfile):
17 """Splits the branchfile argument and assuming branch is 18 the first path component in branchfile, will return 19 branch and file else None.""" 20 21 index = branchfile.find('/') 22 if index == -1: return None, None 23 branch, file = branchfile.split('/', 1) 24 return branch, file
25
26 -class P4Source(base.ChangeSource, util.ComparableMixin):
27 """This source will poll a perforce repository for changes and submit 28 them to the change master.""" 29 30 compare_attrs = ["p4port", "p4user", "p4passwd", "p4base", 31 "p4bin", "pollinterval"] 32 33 changes_line_re = re.compile( 34 r"Change (?P<num>\d+) on \S+ by \S+@\S+ '.*'$") 35 describe_header_re = re.compile( 36 r"Change \d+ by (?P<who>\S+)@\S+ on (?P<when>.+)$") 37 file_re = re.compile(r"^\.\.\. (?P<path>[^#]+)#\d+ \w+$") 38 datefmt = '%Y/%m/%d %H:%M:%S' 39 40 parent = None # filled in when we're added 41 last_change = None 42 loop = None 43 working = False 44
45 - def __init__(self, p4port=None, p4user=None, p4passwd=None, 46 p4base='//', p4bin='p4', 47 split_file=lambda branchfile: (None, branchfile), 48 pollinterval=60 * 10, histmax=None):
49 """ 50 @type p4port: string 51 @param p4port: p4 port definition (host:portno) 52 @type p4user: string 53 @param p4user: p4 user 54 @type p4passwd: string 55 @param p4passwd: p4 passwd 56 @type p4base: string 57 @param p4base: p4 file specification to limit a poll to 58 without the trailing '...' (i.e., //) 59 @type p4bin: string 60 @param p4bin: path to p4 binary, defaults to just 'p4' 61 @type split_file: func 62 $param split_file: splits a filename into branch and filename. 63 @type pollinterval: int 64 @param pollinterval: interval in seconds between polls 65 @type histmax: int 66 @param histmax: (obsolete) maximum number of changes to look back through. 67 ignored; accepted for backwards compatibility. 68 """ 69 70 self.p4port = p4port 71 self.p4user = p4user 72 self.p4passwd = p4passwd 73 self.p4base = p4base 74 self.p4bin = p4bin 75 self.split_file = split_file 76 self.pollinterval = pollinterval 77 self.loop = LoopingCall(self.checkp4)
78
79 - def startService(self):
80 base.ChangeSource.startService(self) 81 82 # Don't start the loop just yet because the reactor isn't running. 83 # Give it a chance to go and install our SIGCHLD handler before 84 # spawning processes. 85 reactor.callLater(0, self.loop.start, self.pollinterval)
86
87 - def stopService(self):
88 self.loop.stop() 89 return base.ChangeSource.stopService(self)
90
91 - def describe(self):
92 return "p4source %s %s" % (self.p4port, self.p4base)
93
94 - def checkp4(self):
95 # Our return value is only used for unit testing. 96 if self.working: 97 log.msg("Skipping checkp4 because last one has not finished") 98 return defer.succeed(None) 99 else: 100 self.working = True 101 d = self._get_changes() 102 d.addCallback(self._process_changes) 103 d.addBoth(self._finished) 104 return d
105
106 - def _finished(self, res):
107 assert self.working 108 self.working = False 109 110 # Again, the return value is only for unit testing. 111 # If there's a failure, log it so it isn't lost. 112 if isinstance(res, failure.Failure): 113 log.msg('P4 poll failed: %s' % res) 114 return None 115 return res
116
117 - def _get_changes(self):
118 args = [] 119 if self.p4port: 120 args.extend(['-p', self.p4port]) 121 if self.p4user: 122 args.extend(['-u', self.p4user]) 123 if self.p4passwd: 124 args.extend(['-P', self.p4passwd]) 125 args.extend(['changes']) 126 if self.last_change is not None: 127 args.extend(['%s...@%d,now' % (self.p4base, self.last_change+1)]) 128 else: 129 args.extend(['-m', '1', '%s...' % (self.p4base,)]) 130 env = {} 131 return getProcessOutput(self.p4bin, args, env)
132
133 - def _process_changes(self, result):
134 last_change = self.last_change 135 changelists = [] 136 for line in result.split('\n'): 137 line = line.strip() 138 if not line: continue 139 m = self.changes_line_re.match(line) 140 assert m, "Unexpected 'p4 changes' output: %r" % result 141 num = int(m.group('num')) 142 if last_change is None: 143 log.msg('P4Poller: starting at change %d' % num) 144 self.last_change = num 145 return [] 146 changelists.append(num) 147 changelists.reverse() # oldest first 148 149 # Retrieve each sequentially. 150 d = defer.succeed(None) 151 for c in changelists: 152 d.addCallback(self._get_describe, c) 153 d.addCallback(self._process_describe, c) 154 return d
155
156 - def _get_describe(self, dummy, num):
157 args = [] 158 if self.p4port: 159 args.extend(['-p', self.p4port]) 160 if self.p4user: 161 args.extend(['-u', self.p4user]) 162 if self.p4passwd: 163 args.extend(['-P', self.p4passwd]) 164 args.extend(['describe', '-s', str(num)]) 165 env = {} 166 d = getProcessOutput(self.p4bin, args, env) 167 return d
168
169 - def _process_describe(self, result, num):
170 lines = result.split('\n') 171 # SF#1555985: Wade Brainerd reports a stray ^M at the end of the date 172 # field. The rstrip() is intended to remove that. 173 lines[0] = lines[0].rstrip() 174 m = self.describe_header_re.match(lines[0]) 175 assert m, "Unexpected 'p4 describe -s' result: %r" % result 176 who = m.group('who') 177 when = time.mktime(time.strptime(m.group('when'), self.datefmt)) 178 comments = '' 179 while not lines[0].startswith('Affected files'): 180 comments += lines.pop(0) + '\n' 181 lines.pop(0) # affected files 182 183 branch_files = {} # dict for branch mapped to file(s) 184 while lines: 185 line = lines.pop(0).strip() 186 if not line: continue 187 m = self.file_re.match(line) 188 assert m, "Invalid file line: %r" % line 189 path = m.group('path') 190 if path.startswith(self.p4base): 191 branch, file = self.split_file(path[len(self.p4base):]) 192 if (branch == None and file == None): continue 193 if branch_files.has_key(branch): 194 branch_files[branch].append(file) 195 else: 196 branch_files[branch] = [file] 197 198 for branch in branch_files: 199 c = changes.Change(who=who, 200 files=branch_files[branch], 201 comments=comments, 202 revision=num, 203 when=when, 204 branch=branch) 205 self.parent.addChange(c) 206 207 self.last_change = num
208