Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/changes/p4poller.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/changes/p4poller.py')
-rw-r--r--buildbot/buildbot/changes/p4poller.py207
1 files changed, 207 insertions, 0 deletions
diff --git a/buildbot/buildbot/changes/p4poller.py b/buildbot/buildbot/changes/p4poller.py
new file mode 100644
index 0000000..a313343
--- /dev/null
+++ b/buildbot/buildbot/changes/p4poller.py
@@ -0,0 +1,207 @@
+# -*- test-case-name: buildbot.test.test_p4poller -*-
+
+# Many thanks to Dave Peticolas for contributing this module
+
+import re
+import time
+
+from twisted.python import log, failure
+from twisted.internet import defer, reactor
+from twisted.internet.utils import getProcessOutput
+from twisted.internet.task import LoopingCall
+
+from buildbot import util
+from buildbot.changes import base, changes
+
+def get_simple_split(branchfile):
+ """Splits the branchfile argument and assuming branch is
+ the first path component in branchfile, will return
+ branch and file else None."""
+
+ index = branchfile.find('/')
+ if index == -1: return None, None
+ branch, file = branchfile.split('/', 1)
+ return branch, file
+
+class P4Source(base.ChangeSource, util.ComparableMixin):
+ """This source will poll a perforce repository for changes and submit
+ them to the change master."""
+
+ compare_attrs = ["p4port", "p4user", "p4passwd", "p4base",
+ "p4bin", "pollinterval"]
+
+ changes_line_re = re.compile(
+ r"Change (?P<num>\d+) on \S+ by \S+@\S+ '.+'$")
+ describe_header_re = re.compile(
+ r"Change \d+ by (?P<who>\S+)@\S+ on (?P<when>.+)$")
+ file_re = re.compile(r"^\.\.\. (?P<path>[^#]+)#\d+ \w+$")
+ datefmt = '%Y/%m/%d %H:%M:%S'
+
+ parent = None # filled in when we're added
+ last_change = None
+ loop = None
+ working = False
+
+ def __init__(self, p4port=None, p4user=None, p4passwd=None,
+ p4base='//', p4bin='p4',
+ split_file=lambda branchfile: (None, branchfile),
+ pollinterval=60 * 10, histmax=None):
+ """
+ @type p4port: string
+ @param p4port: p4 port definition (host:portno)
+ @type p4user: string
+ @param p4user: p4 user
+ @type p4passwd: string
+ @param p4passwd: p4 passwd
+ @type p4base: string
+ @param p4base: p4 file specification to limit a poll to
+ without the trailing '...' (i.e., //)
+ @type p4bin: string
+ @param p4bin: path to p4 binary, defaults to just 'p4'
+ @type split_file: func
+ $param split_file: splits a filename into branch and filename.
+ @type pollinterval: int
+ @param pollinterval: interval in seconds between polls
+ @type histmax: int
+ @param histmax: (obsolete) maximum number of changes to look back through.
+ ignored; accepted for backwards compatibility.
+ """
+
+ self.p4port = p4port
+ self.p4user = p4user
+ self.p4passwd = p4passwd
+ self.p4base = p4base
+ self.p4bin = p4bin
+ self.split_file = split_file
+ self.pollinterval = pollinterval
+ self.loop = LoopingCall(self.checkp4)
+
+ def startService(self):
+ base.ChangeSource.startService(self)
+
+ # Don't start the loop just yet because the reactor isn't running.
+ # Give it a chance to go and install our SIGCHLD handler before
+ # spawning processes.
+ reactor.callLater(0, self.loop.start, self.pollinterval)
+
+ def stopService(self):
+ self.loop.stop()
+ return base.ChangeSource.stopService(self)
+
+ def describe(self):
+ return "p4source %s %s" % (self.p4port, self.p4base)
+
+ def checkp4(self):
+ # Our return value is only used for unit testing.
+ if self.working:
+ log.msg("Skipping checkp4 because last one has not finished")
+ return defer.succeed(None)
+ else:
+ self.working = True
+ d = self._get_changes()
+ d.addCallback(self._process_changes)
+ d.addBoth(self._finished)
+ return d
+
+ def _finished(self, res):
+ assert self.working
+ self.working = False
+
+ # Again, the return value is only for unit testing.
+ # If there's a failure, log it so it isn't lost.
+ if isinstance(res, failure.Failure):
+ log.msg('P4 poll failed: %s' % res)
+ return None
+ return res
+
+ def _get_changes(self):
+ args = []
+ if self.p4port:
+ args.extend(['-p', self.p4port])
+ if self.p4user:
+ args.extend(['-u', self.p4user])
+ if self.p4passwd:
+ args.extend(['-P', self.p4passwd])
+ args.extend(['changes'])
+ if self.last_change is not None:
+ args.extend(['%s...@%d,now' % (self.p4base, self.last_change+1)])
+ else:
+ args.extend(['-m', '1', '%s...' % (self.p4base,)])
+ env = {}
+ return getProcessOutput(self.p4bin, args, env)
+
+ def _process_changes(self, result):
+ last_change = self.last_change
+ changelists = []
+ for line in result.split('\n'):
+ line = line.strip()
+ if not line: continue
+ m = self.changes_line_re.match(line)
+ assert m, "Unexpected 'p4 changes' output: %r" % result
+ num = int(m.group('num'))
+ if last_change is None:
+ log.msg('P4Poller: starting at change %d' % num)
+ self.last_change = num
+ return []
+ changelists.append(num)
+ changelists.reverse() # oldest first
+
+ # Retrieve each sequentially.
+ d = defer.succeed(None)
+ for c in changelists:
+ d.addCallback(self._get_describe, c)
+ d.addCallback(self._process_describe, c)
+ return d
+
+ def _get_describe(self, dummy, num):
+ args = []
+ if self.p4port:
+ args.extend(['-p', self.p4port])
+ if self.p4user:
+ args.extend(['-u', self.p4user])
+ if self.p4passwd:
+ args.extend(['-P', self.p4passwd])
+ args.extend(['describe', '-s', str(num)])
+ env = {}
+ d = getProcessOutput(self.p4bin, args, env)
+ return d
+
+ def _process_describe(self, result, num):
+ lines = result.split('\n')
+ # SF#1555985: Wade Brainerd reports a stray ^M at the end of the date
+ # field. The rstrip() is intended to remove that.
+ lines[0] = lines[0].rstrip()
+ m = self.describe_header_re.match(lines[0])
+ assert m, "Unexpected 'p4 describe -s' result: %r" % result
+ who = m.group('who')
+ when = time.mktime(time.strptime(m.group('when'), self.datefmt))
+ comments = ''
+ while not lines[0].startswith('Affected files'):
+ comments += lines.pop(0) + '\n'
+ lines.pop(0) # affected files
+
+ branch_files = {} # dict for branch mapped to file(s)
+ while lines:
+ line = lines.pop(0).strip()
+ if not line: continue
+ m = self.file_re.match(line)
+ assert m, "Invalid file line: %r" % line
+ path = m.group('path')
+ if path.startswith(self.p4base):
+ branch, file = self.split_file(path[len(self.p4base):])
+ if (branch == None and file == None): continue
+ if branch_files.has_key(branch):
+ branch_files[branch].append(file)
+ else:
+ branch_files[branch] = [file]
+
+ for branch in branch_files:
+ c = changes.Change(who=who,
+ files=branch_files[branch],
+ comments=comments,
+ revision=num,
+ when=when,
+ branch=branch)
+ self.parent.addChange(c)
+
+ self.last_change = num