1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import re
20 import time
21 import os
22
23 from twisted.python import log
24 from twisted.internet import defer, reactor
25 from twisted.internet.utils import getProcessOutput
26 from twisted.internet.task import LoopingCall
27
28 from buildbot import util
29 from buildbot.changes import base, changes
30
32 """Something went wrong with the poll. This is used as a distinctive
33 exception type so that unit tests can detect and ignore it."""
34
36 """Splits the branchfile argument and assuming branch is
37 the first path component in branchfile, will return
38 branch and file else None."""
39
40 index = branchfile.find('/')
41 if index == -1: return None, None
42 branch, file = branchfile.split('/', 1)
43 return branch, file
44
45 -class P4Source(base.ChangeSource, util.ComparableMixin):
46 """This source will poll a perforce repository for changes and submit
47 them to the change master."""
48
49 compare_attrs = ["p4port", "p4user", "p4passwd", "p4base",
50 "p4bin", "pollinterval"]
51
52 env_vars = ["P4CLIENT", "P4PORT", "P4PASSWD", "P4USER",
53 "P4CHARSET"]
54
55 changes_line_re = re.compile(
56 r"Change (?P<num>\d+) on \S+ by \S+@\S+ '.*'$")
57 describe_header_re = re.compile(
58 r"Change \d+ by (?P<who>\S+)@\S+ on (?P<when>.+)$")
59 file_re = re.compile(r"^\.\.\. (?P<path>[^#]+)#\d+ [/\w]+$")
60 datefmt = '%Y/%m/%d %H:%M:%S'
61
62 parent = None
63 last_change = None
64 loop = None
65 working = False
66
67 - def __init__(self, p4port=None, p4user=None, p4passwd=None,
68 p4base='//', p4bin='p4',
69 split_file=lambda branchfile: (None, branchfile),
70 pollinterval=60 * 10, histmax=None):
71 """
72 @type p4port: string
73 @param p4port: p4 port definition (host:portno)
74 @type p4user: string
75 @param p4user: p4 user
76 @type p4passwd: string
77 @param p4passwd: p4 passwd
78 @type p4base: string
79 @param p4base: p4 file specification to limit a poll to
80 without the trailing '...' (i.e., //)
81 @type p4bin: string
82 @param p4bin: path to p4 binary, defaults to just 'p4'
83 @type split_file: func
84 $param split_file: splits a filename into branch and filename.
85 @type pollinterval: int
86 @param pollinterval: interval in seconds between polls
87 @type histmax: int
88 @param histmax: (obsolete) maximum number of changes to look back through.
89 ignored; accepted for backwards compatibility.
90 """
91
92 self.p4port = p4port
93 self.p4user = p4user
94 self.p4passwd = p4passwd
95 self.p4base = p4base
96 self.p4bin = p4bin
97 self.split_file = split_file
98 self.pollinterval = pollinterval
99 self.loop = LoopingCall(self.checkp4)
100
108
112
114 return "p4source %s %s" % (self.p4port, self.p4base)
115
117
118 if self.working:
119 log.msg("Skipping checkp4 because last one has not finished")
120 return defer.succeed(None)
121 else:
122 self.working = True
123 d = self._get_changes()
124 d.addCallback(self._process_changes)
125 d.addCallbacks(self._finished_ok, self._finished_failure)
126 return d
127
132
134 assert self.working
135 self.working = False
136
137
138
139
140 log.err(res, 'P4 poll failed')
141 return None
142
147
149 args = []
150 if self.p4port:
151 args.extend(['-p', self.p4port])
152 if self.p4user:
153 args.extend(['-u', self.p4user])
154 if self.p4passwd:
155 args.extend(['-P', self.p4passwd])
156 args.extend(['changes'])
157 if self.last_change is not None:
158 args.extend(['%s...@%d,now' % (self.p4base, self.last_change+1)])
159 else:
160 args.extend(['-m', '1', '%s...' % (self.p4base,)])
161 return self._get_process_output(args)
162
164 last_change = self.last_change
165 changelists = []
166 for line in result.split('\n'):
167 line = line.strip()
168 if not line: continue
169 m = self.changes_line_re.match(line)
170 if not m:
171 raise P4PollerError("Unexpected 'p4 changes' output: %r" % result)
172 num = int(m.group('num'))
173 if last_change is None:
174 log.msg('P4Poller: starting at change %d' % num)
175 self.last_change = num
176 return []
177 changelists.append(num)
178 changelists.reverse()
179
180
181 d = defer.succeed(None)
182 for c in changelists:
183 d.addCallback(self._get_describe, c)
184 d.addCallback(self._process_describe, c)
185 return d
186
188 args = []
189 if self.p4port:
190 args.extend(['-p', self.p4port])
191 if self.p4user:
192 args.extend(['-u', self.p4user])
193 if self.p4passwd:
194 args.extend(['-P', self.p4passwd])
195 args.extend(['describe', '-s', str(num)])
196 return self._get_process_output(args)
197
199 lines = result.split('\n')
200
201
202 lines[0] = lines[0].rstrip()
203 m = self.describe_header_re.match(lines[0])
204 if not m:
205 raise P4PollerError("Unexpected 'p4 describe -s' result: %r" % result)
206 who = m.group('who')
207 when = time.mktime(time.strptime(m.group('when'), self.datefmt))
208 comments = ''
209 while not lines[0].startswith('Affected files'):
210 comments += lines.pop(0) + '\n'
211 lines.pop(0)
212
213 branch_files = {}
214 while lines:
215 line = lines.pop(0).strip()
216 if not line: continue
217 m = self.file_re.match(line)
218 if not m:
219 raise P4PollerError("Invalid file line: %r" % line)
220 path = m.group('path')
221 if path.startswith(self.p4base):
222 branch, file = self.split_file(path[len(self.p4base):])
223 if (branch == None and file == None): continue
224 if branch_files.has_key(branch):
225 branch_files[branch].append(file)
226 else:
227 branch_files[branch] = [file]
228
229 for branch in branch_files:
230 c = changes.Change(who=who,
231 files=branch_files[branch],
232 comments=comments,
233 revision=str(num),
234 when=when,
235 branch=branch)
236 self.parent.addChange(c)
237
238 self.last_change = num
239