1 # -*- test-case-name: buildbot.test.test_p4poller -*-
3 # Many thanks to Dave Peticolas for contributing this module
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)
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
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):
51 @param p4port: p4 port definition (host:portno)
53 @param p4user: p4 user
54 @type p4passwd: string
55 @param p4passwd: p4 passwd
57 @param p4base: p4 file specification to limit a poll to
58 without the trailing '...' (i.e., //)
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
66 @param histmax: (obsolete) maximum number of changes to look back through.
67 ignored; accepted for backwards compatibility.
72 self
.p4passwd
= p4passwd
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
85 reactor
.callLater(0, self
.loop
.start
, self
.pollinterval
)
87 def stopService(self
):
89 return base
.ChangeSource
.stopService(self
)
92 return "p4source %s %s" % (self
.p4port
, self
.p4base
)
95 # Our return value is only used for unit testing.
97 log
.msg("Skipping checkp4 because last one has not finished")
98 return defer
.succeed(None)
101 d
= self
._get
_changes
()
102 d
.addCallback(self
._process
_changes
)
103 d
.addBoth(self
._finished
)
106 def _finished(self
, res
):
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
)
117 def _get_changes(self
):
120 args
.extend(['-p', self
.p4port
])
122 args
.extend(['-u', self
.p4user
])
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)])
129 args
.extend(['-m', '1', '%s...' % (self
.p4base
,)])
131 return getProcessOutput(self
.p4bin
, args
, env
)
133 def _process_changes(self
, result
):
134 last_change
= self
.last_change
136 for line
in result
.split('\n'):
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
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
)
156 def _get_describe(self
, dummy
, num
):
159 args
.extend(['-p', self
.p4port
])
161 args
.extend(['-u', self
.p4user
])
163 args
.extend(['-P', self
.p4passwd
])
164 args
.extend(['describe', '-s', str(num
)])
166 d
= getProcessOutput(self
.p4bin
, args
, env
)
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
177 when
= time
.mktime(time
.strptime(m
.group('when'), self
.datefmt
))
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)
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)
196 branch_files
[branch
] = [file]
198 for branch
in branch_files
:
199 c
= changes
.Change(who
=who
,
200 files
=branch_files
[branch
],
205 self
.parent
.addChange(c
)
207 self
.last_change
= num