(closes #493) convert remaining GET form to use POST
[buildbot.git] / buildbot / changes / p4poller.py
bloba313343a54ce80f1cb1feb555e3c0a74b5657283
1 # -*- test-case-name: buildbot.test.test_p4poller -*-
3 # Many thanks to Dave Peticolas for contributing this module
5 import re
6 import time
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
13 from buildbot import util
14 from buildbot.changes import base, changes
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."""
21 index = branchfile.find('/')
22 if index == -1: return None, None
23 branch, file = branchfile.split('/', 1)
24 return branch, file
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."""
30 compare_attrs = ["p4port", "p4user", "p4passwd", "p4base",
31 "p4bin", "pollinterval"]
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'
40 parent = None # filled in when we're added
41 last_change = None
42 loop = None
43 working = False
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 """
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)
79 def startService(self):
80 base.ChangeSource.startService(self)
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)
87 def stopService(self):
88 self.loop.stop()
89 return base.ChangeSource.stopService(self)
91 def describe(self):
92 return "p4source %s %s" % (self.p4port, self.p4base)
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
106 def _finished(self, res):
107 assert self.working
108 self.working = False
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
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)
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
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
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
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
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]
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)
207 self.last_change = num