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