Ported LLVMPoller.
diff --git a/zorg/buildbot/changes/llvmgitpoller.py b/zorg/buildbot/changes/llvmgitpoller.py
index cc1cf42..012d0ef 100644
--- a/zorg/buildbot/changes/llvmgitpoller.py
+++ b/zorg/buildbot/changes/llvmgitpoller.py
@@ -1,284 +1,41 @@
# LLVM buildbot needs to watch multiple projects within a single repository.
-
-# Based on the buildbot.changes.gitpoller.GitPoller source code.
-# For buildbot v0.8.5
-
-import time
-import tempfile
-import os
+# TODO: Handle both author and committer for a commit to build a correct blame list later.
import re
-import itertools
from twisted.python import log
-from twisted.internet import defer, utils
+from datetime import datetime
-from buildbot.util import deferredLocked
-from buildbot.changes import base
-from buildbot.util import epoch2datetime
+from twisted.internet import defer
+from buildbot.util import bytes2unicode
+from buildbot.plugins import changes
-class LLVMPoller(base.PollingChangeSource):
+class LLVMPoller(changes.GitPoller):
"""
Poll LLVM repository for changes and submit them to the change master.
- Following Multiple Projects.
+ Following Multiple LLVM Projects.
This source will poll a remote LLVM git _monorepo_ for changes and submit
them to the change master."""
_repourl = "https://github.com/llvm/llvm-project"
_branch = "master"
- _categories = {
- # Project: Category:
- 'llvm' : 'llvm',
- 'cfe' : 'clang',
- 'polly' : 'polly',
- 'compiler-rt' : 'compiler-rt',
- 'flang' : 'flang',
- 'libc' : 'libc',
- 'libcxx' : 'libcxx',
- 'libcxxabi' : 'libcxxabi',
- 'libunwind' : 'libunwind',
- 'lld' : 'lld',
- 'lldb' : 'lldb',
- 'mlir' : 'mlir',
- 'llgo' : 'llgo',
- 'openmp' : 'openmp',
- }
compare_attrs = ["repourl", "branch", "workdir",
"pollInterval", "gitbin", "usetimestamps",
"category", "project",
"projects"]
- projects = None # Projects and branches to watch.
-
- def __init__(self, repourl=_repourl, branch=_branch,
- workdir=None, pollInterval=10*60,
- gitbin='git', usetimestamps=True,
- category=None, project=None,
- pollinterval=-2, fetch_refspec=None,
- encoding='utf-8', projects=None):
+ def __init__(self,
+ repourl=_repourl, branch=_branch,
+ **kwargs):
self.cleanRe = re.compile(r"Require(?:s?)\s*.*\s*clean build", re.IGNORECASE + re.MULTILINE)
self.cleanCfg = re.compile(r"(CMakeLists\.txt$|\.cmake$|\.cmake\.in$)")
- # projects is a list of projects to watch or None to watch all.
- if projects:
- if isinstance(projects, str) or isinstance(projects, tuple):
- projects = [projects]
- assert isinstance(projects, list)
- assert len(projects) > 0
+ # TODO: Add support for an optional list of projects.
+ # For now we always watch all the projects.
- # Each project to watch is a string (project name) or a tuple
- # (project name, branch) like ('llvm', 'branches/release_30').
- # But we want it always to be a tuple, so we convert a project
- # name string to a tuple (project, 'master').
- self.projects = set()
- for project in projects:
- if isinstance(project, str):
- project = (project, branch)
-
- assert isinstance(project, tuple)
- self.projects.add(project)
-
- # for backward compatibility; the parameter used to be spelled with 'i'
- if pollinterval != -2:
- pollInterval = pollinterval
- if project is None: project = ''
-
- self.repourl = repourl
- self.branch = branch
- self.pollInterval = pollInterval
- self.fetch_refspec = fetch_refspec
- self.encoding = encoding
- self.lastChange = time.time()
- self.lastPoll = time.time()
- self.gitbin = gitbin
- self.workdir = workdir
- self.usetimestamps = usetimestamps
- self.category = category
- self.project = project
- self.changeCount = 0
- self.commitInfo = {}
- self.initLock = defer.DeferredLock()
-
- if self.workdir == None:
- self.workdir = tempfile.gettempdir() + '/gitpoller_work'
- log.msg("WARNING: LLVMGitPoller using deprecated temporary workdir " +
- "'%s'; consider setting workdir=" % self.workdir)
-
- def startService(self):
- # make our workdir absolute, relative to the master's basedir
- if not os.path.isabs(self.workdir):
- self.workdir = os.path.join(self.master.basedir, self.workdir)
- log.msg("LLVMGitPoller: using workdir '%s'" % self.workdir)
-
- # initialize the repository we'll use to get changes; note that
- # startService is not an event-driven method, so this method will
- # instead acquire self.initLock immediately when it is called.
- if not os.path.exists(self.workdir + r'/.git'):
- d = self.initRepository()
- d.addErrback(log.err, 'while initializing LLVMGitPoller repository')
- else:
- log.msg("LLVMGitPoller repository already exists")
-
- # call this *after* initRepository, so that the initLock is locked first
- base.PollingChangeSource.startService(self)
-
- @deferredLocked('initLock')
- def initRepository(self):
- d = defer.succeed(None)
- def make_dir(_):
- dirpath = os.path.dirname(self.workdir.rstrip(os.sep))
- if not os.path.exists(dirpath):
- log.msg('LLVMGitPoller: creating parent directories for workdir')
- os.makedirs(dirpath)
- d.addCallback(make_dir)
-
- def git_init(_):
- log.msg('LLVMGitPoller: initializing working dir from %s' % self.repourl)
- d = utils.getProcessOutputAndValue(self.gitbin,
- ['init', self.workdir], env=dict(PATH=os.environ['PATH']))
- d.addCallback(self._convert_nonzero_to_failure)
- d.addErrback(self._stop_on_failure)
- return d
- d.addCallback(git_init)
-
- def git_remote_add(_):
- d = utils.getProcessOutputAndValue(self.gitbin,
- ['remote', 'add', 'origin', self.repourl],
- path=self.workdir, env=dict(PATH=os.environ['PATH']))
- d.addCallback(self._convert_nonzero_to_failure)
- d.addErrback(self._stop_on_failure)
- return d
- d.addCallback(git_remote_add)
-
- def git_fetch_origin(_):
- args = ['fetch', 'origin']
- self._extend_with_fetch_refspec(args)
- d = utils.getProcessOutputAndValue(self.gitbin, args,
- path=self.workdir, env=dict(PATH=os.environ['PATH']))
- d.addCallback(self._convert_nonzero_to_failure)
- d.addErrback(self._stop_on_failure)
- return d
- d.addCallback(git_fetch_origin)
-
- def set_master(_):
- log.msg('LLVMGitPoller: checking out %s' % self.branch)
- if self.branch == 'master': # repo is already on branch 'master', so reset
- d = utils.getProcessOutputAndValue(self.gitbin,
- ['reset', '--hard', 'origin/%s' % self.branch],
- path=self.workdir, env=dict(PATH=os.environ['PATH']))
- else:
- d = utils.getProcessOutputAndValue(self.gitbin,
- ['checkout', '-b', self.branch, 'origin/%s' % self.branch],
- path=self.workdir, env=dict(PATH=os.environ['PATH']))
- d.addCallback(self._convert_nonzero_to_failure)
- d.addErrback(self._stop_on_failure)
- return d
- d.addCallback(set_master)
- def get_rev(_):
- d = utils.getProcessOutputAndValue(self.gitbin,
- ['rev-parse', self.branch],
- path=self.workdir, env={})
- d.addCallback(self._convert_nonzero_to_failure)
- d.addErrback(self._stop_on_failure)
- d.addCallback(lambda (out, err, code) : out.strip())
- return d
- d.addCallback(get_rev)
- def print_rev(rev):
- log.msg("LLVMGitPoller: finished initializing working dir from %s at rev %s"
- % (self.repourl, rev))
- d.addCallback(print_rev)
- return d
-
- def describe(self):
- status = ""
- if not self.master:
- status = "[STOPPED - check log]"
- str = 'LLVMGitPoller watching the remote git repository %s, branch: %s %s' \
- % (self.repourl, self.branch, status)
- return str
-
- @deferredLocked('initLock')
- def poll(self):
- d = self._get_changes()
- d.addCallback(self._process_changes)
- d.addErrback(self._process_changes_failure)
- d.addCallback(self._catch_up)
- d.addErrback(self._catch_up_failure)
- return d
-
- def _get_commit_comments(self, rev):
- args = ['log', rev, '--no-walk', r'--format=%s%n%b']
- d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=dict(PATH=os.environ['PATH']), errortoo=False )
- def process(git_output):
- stripped_output = git_output.strip().decode(self.encoding)
- if len(stripped_output) == 0:
- raise EnvironmentError('could not get commit comment for rev')
- #log.msg("LLVMGitPoller: _get_commit_comments: '%s'" % stripped_output)
- return stripped_output
- d.addCallback(process)
- return d
-
- def _get_commit_timestamp(self, rev):
- # unix timestamp
- args = ['log', rev, '--no-walk', r'--format=%ct']
- d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=dict(PATH=os.environ['PATH']), errortoo=False )
- def process(git_output):
- stripped_output = git_output.strip()
- if self.usetimestamps:
- try:
- stamp = float(stripped_output)
- #log.msg("LLVMGitPoller: _get_commit_timestamp: \'%s\'" % stamp)
- except Exception, e:
- log.msg('LLVMGitPoller: caught exception converting output \'%s\' to timestamp' % stripped_output)
- raise e
- return stamp
- else:
- return None
- d.addCallback(process)
- return d
-
- def _get_commit_files(self, rev):
- args = ['log', rev, '--name-only', '--no-walk', r'--format=%n']
- d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=dict(PATH=os.environ['PATH']), errortoo=False )
- def process(git_output):
- fileList = git_output.split()
- #log.msg("LLVMGitPoller: _get_commit_files: \'%s\'" % fileList)
- return fileList
- d.addCallback(process)
- return d
-
- def _get_commit_name(self, rev):
- args = ['log', rev, '--no-walk', r'--format=%aN <%aE>']
- d = utils.getProcessOutput(self.gitbin, args, path=self.workdir, env=dict(PATH=os.environ['PATH']), errortoo=False )
- def process(git_output):
- stripped_output = git_output.strip().decode(self.encoding)
- if len(stripped_output) == 0:
- raise EnvironmentError('could not get commit name for rev')
- #log.msg("LLVMGitPoller: _get_commit_name: \'%s\'" % stripped_output)
- return stripped_output
- d.addCallback(process)
- return d
-
- def _get_changes(self):
- log.msg('LLVMGitPoller: polling git repo at %s' % self.repourl)
-
- self.lastPoll = time.time()
-
- # get a deferred object that performs the fetch
- args = ['fetch', 'origin']
- self._extend_with_fetch_refspec(args)
-
- # This command always produces data on stderr, but we actually do not care
- # about the stderr or stdout from this command. We set errortoo=True to
- # avoid an errback from the deferred. The callback which will be added to this
- # deferred will not use the response.
- d = utils.getProcessOutput(self.gitbin, args,
- path=self.workdir,
- env=dict(PATH=os.environ['PATH']), errortoo=True )
-
- return d
+ super().__init__(repourl=repourl, branch=branch, **kwargs)
def _transform_path(self, fileList):
"""
@@ -288,220 +45,139 @@
NOTE: we don't change result path, just extract a project name.
"""
- #log.msg("LLVMGitPoller: _transform_path: got a file list: %s" % fileList)
-
- if fileList is None or len(fileList) == 0:
- return None
+ #log.msg("LLVMPoller: _transform_path: got a file list: %s" % fileList)
result = {}
+ # It is possible that this commit does not change anything.
+ if not fileList:
+ return result
+
# turn libcxxabi/include/__cxxabi_config.h into
# ("libcxxabi", "libcxxabi/include/__cxxabi_config.h")
# and filter projects we are not watching.
for path in fileList:
+ if not path:
+ continue
+
pieces = path.split('/')
- project = pieces.pop(0)
- #NOTE:TODO: a dirty hack for backward compatibility.
- if project == "clang":
- project = "cfe"
+ project = pieces[0] if len(pieces) > 1 else None
- #log.msg("LLVMGitPoller: _transform_path: processing path %s: project: %s" % (path, project))
- if self.projects:
- #NOTE: multibranch is not supported.
- #log.msg("LLVMGitPoller: _transform_path: (%s, %s) in projects: %s" % (project, self.branch, (project, self.branch) in self.projects))
- if (project, self.branch) in self.projects:
- # Collect file path for each detected projects.
- if project in result:
- result[project].append(path)
- else:
- result[project] = [path]
+ #log.msg("LLVMPoller: _transform_path: processing path %s: project: %s" % (path, project))
+ # Collect file path for each detected projects.
+ if project in result:
+ result[project].append(path)
+ else:
+ result[project] = [path]
- #log.msg("LLVMGitPoller: _transform_path: result: %s" % result)
+ log.msg("LLVMPoller: _transform_path: result: %s" % result)
return [(k, result[k]) for k in result]
- @defer.deferredGenerator
- def _process_changes(self, unused_output):
+ @defer.inlineCallbacks
+ def _process_changes(self, newRev, branch):
+ """
+ Read changes since last change.
+
+ - Read list of commit hashes.
+ - Extract details from each commit.
+ - Add changes to database.
+ """
+
+ # initial run, don't parse all history
+ if not self.lastRev:
+ return
+
# get the change list
- revListArgs = ['log', '%s..origin/%s' % (self.branch, self.branch), r'--format=%H']
+ revListArgs = (['--ignore-missing'] +
+ ['--format=%H', '{}'.format(newRev)] +
+ ['^' + rev
+ for rev in sorted(self.lastRev.values())] +
+ ['--'])
self.changeCount = 0
- d = utils.getProcessOutput(self.gitbin, revListArgs, path=self.workdir,
- env=dict(PATH=os.environ['PATH']), errortoo=False )
- wfd = defer.waitForDeferred(d)
- yield wfd
- results = wfd.getResult()
+ results = yield self._dovccmd('log', revListArgs, path=self.workdir)
# process oldest change first
revList = results.split()
- if not revList:
- return
-
revList.reverse()
- self.changeCount = len(revList)
- log.msg('LLVMGitPoller: processing %d changes: %s in "%s"'
- % (self.changeCount, revList, self.workdir) )
+ if self.buildPushesWithNoCommits and not revList:
+ existingRev = self.lastRev.get(branch)
+ if existingRev != newRev:
+ revList = [newRev]
+ if existingRev is None:
+ # This branch was completely unknown, rebuild
+ log.msg('LLVMPoller: rebuilding {} for new branch "{}"'.format(
+ newRev, branch))
+ else:
+ # This branch is known, but it now points to a different
+ # commit than last time we saw it, rebuild.
+ log.msg('LLVMPoller: rebuilding {} for updated branch "{}"'.format(
+ newRev, branch))
+
+ self.changeCount = len(revList)
+ self.lastRev[branch] = newRev
+
+ if self.changeCount:
+ log.msg('LLVMPoller: processing {} changes: {} from "{}" branch "{}"'.format(
+ self.changeCount, revList, self.repourl, branch))
for rev in revList:
- #log.msg('LLVMGitPoller: waiting defer for revision: %s' % rev)
dl = defer.DeferredList([
self._get_commit_timestamp(rev),
- self._get_commit_name(rev),
+ self._get_commit_author(rev),
+ self._get_commit_committer(rev),
self._get_commit_files(rev),
self._get_commit_comments(rev),
], consumeErrors=True)
- wfd = defer.waitForDeferred(dl)
- yield wfd
- results = wfd.getResult()
- #log.msg('LLVMGitPoller: got defer results: %s' % results)
+ results = yield dl
# check for failures
- failures = [ r[1] for r in results if not r[0] ]
+ failures = [r[1] for r in results if not r[0]]
if failures:
+ for failure in failures:
+ log.err(
+ failure, "while processing changes for {} {}".format(newRev, branch))
# just fail on the first error; they're probably all related!
- raise failures[0]
+ failures[0].raiseException()
- #log.msg('LLVMGitPoller: begin change adding cycle for revision: %s' % rev)
+ log.msg('>>> LLVMPoller: begin change adding cycle for revision: %s' % rev)
- timestamp, name, files, comments = [ r[1] for r in results ]
+ timestamp, author, committer, files, comments = [r[1] for r in results]
+
where = self._transform_path(files)
- #log.msg('LLVMGitPoller: walking over transformed path/projects: %s' % where)
+
+ projects = list()
+ properties = dict()
+
+ #log.msg('LLVMPoller: walking over transformed path/projects: %s' % where)
for wh in where:
where_project, where_project_files = wh
- #log.msg('LLVMGitPoller: processing transformed pair: %s, files:' % where_project, where_project_files)
+ #log.msg('LLVMPoller: processing transformed pair: %s, files:' % where_project, where_project_files)
+ projects += [where_project]
- properties = dict()
if self.cleanRe.search(comments) or \
any([m for f in where_project_files for m in [self.cleanCfg.search(f)] if m]):
- log.msg("LLVMGitPoller: creating a change with the 'clean' property for r%s" % rev)
+ log.msg("LLVMPoller: creating a change with the 'clean_obj' property for r%s" % rev)
properties['clean_obj'] = (True, "change")
- log.msg("LLVMGitPoller: creating a change rev=%s" % rev)
- d = self.master.addChange(
- author=name,
- revision=rev,
- files=where_project_files,
+ log.msg("LLVMPoller: creating a change rev=%s" % rev)
+ log.msg(" >>> revision=%s, timestamp=%s, author=%s, committer=%s, project=%s, files=%s, comments=\"%s\", properties=%s" % \
+ (bytes2unicode(rev, encoding=self.encoding), datetime.fromtimestamp(timestamp), author, committer,
+ projects, files, comments, properties))
+
+ yield self.master.data.updates.addChange(
+ author=author,
+ committer=committer if committer != author else None,
+ revision=bytes2unicode(rev, encoding=self.encoding),
+ files=files,
comments=comments,
- when_timestamp=epoch2datetime(timestamp),
- branch=self.branch,
- category=self._categories.get(where_project, self.category),
- project=where_project,
+ when_timestamp=timestamp,
+ branch=bytes2unicode(self._removeHeads(branch)),
+ category=self.category, ## TODO: Figure out if we could support tags here
+ project=",".join(projects),
# Always promote an external github url of the LLVM project with the changes.
repository=self._repourl,
- src='git',
+ src='git', # Must be one of the buildbot.process.users.srcs
properties=properties)
- wfd = defer.waitForDeferred(d)
- yield wfd
- results = wfd.getResult()
-
- def _process_changes_failure(self, f):
- log.msg('LLVMGitPoller: repo poll failed')
- log.err(f)
- # eat the failure to continue along the defered chain - we still want to catch up
- return None
-
- def _catch_up(self, res):
- if self.changeCount == 0:
- log.msg('LLVMGitPoller: no changes, no catch_up')
- return
- log.msg('LLVMGitPoller: catching up tracking branch')
- args = ['reset', '--hard', 'origin/%s' % (self.branch,)]
- d = utils.getProcessOutputAndValue(self.gitbin, args, path=self.workdir, env=dict(PATH=os.environ['PATH']))
- d.addCallback(self._convert_nonzero_to_failure)
- return d
-
- def _catch_up_failure(self, f):
- log.err(f)
- log.msg('LLVMGitPoller: please resolve issues in local repo: %s' % self.workdir)
- # this used to stop the service, but this is (a) unfriendly to tests and (b)
- # likely to leave the error message lost in a sea of other log messages
-
- def _convert_nonzero_to_failure(self, res):
- "utility method to handle the result of getProcessOutputAndValue"
- (stdout, stderr, code) = res
- if code != 0:
- raise EnvironmentError('command failed with exit code %d: %s' % (code, stderr))
- return (stdout, stderr, code)
-
- def _stop_on_failure(self, f):
- "utility method to stop the service when a failure occurs"
- if self.running:
- d = defer.maybeDeferred(lambda : self.stopService())
- d.addErrback(log.err, 'while stopping broken GitPoller service')
- return f
-
- def _extend_with_fetch_refspec(self, args):
- if self.fetch_refspec:
- if type(self.fetch_refspec) in (list,set):
- args.extend(self.fetch_refspec)
- else:
- args.append(self.fetch_refspec)
-
-
-# Run: python -m zorg.buildbot.changes.llvmgitpoller
-if __name__ == '__main__':
- print "Testing Git LLVMPoller..."
- poller = LLVMPoller(projects = [
- "llvm",
- "cfe",
- "clang-tests-external",
- "clang-tools-extra",
- "polly",
- "compiler-rt",
- "libcxx",
- "libcxxabi",
- "libunwind",
- "lld",
- "lldb",
- "openmp",
- "lnt",
- "test-suite"
- ],
- workdir = os.getcwd()
- )
-
- # Test _transform_path method.
- fileList = [
- "clang-tools-extra/clang-doc/Generators.cpp",
- "clang-tools-extra/clang-doc/Generators.h",
- "clang-tools-extra/clang-doc/HTMLGenerator.cpp",
- "clang-tools-extra/clang-doc/MDGenerator.cpp",
- "clang-tools-extra/clang-doc/Representation.cpp",
- "clang-tools-extra/clang-doc/Representation.h",
- "clang-tools-extra/clang-doc/YAMLGenerator.cpp",
- "clang-tools-extra/clang-doc/assets/clang-doc-default-stylesheet.css",
- "clang-tools-extra/clang-doc/assets/index.js",
- "clang-tools-extra/clang-doc/stylesheets/clang-doc-default-stylesheet.css",
- "clang-tools-extra/clang-doc/tool/CMakeLists.txt",
- "clang-tools-extra/clang-doc/tool/ClangDocMain.cpp",
- "clang-tools-extra/unittests/clang-doc/CMakeLists.txt",
- "clang-tools-extra/unittests/clang-doc/ClangDocTest.cpp",
- "clang-tools-extra/unittests/clang-doc/ClangDocTest.h",
- "clang-tools-extra/unittests/clang-doc/GeneratorTest.cpp",
- "clang-tools-extra/unittests/clang-doc/HTMLGeneratorTest.cpp",
-
- "llvm/docs/BugpointRedesign.md",
- "llvm/test/Reduce/Inputs/remove-funcs.sh",
- "llvm/test/Reduce/remove-funcs.ll",
- "llvm/tools/LLVMBuild.txt",
- "llvm/tools/llvm-reduce/CMakeLists.txt",
- "llvm/tools/llvm-reduce/DeltaManager.h",
- "llvm/tools/llvm-reduce/LLVMBuild.txt",
- "llvm/tools/llvm-reduce/TestRunner.cpp",
- "llvm/tools/llvm-reduce/TestRunner.h",
- "llvm/tools/llvm-reduce/deltas/Delta.h",
- "llvm/tools/llvm-reduce/deltas/RemoveFunctions.cpp",
- "llvm/tools/llvm-reduce/deltas/RemoveFunctions.h",
- "llvm/tools/llvm-reduce/llvm-reduce.cpp",
-
- "openmp/libomptarget/test/mapping/declare_mapper_api.cpp",
-
- "unknown/lib/unknonw.cpp"
- ]
-
- where = poller._transform_path(fileList)
- for wh in where:
- where_project, where_project_files = wh
- print "category: %s" % poller._categories.get(where_project, poller.category)
- print "project: %s, files(%s): %s\n" % (where_project, len(where_project_files), where_project_files)